263 lines
16 KiB
HTML
263 lines
16 KiB
HTML
{% 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 %}
|