Files

263 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'base.html' %}
{% block content %}
{% set appinfo = application_info() %}
<div class="toolbar"><h1>{{ t("app.settings") }}</h1></div>
<form method="post" action="/settings" data-active-settings-section="{{ settings_section }}" enctype="multipart/form-data" class="settings-form">
<section class="settings-section" data-settings-section="appearance">
<h2>{{ t("settings.appearance") }}</h2>
<label>{{ t("settings.application_title") }}<input type="text" name="title" value="{{ settings.general.title or 'AssetManager' }}" required></label>
<label>{{ t("settings.company_name") }}<input type="text" name="company_name" value="{{ settings.general.company_name or '' }}"></label>
<label>{{ t("settings.company_logo") }}<input type="file" name="logo" accept=".png,.jpg,.jpeg,.webp,.gif"></label>
{% if settings.general.logo %}<div class="logo-preview"><img src="{{ settings.general.logo }}" alt="{{ t('settings.current_logo') }}"><label class="checkbox-label"><input class="checkbox" type="checkbox" name="remove_logo"> {{ t('settings.remove_logo') }}</label></div>{% endif %}
<label>{{ t('settings.favicon', 'Browser-Tab-Logo (Favicon)') }}
<input type="file" name="favicon" accept=".png,.jpg,.jpeg,.webp,.gif">
<small>{{ t('settings.favicon_hint', 'Empfohlen: quadratisches PNG, zum Beispiel 32 × 32 oder 64 × 64 Pixel.') }}</small>
</label>
{% if settings.general.favicon %}
<div class="logo-preview favicon-preview">
<img src="{{ settings.general.favicon }}" alt="{{ t('settings.current_favicon', 'Aktuelles Browser-Tab-Logo') }}">
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="remove_favicon"> {{ t('settings.remove_favicon', 'Browser-Tab-Logo entfernen') }}</label>
</div>
{% endif %}
<label>{{ t("common.language") }}<select name="default_language">{% for language in languages %}<option value="{{ language.code }}" {% if language.code == settings.general.get("default_language", "en") %}selected{% endif %}>{{ language.native_name }}</option>{% endfor %}</select></label>
<label>{{ t('appearance.default_theme') }}
<select name="default_theme">
<option value="light" {% if settings.general.get('default_theme', 'light') == 'light' %}selected{% endif %}>{{ t('appearance.light') }}</option>
<option value="dark" {% if settings.general.get('default_theme', 'light') == 'dark' %}selected{% endif %}>{{ t('appearance.dark') }}</option>
</select>
</label>
</section>
{% set theme_colors = settings.general.get('colors', {}) %}
<section class="settings-section appearance-color-section" data-settings-section="appearance">
<div class="settings-section-heading">
<div><h2>{{ t('appearance.colors_title') }}</h2><p class="muted">{{ t('appearance.colors_help') }}</p></div>
<button type="button" class="button button-secondary" id="reset-theme-colors">{{ t('appearance.reset_colors') }}</button>
</div>
<div class="theme-color-layout">
<div class="theme-color-grid">
{% set color_fields = [
('primary_color','appearance.primary_color','#0878bd'),
('primary_hover_color','appearance.primary_hover_color','#075f94'),
('header_color','appearance.header_color','#063c66'),
('navigation_color','appearance.navigation_color','#ffffff'),
('page_background','appearance.page_background','#f2f4f6'),
('surface_color','appearance.surface_color','#ffffff'),
('text_color','appearance.text_color','#1f2933'),
('border_color','appearance.border_color','#d7dde3'),
('success_color','appearance.success_color','#198754'),
('warning_color','appearance.warning_color','#d97706'),
('danger_color','appearance.danger_color','#b42318')
] %}
{% for name, label_key, default in color_fields %}
<label class="theme-color-field">
<span>{{ t(label_key) }}</span>
<span class="theme-color-input">
<input type="color" name="{{ name }}" value="{{ theme_colors.get(name, default) }}" data-default-color="{{ default }}">
<code>{{ theme_colors.get(name, default) }}</code>
</span>
</label>
{% endfor %}
</div>
<div class="theme-live-preview" id="theme-live-preview">
<h3>{{ t('appearance.color_preview') }}</h3>
<div class="theme-preview-window">
<div class="theme-preview-header"></div>
<div class="theme-preview-body">
<div class="theme-preview-nav"></div>
<div class="theme-preview-content">
<div class="theme-preview-card"></div>
<div class="theme-preview-buttons"><i></i><i></i><i></i></div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="settings-section" data-settings-section="system">
<h2>{{ t('settings.system_information') }}</h2>
<p class="muted">{{ t('settings.system_information_file_help') }}</p>
<dl class="system-info-grid">
<dt>{{ t('settings.appinfo_path') }}</dt><dd><code>/app/config/APPINFO.json</code></dd>
<dt>{{ t('about.version') }}</dt><dd>{{ application_version() }}</dd>
<dt>{{ t('about.author') }}</dt><dd>{{ appinfo.author or '—' }}</dd>
<dt>{{ t('about.organization') }}</dt><dd>{{ appinfo.organization or '—' }}</dd>
<dt>{{ t('about.contact') }}</dt><dd>{{ appinfo.contact or '—' }}</dd>
<dt>{{ t('about.website') }}</dt><dd>{% if appinfo.website %}<a href="{{ appinfo.website }}" target="_blank" rel="noopener">{{ appinfo.website }}</a>{% else %}—{% endif %}</dd>
<dt>{{ t('about.repository') }}</dt><dd>{% if appinfo.repository %}<a href="{{ appinfo.repository }}" target="_blank" rel="noopener">{{ appinfo.repository }}</a>{% else %}—{% endif %}</dd>
<dt>{{ t('about.license') }}</dt><dd>{{ appinfo.license or '—' }}</dd>
<dt>{{ t('about.copyright') }}</dt><dd>{{ appinfo.copyright or '—' }}</dd>
</dl>
<h3>{{ t('settings.storage_information', 'Speicher und Laufzeit') }}</h3>
<p class="muted">{{ t('settings.storage_information_help', 'Die folgenden Pfade und Zustände werden zur Laufzeit im Container ermittelt.') }}</p>
<dl class="system-info-grid">
<dt>{{ t('settings.database', 'Datenbank') }}</dt>
<dd><span class="status-badge status-success">✓ {{ t('settings.database_connected', 'PostgreSQL verbunden') }}</span></dd>
<dt>{{ t('settings.config_directory', 'Konfigurationsverzeichnis') }}</dt>
<dd><code>{{ system_storage.config.path }}</code></dd>
<dt>{{ t('settings.upload_directory', 'Uploadverzeichnis') }}</dt>
<dd><code>{{ system_storage.uploads.path }}</code></dd>
<dt>{{ t('settings.log_directory', 'Logverzeichnis') }}</dt>
<dd><code>{{ system_storage.logs.path }}</code></dd>
<dt>{{ t('settings.backup_directory', 'Backupverzeichnis') }}</dt>
<dd><code>{{ system_storage.backups.path }}</code></dd>
<dt>{{ t('settings.backup_writable', 'Backupverzeichnis beschreibbar') }}</dt>
<dd>
{% if system_storage.backups.writable %}
<span class="status-badge status-success">✓ {{ t('common.yes', 'Ja') }}</span>
{% else %}
<span class="status-badge status-danger">✕ {{ t('common.no', 'Nein') }}</span>
{% if system_storage.backups.error %}<br><small class="muted">{{ system_storage.backups.error }}</small>{% endif %}
{% endif %}
</dd>
<dt>{{ t('settings.backup_mount_detected', 'Eigenes Docker-Mount erkannt') }}</dt>
<dd>{% if system_storage.backups.is_mount %}✓ {{ t('common.yes', 'Ja') }}{% else %}— {{ t('common.no', 'Nein') }}{% endif %}</dd>
<dt>{{ t('settings.latest_backup', 'Letztes Backup') }}</dt>
<dd>
{% if system_storage.latest_backup %}
<time class="local-time" datetime="{{ system_storage.latest_backup.modified_at.isoformat() }}" data-utc="{{ system_storage.latest_backup.modified_at.isoformat() }}">{{ system_storage.latest_backup.modified_at.strftime('%Y-%m-%d %H:%M:%S') }}</time>
{% else %}—{% endif %}
</dd>
<dt>{{ t('settings.backup_count', 'Anzahl vorhandener Backups') }}</dt>
<dd>{{ system_storage.backup_count }}</dd>
<dt>{{ t('settings.backup_storage_used', 'Belegter Backup-Speicher') }}</dt>
<dd>{{ '%.2f'|format(system_storage.backup_total_size / 1024 / 1024) }} MB</dd>
</dl>
</section>
<section class="settings-section" data-settings-section="appearance">
<h2>{{ t('settings.custom_css') }}</h2>
<p class="muted">{{ t('settings.custom_css_help') }}</p>
<label for="custom-css-editor">{{ t('settings.custom_css') }}
<textarea id="custom-css-editor" name="custom_css" class="custom-css-editor" rows="18" spellcheck="false" placeholder="/* Beispiel */
.inventory-tab { color: #000; }">{{ settings.general.get('custom_css', '') }}</textarea>
</label>
</section>
<section class="settings-section" data-settings-section="meshcentral">
<h2>{{ t('settings.mesh_mapping') }}</h2>
{% set mesh = settings.meshcentral %}
{% set mapping = mesh.get('category_mapping', {}) %}
<p class="muted">{{ t('settings.mesh_mapping_help') }}</p>
<label>{{ t('settings.fallback_category') }}
<select name="mesh_fallback_category_id" required>
{% for category in categories %}
<option value="{{ category.id }}" {% if category.id == (mesh.get('fallback_category_id', 1)|int) %}selected{% endif %}>{{ category.name }} (ID {{ category.id }})</option>
{% endfor %}
</select>
</label>
<div class="form-grid">
{% set mesh_types = [
('1', 'settings.mesh_type_desktop'),
('2', 'settings.mesh_type_notebook'),
('3', 'settings.mesh_type_mobile'),
('4', 'settings.mesh_type_server'),
('5', 'settings.mesh_type_5'),
('6', 'settings.mesh_type_6'),
('7', 'settings.mesh_type_7'),
('8', 'settings.mesh_type_linux_other')
] %}
{% for type_id, type_label in mesh_types %}
<label>{{ t(type_label) }} ({{ t('settings.meshcentral_type') }} {{ type_id }})
<select name="mesh_category_{{ type_id }}">
<option value="">{{ t('settings.use_fallback') }}</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if mapping.get(type_id) is not none and category.id == (mapping.get(type_id)|int) %}selected{% endif %}>{{ category.name }} (ID {{ category.id }})</option>
{% endfor %}
</select>
</label>
{% endfor %}
</div>
</section>
<section class="settings-section" data-settings-section="authentication">
<h2>{{ t('settings.authentication') }}</h2>
<p class="muted">{{ t('settings.auth_help') }}</p>
<label>{{ t('settings.auth_mode') }}
<select name="auth_mode" id="auth-mode">
<option value="none" {% if settings.authentication.mode == 'none' %}selected{% endif %}>{{ t('settings.auth_none') }}</option>
<option value="local" {% if settings.authentication.mode == 'local' %}selected{% endif %}>{{ t('settings.auth_local') }}</option>
<option value="ldap" {% if settings.authentication.mode == 'ldap' %}selected{% endif %}>LDAP / Active Directory</option>
</select>
</label>
{% set ldap = settings.authentication.get('ldap', {}) %}
<div id="ldap-settings" class="settings-subsection">
<h3>{{ t('settings.ldap_connection') }}</h3>
<div class="form-grid">
<label>{{ t('settings.ldap_server') }}<input type="text" name="ldap_server" value="{{ ldap.get('server', '') }}" placeholder="dc01.example.org"></label>
<label>{{ t('settings.port') }}<input type="number" name="ldap_port" value="{{ ldap.get('port', 389) }}" min="1" max="65535"></label>
</div>
<div class="checkbox-row"><label class="checkbox-label"><input class="checkbox" type="checkbox" name="ldap_use_ssl" {% if ldap.get('use_ssl') %}checked{% endif %}> {{ t('settings.use_ssl') }}</label><label class="checkbox-label"><input class="checkbox" type="checkbox" name="ldap_start_tls" {% if ldap.get('start_tls') %}checked{% endif %}> {{ t('settings.use_starttls') }}</label></div>
<label>{{ t('settings.bind_user') }}<input type="text" name="ldap_bind_dn" value="{{ ldap.get('bind_dn', '') }}" placeholder="svc_assetmanager@example.org"></label>
<label>{{ t('settings.bind_password_env') }}<input type="text" name="ldap_bind_password_env" value="{{ ldap.get('bind_password_env', 'LDAP_BIND_PASSWORD') }}"></label>
<label>{{ t('settings.user_base_dn') }}<input type="text" name="ldap_user_base_dn" value="{{ ldap.get('user_base_dn', '') }}" placeholder="DC=example,DC=org"></label>
<label>{{ t('settings.user_filter') }}<input type="text" name="ldap_user_filter" value="{{ ldap.get('user_filter', '(sAMAccountName={username})') }}"></label>
<div class="form-grid"><label>{{ t('settings.display_name_attribute') }}<input type="text" name="ldap_display_name_attribute" value="{{ ldap.get('display_name_attribute', 'displayName') }}"></label><label>{{ t('settings.email_attribute') }}<input type="text" name="ldap_email_attribute" value="{{ ldap.get('email_attribute', 'mail') }}"></label></div>
<div class="checkbox-row"><label class="checkbox-label"><input class="checkbox" type="checkbox" name="ldap_debug_logging" {% if ldap.get('debug_logging', false) %}checked{% endif %}> {{ t('settings.ldap_debug_logging') }}</label></div>
<p class="muted">{{ t('settings.ldap_debug_logging_help') }}</p>
<p class="muted">{{ t('settings.bind_password_help') }}</p><p><a class="button button-secondary" href="/settings/ldap-search">🔎 {{ t('ldap.title') }}</a></p>
</div>
</section>
<button type="submit" class="button button-save settings-save-button">💾 {{ t("settings.save") }}</button>
</form>
<script>
(() => { const mode=document.getElementById('auth-mode'); const ldap=document.getElementById('ldap-settings'); const update=()=>ldap.hidden=mode.value!=='ldap'; mode.addEventListener('change',update); update(); })();
</script>
<script>
(() => {
const form = document.querySelector('[data-active-settings-section]');
if (!form) return;
const active = form.dataset.activeSettingsSection || 'appearance';
form.querySelectorAll('[data-settings-section]').forEach(section => {
section.hidden = section.dataset.settingsSection !== active;
});
const save = form.querySelector('.settings-save-button');
if (save) save.hidden = active === 'system';
})();
</script>
<script>
(() => {
const fields = [...document.querySelectorAll('.theme-color-field input[type="color"]')];
const reset = document.getElementById('reset-theme-colors');
const variableNames = {
primary_color: '--preview-primary',
primary_hover_color: '--preview-primary-hover',
header_color: '--preview-header',
navigation_color: '--preview-navigation',
page_background: '--preview-page-bg',
surface_color: '--preview-surface',
text_color: '--preview-text',
border_color: '--preview-border',
success_color: '--preview-success',
warning_color: '--preview-warning',
danger_color: '--preview-danger'
};
const preview = document.getElementById('theme-live-preview');
function refresh() {
fields.forEach(input => {
input.nextElementSibling.textContent = input.value.toUpperCase();
preview?.style.setProperty(variableNames[input.name], input.value);
});
}
fields.forEach(input => input.addEventListener('input', refresh));
reset?.addEventListener('click', () => {
fields.forEach(input => { input.value = input.dataset.defaultColor; });
refresh();
});
refresh();
})();
</script>
{% endblock %}