Statisch sichtbare Erklaerungstexte (.hint/.field-hint) app-weit auf ein
einheitliches "i"-Icon umgestellt: Hover zeigt den Text neben dem
Mauszeiger, Klick/Tap oeffnet ein wiederverwendetes Modal mit dem vollen
Text. Zentral in app.js (initHintIcons, ensureHintTooltip/-Modal) via
Event-Delegation, kein Aufwand pro Seite. Hover ist bewusst per
matchMedia("(hover: hover) and (pointer: fine)") gated -- auf Touch-
Geraeten faellt automatisch nur Tap-zu-Modal an, da echte
mouseenter/mousemove-Events dort ohnehin nicht zuverlaessig auftreten.
Nicht jede .hint/.field-hint wurde umgestellt -- nach folgenden Kriterien
entschieden:
- Live-Status-Anzeigen (aktuelle Log-Pfade, Netzwerkstatus, Auditlog-
Schwellenwerte, aktive Reservierungs-/Lease-Zaehler, certbot-Timer-
Status usw.) bleiben sichtbar, da sie aktuellen Zustand zeigen, keine
generische Erklaerung.
- Kurze, einzeilige Abschnitts-Untertitel (z.B. "Server, Bind-Konto und
Suchparameter fuer die Anbindung an AD/LDAP.") bleiben als Orientierung
direkt unter der Ueberschrift sichtbar.
- Bereits JS-live-aktualisierte Status-Felder (id-tragend, z.B.
"Fragt live... ab." -> "N Gruppen geladen.") bleiben unangetastet.
- Laengere, mehrsaetzige Abschnitts-Erklaerungen (z.B. AD-Gruppenzu-
ordnungen, Fileshare-Gruppenzuordnung, Subnetze, DHCP-Options, Wartung)
sowie so gut wie alle Feld-Erklaerungen wandern ins Icon.
Nebenbei: Karte "Fileshare-Gruppen" in LDAP-Einstellungen zu "Fileshare-
Gruppenzuordnung" umbenannt (passt jetzt zur bereits bestehenden
log_action-Zielbezeichnung in app.py).
Neue gemeinsame Macro-Datei templates/_hint_icon.html, importiert von
15 Templates. Verifiziert: alle 18 betroffenen Seiten laden fehlerfrei
(200, keine JS-Fehler), Icon-Anzahl je Seite geprueft, Hover-Tooltip und
Klick-Modal auf Desktop, Tap-Modal auf simuliertem Mobilgeraet (390x844,
Touch), sowie stichprobenartig bestaetigt dass die bewusst belassenen
Hinweise unveraendert sichtbar bleiben.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
323 lines
17 KiB
HTML
323 lines
17 KiB
HTML
{% extends "base.html" %}
|
|
{% set active_page = "settings_system" %}
|
|
{% block page_title %}Systemeinstellungen{% endblock %}
|
|
{% block page_sub %}<div class="topbar-sub">Prüfintervall und Netzwerkkonfiguration dieses Hosts</div>{% endblock %}
|
|
|
|
{% block content %}
|
|
{% import "_hint_icon.html" as hi %}
|
|
<div class="settings-grid">
|
|
|
|
<div class="card card-pad">
|
|
<div class="section-head" style="margin-bottom:16px;">
|
|
<div>
|
|
<h2 style="font-size:16px;">Host</h2>
|
|
<div class="hint">Name und Zeitzone dieses Hosts.</div>
|
|
</div>
|
|
</div>
|
|
{% if current_user.has_permission('settings_system.edit') %}
|
|
<form method="post">
|
|
<div class="field">
|
|
<label for="hostname">Hostname</label>
|
|
<input type="text" name="hostname" id="hostname" value="{{ current_hostname or '' }}" pattern="[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?" required>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
Hostname setzen
|
|
</button>
|
|
</form>
|
|
<hr style="border:none; border-top:1px solid var(--border-soft); margin:18px 0;">
|
|
<form method="post">
|
|
<div class="field">
|
|
<label for="timezone">Zeitzone {{ hi.hint_icon("Bestimmt die lokale Zeit in allen Logs und im Änderungsverlauf — wirkt sofort für diese laufende App-Instanz, ohne Dienst-Neustart.", "Zeitzone") }}</label>
|
|
<select name="timezone" id="timezone" required>
|
|
{% for tz in timezones %}
|
|
<option value="{{ tz }}" {% if tz == current_timezone %}selected{% endif %}>{{ tz }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
Zeitzone setzen
|
|
</button>
|
|
</form>
|
|
<hr style="border:none; border-top:1px solid var(--border-soft); margin:18px 0;">
|
|
<form method="post">
|
|
<div class="field">
|
|
<label for="interval">Prüfintervall (Minuten) {{ hi.hint_icon("Wie oft Geräte auf Erreichbarkeit geprüft werden. Der Hintergrund-Dienst (tesm-check.service) wird nach dem Speichern automatisch neu gestartet.", "Prüfintervall (Minuten)") }}</label>
|
|
<input type="number" name="interval" id="interval" value="{{ interval }}" min="1" required>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
Speichern & Service neustarten
|
|
</button>
|
|
</form>
|
|
{% else %}
|
|
<div class="field">
|
|
<label>Hostname</label>
|
|
<input type="text" value="{{ current_hostname or '' }}" disabled>
|
|
</div>
|
|
<div class="field">
|
|
<label>Zeitzone</label>
|
|
<input type="text" value="{{ current_timezone or '' }}" disabled>
|
|
</div>
|
|
<div class="field">
|
|
<label>Prüfintervall (Minuten)</label>
|
|
<input type="number" value="{{ interval }}" disabled>
|
|
<div class="field-hint">Nur Lesezugriff — für Änderungen fehlt das Recht „Systemeinstellungen ändern“.</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="card card-pad">
|
|
<div class="section-head" style="margin-bottom:16px;">
|
|
<div>
|
|
<h2 style="font-size:16px;">Netzwerkeinstellungen</h2>
|
|
<div class="hint">IP/DNS/DHCP-Umschaltung dieses Hosts (nicht der DHCP-<em>Server</em> unter „DHCP“).</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2" style="align-items:center; margin-bottom:12px; flex-wrap:wrap;">
|
|
{% if net_backend == 'unknown' %}
|
|
<span class="pill unknown">Kein unterstütztes Backend erkannt</span>
|
|
{% else %}
|
|
<span class="pill online">{{ {'networkmanager': 'NetworkManager', 'dhcpcd': 'dhcpcd', 'netplan': 'netplan'}[net_backend] }}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% for state in active_net_states %}
|
|
<div class="flex gap-2" style="align-items:center; margin-bottom:8px; flex-wrap:wrap;">
|
|
<span class="mono text-faint" style="font-size:12px;">
|
|
{{ state.interface }} — {{ state.ip }}/{{ state.prefix }}{% if state.gateway %}, Gateway {{ state.gateway }}{% endif %}
|
|
</span>
|
|
<span class="pill {{ 'user' if state.mode == 'static' else ('online' if state.mode == 'dhcp' else 'unknown') }}">
|
|
{{ {'static': 'Statisch', 'dhcp': 'DHCP', 'unknown': 'Modus unbekannt'}[state.mode] }}
|
|
</span>
|
|
{% if state.dns %}
|
|
<span class="text-faint" style="font-size:12px;">DNS: <span class="mono">{{ state.dns|join(', ') }}</span></span>
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-faint" style="font-size:12px; margin-bottom:8px;">Kein aktives Interface mit IPv4-Adresse gefunden.</div>
|
|
{% endfor %}
|
|
<div style="margin-bottom:16px;"></div>
|
|
|
|
{% if pending_network_token %}
|
|
<div class="card-pad" style="background:var(--warning-dim); border-radius:var(--radius-sm); margin-bottom:16px;">
|
|
<p style="margin:0 0 12px; font-size:13px;">
|
|
Neue Netzwerkkonfiguration wurde angewendet. Wenn diese Seite gerade noch lädt, funktioniert die Verbindung —
|
|
bitte bestätigen, bevor automatisch zurückgerollt wird (nach {{ net_revert_seconds }}s ohne Bestätigung).
|
|
</p>
|
|
<form method="post">
|
|
<input type="hidden" name="confirm_network" value="{{ pending_network_token }}">
|
|
<button type="submit" class="btn btn-primary">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
Verbindung funktioniert — bestätigen
|
|
</button>
|
|
</form>
|
|
</div>
|
|
{% elif net_backend != 'unknown' and current_user.has_permission('settings_system.edit') %}
|
|
<form method="post" data-confirm="Netzwerkkonfiguration wirklich ändern? Falls die Verbindung danach abbricht, wird die vorherige Konfiguration automatisch nach {{ net_revert_seconds }} Sekunden wiederhergestellt.">
|
|
<input type="hidden" name="apply_network" value="1">
|
|
<div class="field"><label>Interface {{ hi.hint_icon("Jedes Interface hat seine eigene Konfiguration — ein Wechsel hier lädt unten dessen tatsächlichen Ist-Zustand, ändert aber noch nichts.", "Interface") }}</label>
|
|
<select name="net_interface" id="netInterfaceSelect" onchange="loadNetworkState(this.value)">
|
|
{% for iface in net_interfaces %}
|
|
<option value="{{ iface }}" {% if iface == net_interface %}selected{% endif %}>{{ iface }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="field"><label>Modus</label>
|
|
<select name="net_mode" id="netModeSelect" onchange="document.getElementById('netStaticFields').classList.toggle('hidden', this.value !== 'static')">
|
|
<option value="dhcp" {% if net_state.mode != 'static' %}selected{% endif %}>DHCP (automatisch)</option>
|
|
<option value="static" {% if net_state.mode == 'static' %}selected{% endif %}>Statisch</option>
|
|
</select>
|
|
</div>
|
|
<div id="netStaticFields" class="{{ 'hidden' if net_state.mode != 'static' }}">
|
|
<div class="field"><label>IP-Adresse</label>
|
|
<input type="text" name="net_ip" id="netIpInput" value="{{ net_state.ip if net_state.mode == 'static' else '' }}" placeholder="z.B. 192.168.1.50">
|
|
</div>
|
|
<div class="field"><label>Prefix (CIDR-Bits)</label>
|
|
<input type="number" name="net_prefix" id="netPrefixInput" min="1" max="32" value="{{ net_state.prefix if net_state.mode == 'static' else '' }}" placeholder="z.B. 24">
|
|
</div>
|
|
<div class="field"><label>Gateway</label>
|
|
<input type="text" name="net_gateway" id="netGatewayInput" value="{{ net_state.gateway if net_state.mode == 'static' else '' }}" placeholder="z.B. 192.168.1.1">
|
|
</div>
|
|
</div>
|
|
<div class="field"><label>DNS-Server {{ hi.hint_icon("Kommagetrennt. Leer lassen, um die per DHCP zugewiesenen DNS-Server zu verwenden.", "DNS-Server") }}</label>
|
|
<input type="text" name="net_dns" id="netDnsInput" value="{{ net_state.dns|join(', ') if net_state and net_state.dns else '' }}" placeholder="z.B. 1.1.1.1, 8.8.8.8">
|
|
</div>
|
|
<button type="submit" class="btn btn-primary btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
Netzwerkkonfiguration anwenden
|
|
</button>
|
|
<p class="text-faint" style="font-size:11px; margin-top:10px;">
|
|
⚠ Kann die Erreichbarkeit dieses Hosts unterbrechen. Ohne Bestätigung wird automatisch nach {{ net_revert_seconds }}s zurückgerollt.
|
|
</p>
|
|
</form>
|
|
{% elif net_backend == 'unknown' %}
|
|
<p class="text-faint" style="font-size:12.5px;">
|
|
Weder NetworkManager, dhcpcd noch netplan/systemd-networkd aktiv erkannt — Netzwerkänderungen über diese Seite sind deaktiviert.
|
|
Bitte die Netzwerkkonfiguration dieses Hosts manuell vornehmen.
|
|
</p>
|
|
{% else %}
|
|
<p class="text-faint" style="font-size:12.5px;">Für Änderungen fehlt das Recht „Systemeinstellungen ändern“.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="card card-pad">
|
|
<div class="section-head" style="margin-bottom:16px;">
|
|
<div>
|
|
<h2 style="font-size:16px;">Logs</h2>
|
|
<div class="hint">Rotation & Aufbewahrung von Live-, Änderungs-, App- und Kea-DHCP-Log.</div>
|
|
</div>
|
|
</div>
|
|
{% if current_user.has_permission('settings_system.edit') %}
|
|
<form method="post">
|
|
<input type="hidden" name="save_log_rotation" value="1">
|
|
<div class="field">
|
|
<label for="log_rotation_interval">Rotations-Intervall</label>
|
|
<select name="log_rotation_interval" id="log_rotation_interval">
|
|
<option value="daily" {% if log_rotation_interval == "daily" %}selected{% endif %}>Täglich</option>
|
|
<option value="weekly" {% if log_rotation_interval == "weekly" %}selected{% endif %}>Wöchentlich</option>
|
|
<option value="monthly" {% if log_rotation_interval == "monthly" %}selected{% endif %}>Monatlich</option>
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label for="log_rotation_keep">Aufbewahrung (Anzahl Rotationen) {{ hi.hint_icon("Standard: wöchentlich, 4 Rotationen (≈ 1 Monat Historie je Log).", "Aufbewahrung (Anzahl Rotationen)") }}</label>
|
|
<input type="number" name="log_rotation_keep" id="log_rotation_keep" value="{{ log_rotation_keep }}" min="1" required>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
Speichern
|
|
</button>
|
|
</form>
|
|
{% else %}
|
|
<div class="field">
|
|
<label>Rotations-Intervall</label>
|
|
<input type="text" value="{{ log_rotation_interval }}" disabled>
|
|
</div>
|
|
<div class="field">
|
|
<label>Aufbewahrung (Anzahl Rotationen)</label>
|
|
<input type="text" value="{{ log_rotation_keep }}" disabled>
|
|
</div>
|
|
{% endif %}
|
|
<div class="field-hint mono" style="margin-top:10px; font-size:11px; line-height:1.6;">
|
|
Live: {{ log_paths.live }}<br>
|
|
Änderungen: {{ log_paths.changes }}<br>
|
|
App: {{ log_paths.app }}<br>
|
|
Kea-DHCP: {{ log_paths.kea }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card card-pad">
|
|
<div class="section-head" style="margin-bottom:16px;">
|
|
<div>
|
|
<h2 style="font-size:16px;">Papierkorb</h2>
|
|
<div class="hint">Gelöschte Clients, Switche, Zugangsdaten, Benutzer und Gruppen — Liste unter Geräte → Wartung.</div>
|
|
</div>
|
|
</div>
|
|
{% if current_user.has_permission('settings_system.edit') %}
|
|
<form method="post">
|
|
<div class="field">
|
|
<label for="trash_retention_days">Aufbewahrungsdauer (Tage) {{ hi.hint_icon("Danach werden Papierkorb-Einträge automatisch unwiderruflich gelöscht (AD/LDAP-Benutzer sind nie im Papierkorb, da sie sich beim nächsten Login automatisch neu anlegen).", "Aufbewahrungsdauer (Tage)") }}</label>
|
|
<input type="number" name="trash_retention_days" id="trash_retention_days" value="{{ trash_retention_days }}" min="1" required>
|
|
</div>
|
|
<button type="submit" name="save_trash_retention" value="1" class="btn btn-primary btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
Speichern
|
|
</button>
|
|
</form>
|
|
<form method="post" data-confirm="Alle bereits abgelaufenen Papierkorb-Einträge jetzt unwiderruflich löschen?" style="margin-top:10px;">
|
|
<input type="hidden" name="purge_trash_now" value="1">
|
|
<button type="submit" class="btn btn-sm" style="color:var(--danger); background:transparent; border-color:var(--danger-dim);">
|
|
Abgelaufene Einträge jetzt bereinigen
|
|
</button>
|
|
</form>
|
|
{% else %}
|
|
<div class="field">
|
|
<label>Aufbewahrungsdauer (Tage)</label>
|
|
<input type="number" value="{{ trash_retention_days }}" disabled>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if current_user.is_admin %}
|
|
{% macro nav_order_buttons() %}
|
|
<div class="nav-order-actions">
|
|
<button type="button" class="icon-btn" title="Nach oben" onclick="moveNavItem(this,-1)">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
|
|
</button>
|
|
<button type="button" class="icon-btn" title="Nach unten" onclick="moveNavItem(this,1)">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12l7 7 7-7"/></svg>
|
|
</button>
|
|
</div>
|
|
{% endmacro %}
|
|
<div class="card card-pad">
|
|
<div class="section-head" style="margin-bottom:16px;">
|
|
<div>
|
|
<h2 style="font-size:16px;">Navbar-Reihenfolge</h2>
|
|
<div class="hint">Reihenfolge der Sidebar-Menüpunkte inkl. Unterpunkte — gilt für alle Benutzer.</div>
|
|
</div>
|
|
</div>
|
|
<form method="post" action="{{ url_for('save_nav_order') }}" id="navOrderForm">
|
|
<ul class="nav-order-list" id="navOrderList">
|
|
{% for item in full_nav_items %}
|
|
<li data-key="{{ item.key }}">
|
|
<div class="nav-order-row">
|
|
<span>{{ item.label }}</span>
|
|
{{ nav_order_buttons() }}
|
|
</div>
|
|
<input type="hidden" name="nav_order" value="{{ item.key }}">
|
|
{% if item.children %}
|
|
<ul class="nav-order-sublist">
|
|
{% for child in item.children %}
|
|
<li data-key="{{ child.key }}">
|
|
<div class="nav-order-row">
|
|
<span>{{ child.label }}</span>
|
|
{{ nav_order_buttons() }}
|
|
</div>
|
|
<input type="hidden" name="nav_child_order_{{ item.key }}" value="{{ child.key }}">
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<button type="submit" class="btn btn-primary btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
Reihenfolge speichern
|
|
</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function moveNavItem(btn, dir) {
|
|
const li = btn.closest("li");
|
|
const target = dir === -1 ? li.previousElementSibling : li.nextElementSibling;
|
|
if (!target) return;
|
|
if (dir === -1) li.parentNode.insertBefore(li, target);
|
|
else li.parentNode.insertBefore(target, li);
|
|
}
|
|
|
|
function loadNetworkState(iface) {
|
|
fetch("{{ url_for('settings_network_state') }}?interface=" + encodeURIComponent(iface))
|
|
.then(r => r.json())
|
|
.then(state => {
|
|
if (state.error) return;
|
|
const isStatic = state.mode === "static";
|
|
document.getElementById("netModeSelect").value = isStatic ? "static" : "dhcp";
|
|
document.getElementById("netStaticFields").classList.toggle("hidden", !isStatic);
|
|
document.getElementById("netIpInput").value = isStatic ? (state.ip || "") : "";
|
|
document.getElementById("netPrefixInput").value = isStatic ? (state.prefix || "") : "";
|
|
document.getElementById("netGatewayInput").value = isStatic ? (state.gateway || "") : "";
|
|
document.getElementById("netDnsInput").value = (state.dns || []).join(", ");
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
</script>
|
|
{% endblock %}
|