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:
+64
-26
@@ -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"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user