Live-Log-Fix, Sidebar-Untermenüs für Geräte/Einstellungen, eigenes Konto

- Live-Log-Bug behoben: _latest_log_file() sortierte nach Datei-ctime statt
  nach Dateiname — auf manchen Dateisystemen (u.a. unter WSL2 auf einem
  gemounteten Windows-Laufwerk) unzuverlässig und lieferte nicht immer das
  tatsächlich neueste Logfile. Sortiert jetzt wie der Rest des Codes über
  den chronologisch sortierbaren Dateinamen (rpi-YYYYMMDDHHMMSS.log).
- Navbar umstrukturiert in aufklappbare Gruppen mit Unterpunkten:
  - "Geräte": Clients (bisherige Devices-Seite), Switche, Zugangsdaten
  - "Einstellungen": Benutzer, Gruppen, Systemeinstellungen (Prüfintervall),
    Im-/Export
  - "Logs": Live, Änderungen (vormals Live-Log/Änderungslog)
  Sichtbarkeit gilt jetzt auch pro Unterpunkt: eine Gruppe erscheint nur,
  wenn mindestens ein Unterpunkt für den Benutzer sichtbar ist, und zeigt
  dann auch nur die sichtbaren Unterpunkte.
- Im-/Export als eigene Unterseite mit Export/Import nebeneinander
  (.settings-grid).
- Neue Seite "Mein Konto" (/account, erreichbar über ein Zahnrad-Symbol
  neben dem eigenen Namen in der Sidebar) ersetzt das bisherige Profil-Modal:
  Profil/Passwort/Profilbild als volle Seite, dazu für Admins die
  Navbar-Reihenfolge (aus den Systemeinstellungen hierher verschoben).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 14:44:02 +02:00
co-authored by Claude Sonnet 5
parent a7a23b7c12
commit 37dac64abc
10 changed files with 275 additions and 194 deletions
+64 -26
View File
@@ -150,36 +150,43 @@ DEFAULT_GROUP_NAME = "Benutzer"
DEFAULT_GROUP_PERMISSIONS = ["devices.view", "switches.view"]
# ---------------------------------------------------------------------------
# Navbar — Reihenfolge ist admin-konfigurierbar (siehe settings(), gespeichert
# als JSON-Liste von Keys unter settings.nav_order); standardmäßig wird pro
# Benutzer nur angezeigt, wofür er auch tatsächlich eine Berechtigung hat.
# "logs" ist eine Gruppe mit Untermenü (Live-Log + Änderungslog).
# Navbar — die Reihenfolge der TOP-LEVEL-Punkte ist admin-konfigurierbar
# (siehe /account, gespeichert als JSON-Liste von Keys unter
# settings.nav_order); standardmäßig wird pro Benutzer nur angezeigt, wofür
# er auch tatsächlich eine Berechtigung hat — das gilt auch pro Unterpunkt
# innerhalb einer Gruppe: eine Gruppe erscheint nur, wenn mindestens ein
# Unterpunkt sichtbar ist, und zeigt dann auch nur die sichtbaren Unterpunkte.
# ---------------------------------------------------------------------------
NAV_ITEMS = [
{"key": "index", "label": "Dashboard", "icon": "grid", "endpoint": "index"},
{"key": "devices", "label": "Geräte", "icon": "cpu", "endpoint": "devices"},
{"key": "switches", "label": "Switche", "icon": "share", "endpoint": "switches"},
{"key": "credentials", "label": "Zugangsdaten", "icon": "key", "endpoint": "credentials"},
{"key": "users", "label": "Benutzer", "icon": "users", "endpoint": "users"},
{"key": "groups", "label": "Gruppen", "icon": "groups", "endpoint": "groups"},
{"key": "logs", "label": "Logs", "icon": "terminal", "children": [
{"key": "logs_live", "label": "Live-Log", "icon": "terminal", "endpoint": "logs"},
{"key": "logs_activity", "label": "Änderungslog", "icon": "history", "endpoint": "activity_log"},
{"key": "devices_group", "label": "Geräte", "icon": "cpu", "children": [
{"key": "devices", "label": "Clients", "icon": "cpu", "endpoint": "devices"},
{"key": "switches", "label": "Switche", "icon": "share", "endpoint": "switches"},
{"key": "credentials", "label": "Zugangsdaten", "icon": "key", "endpoint": "credentials"},
]},
{"key": "settings_group", "label": "Einstellungen", "icon": "sliders", "children": [
{"key": "users", "label": "Benutzer", "icon": "users", "endpoint": "users"},
{"key": "groups", "label": "Gruppen", "icon": "groups", "endpoint": "groups"},
{"key": "settings_system", "label": "Systemeinstellungen", "icon": "sliders", "endpoint": "settings"},
{"key": "settings_importexport", "label": "Im-/Export", "icon": "transfer", "endpoint": "settings_import_export"},
]},
{"key": "logs_group", "label": "Logs", "icon": "terminal", "children": [
{"key": "logs_live", "label": "Live", "icon": "terminal", "endpoint": "logs"},
{"key": "logs_activity", "label": "Änderungen", "icon": "history", "endpoint": "activity_log"},
]},
{"key": "settings", "label": "Einstellungen", "icon": "sliders", "endpoint": "settings"},
]
DEFAULT_NAV_ORDER = [item["key"] for item in NAV_ITEMS]
NAV_ITEMS_BY_KEY = {item["key"]: item for item in NAV_ITEMS}
def _nav_item_visible(key, user):
def _nav_key_visible(key, user):
if key == "index":
return True
if key == "devices":
return user.can_view_devices
if key in ("switches", "credentials"):
return user.can_manage_switches
if key in ("users", "groups", "logs", "settings"):
if key in ("users", "groups", "settings_system", "settings_importexport", "logs_live", "logs_activity"):
return user.is_admin
return False
@@ -194,7 +201,16 @@ def inject_nav():
stored_order = []
ordered_keys = [k for k in stored_order if k in NAV_ITEMS_BY_KEY]
ordered_keys += [k for k in DEFAULT_NAV_ORDER if k not in ordered_keys]
nav_items_ordered = [NAV_ITEMS_BY_KEY[k] for k in ordered_keys if _nav_item_visible(k, current_user)]
nav_items_ordered = []
for k in ordered_keys:
item = NAV_ITEMS_BY_KEY[k]
if item.get("children"):
visible_children = [c for c in item["children"] if _nav_key_visible(c["key"], current_user)]
if visible_children:
nav_items_ordered.append({**item, "children": visible_children})
elif _nav_key_visible(k, current_user):
nav_items_ordered.append(item)
return {"nav_items_ordered": nav_items_ordered}
@@ -507,12 +523,19 @@ def logout():
# ---------------------------------------------------------------------------
# Eigenes Profil — jeder eingeloggte Benutzer darf Vor-/Nachname, eigenes
# Passwort und Profilbild selbst ändern (Klick auf den eigenen Namen in der
# Sidebar). Bewusst getrennt vom Admin-"Users"-Bereich, der andere Benutzer
# verwaltet.
# Eigenes Profil / Konto — jeder eingeloggte Benutzer darf Vor-/Nachname,
# eigenes Passwort und Profilbild selbst ändern (Zahnrad-Symbol neben dem
# eigenen Namen unten in der Sidebar). Bewusst getrennt vom Admin-"Users"-
# Bereich, der andere Benutzer verwaltet. Admins konfigurieren hier
# zusätzlich die Navbar-Reihenfolge (siehe save_nav_order()).
# ---------------------------------------------------------------------------
@app.route("/account")
@login_required
def account():
return render_template("account.html")
@app.route("/profile", methods=["POST"])
@login_required
def profile():
@@ -576,10 +599,16 @@ def profile():
# ---------------------------------------------------------------------------
def _latest_log_file():
"""Neuestes Logfile ermitteln. Bewusst über den Dateinamen sortiert
(rpi-YYYYMMDDHHMMSS.log ist lexikografisch = chronologisch sortierbar),
NICHT über Dateisystem-Metadaten wie ctime/mtime — die sind je nach
Dateisystem (z.B. unter WSL2 auf einem gemounteten Windows-Laufwerk)
unzuverlässig und haben in der Praxis nicht immer das tatsächlich
zuletzt geschriebene Logfile geliefert."""
log_files = glob.glob(LOG_GLOB)
if not log_files:
return None
return max(log_files, key=os.path.getctime)
return max(log_files)
def get_last_seen(dev_name: str):
@@ -758,6 +787,15 @@ def settings():
return render_template("settings.html", interval=interval)
@app.route("/settings/import-export")
@login_required
def settings_import_export():
if not current_user.is_admin:
flash("Nur Admins dürfen Daten importieren/exportieren!", "danger")
return redirect(url_for("index"))
return render_template("settings_import_export.html")
@app.route("/settings/nav-order", methods=["POST"])
@login_required
def save_nav_order():
@@ -770,7 +808,7 @@ def save_nav_order():
set_setting("nav_order", json.dumps(order))
log_action("settings.update", "Navbar-Reihenfolge")
flash("Navbar-Reihenfolge gespeichert.", "success")
return redirect(url_for("settings"))
return redirect(url_for("account"))
# ---------------------------------------------------------------------------
@@ -791,7 +829,7 @@ def export_data():
passphrase = request.form.get("export_passphrase", "")
if not passphrase:
flash("Bitte eine Passphrase für den Export angeben.", "danger")
return redirect(url_for("settings"))
return redirect(url_for("settings_import_export"))
conn = get_db_connection()
credentials_rows = conn.execute("SELECT name, username, password FROM credentials").fetchall()
@@ -850,7 +888,7 @@ def import_data():
upload = request.files.get("import_file")
if not passphrase or not upload or not upload.filename:
flash("Bitte Passphrase und Export-Datei angeben.", "danger")
return redirect(url_for("settings"))
return redirect(url_for("settings_import_export"))
try:
envelope = json.loads(upload.read().decode("utf-8"))
@@ -859,7 +897,7 @@ def import_data():
payload = json.loads(export_fernet.decrypt(envelope["payload"].encode("utf-8")).decode("utf-8"))
except Exception:
flash("Import fehlgeschlagen: Datei ungültig oder Passphrase falsch.", "danger")
return redirect(url_for("settings"))
return redirect(url_for("settings_import_export"))
conn = get_db_connection()
n_cred = n_switch = n_dev = 0
@@ -898,7 +936,7 @@ def import_data():
log_action("data.import", "Devices/Switches/Zugangsdaten", f"{n_dev} Geräte, {n_switch} Switche, {n_cred} Zugangsdaten")
flash(f"Import abgeschlossen: {n_cred} Zugangsdaten, {n_switch} Switche, {n_dev} Geräte.", "success")
return redirect(url_for("settings"))
return redirect(url_for("settings_import_export"))
# ---------------------------------------------------------------------------