From 37dac64abc8de1cc9a2b97eb41e615661b8aea2b Mon Sep 17 00:00:00 2001 From: alientim Date: Mon, 10 Aug 2026 14:44:02 +0200 Subject: [PATCH] =?UTF-8?q?Live-Log-Fix,=20Sidebar-Untermen=C3=BCs=20f?= =?UTF-8?q?=C3=BCr=20Ger=C3=A4te/Einstellungen,=20eigenes=20Konto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- README.md | 59 ++++++----- srv/poe_manager/app.py | 90 ++++++++++++----- srv/poe_manager/static/css/style.css | 12 ++- srv/poe_manager/templates/account.html | 99 +++++++++++++++++++ srv/poe_manager/templates/activity_log.html | 2 +- srv/poe_manager/templates/base.html | 58 ++--------- srv/poe_manager/templates/devices.html | 2 +- srv/poe_manager/templates/logs.html | 2 +- srv/poe_manager/templates/settings.html | 91 +---------------- .../templates/settings_import_export.html | 54 ++++++++++ 10 files changed, 275 insertions(+), 194 deletions(-) create mode 100644 srv/poe_manager/templates/account.html create mode 100644 srv/poe_manager/templates/settings_import_export.html diff --git a/README.md b/README.md index 7d1c640..caba770 100644 --- a/README.md +++ b/README.md @@ -44,16 +44,16 @@ Die App ermöglicht: zuletzt geändert hat. PoE-Neustarts werden bewusst **nicht** geloggt (siehe [Änderungslog](#änderungslog)) - **Verschlüsselter Import/Export**: Geräte, Switche und Zugangsdaten lassen - sich unter Einstellungen als ein mit selbstgewählter Passphrase - verschlüsseltes Bundle exportieren und auf einer neuen Umgebung wieder - importieren (siehe [Import/Export](#importexport)) -- **Eigenes Profil**: jeder Benutzer kann über einen Klick auf seinen Namen - in der Sidebar Vor-/Nachname, eigenes Passwort und ein Profilbild pflegen - (siehe [Eigenes Profil](#eigenes-profil--profilbild)) -- **Anpassbare Navigation**: Admins können die Reihenfolge der Sidebar- - Menüpunkte unter Einstellungen per Auf/Ab-Buttons festlegen; jeder Benutzer - sieht davon ohnehin nur, wofür er berechtigt ist (siehe - [Navigation anpassen](#navigation-anpassen)) + sich unter „Einstellungen → Im-/Export" als ein mit selbstgewählter + Passphrase verschlüsseltes Bundle exportieren und auf einer neuen Umgebung + wieder importieren (siehe [Import/Export](#importexport)) +- **Eigenes Konto**: das Zahnrad-Symbol neben dem eigenen Namen unten in der + Sidebar führt zu „Mein Konto“ — Vor-/Nachname, eigenes Passwort und + Profilbild pflegen (siehe [Eigenes Konto](#eigenes-konto--profilbild)) +- **Anpassbare Navigation**: Admins legen dort zusätzlich die Reihenfolge der + Sidebar-Hauptpunkte per Auf-/Ab-Buttons fest; jeder Benutzer sieht davon + ohnehin nur, wofür er berechtigt ist — auch pro Unterpunkt innerhalb einer + Gruppe (siehe [Navigation anpassen](#navigation-anpassen)) ## Was ist neu gegenüber dem Original? @@ -185,24 +185,35 @@ angelegt. Switche referenzieren ihre Zugangsdaten dabei über den Namen (nicht die interne ID), damit der Import auch bei abweichender ID-Vergabe auf dem Zielsystem korrekt verknüpft. -## Eigenes Profil & Profilbild +## Eigenes Konto & Profilbild -Ein Klick auf den eigenen Namen unten in der Sidebar öffnet „Mein Profil“: -Vor-/Nachname ändern, ein Profilbild hochladen (PNG/JPG/GIF/WEBP) und das -eigene Passwort ändern (erfordert Eingabe des aktuellen Passworts). Das ist -bewusst getrennt vom Admin-Bereich „Benutzer“, der andere Konten verwaltet. -Ist ein Profilbild hinterlegt, erscheint es statt der Initialen sowohl in -der Sidebar als auch im Änderungslog neben dem Benutzernamen. +Das Zahnrad-Symbol neben dem eigenen Namen unten in der Sidebar führt zu +**„Mein Konto“** (`/account`): Vor-/Nachname ändern, ein Profilbild +hochladen (PNG/JPG/GIF/WEBP) und das eigene Passwort ändern (erfordert +Eingabe des aktuellen Passworts). Das ist bewusst getrennt vom Admin-Bereich +„Benutzer“, der andere Konten verwaltet. Ist ein Profilbild hinterlegt, +erscheint es statt der Initialen sowohl in der Sidebar als auch im +Änderungslog neben dem Benutzernamen. Admins sehen auf derselben Seite +zusätzlich die Navbar-Reihenfolge (siehe unten). ## Navigation anpassen -Die Reihenfolge der Sidebar-Menüpunkte ist unter **Einstellungen** per -Auf-/Ab-Buttons konfigurierbar (gespeichert als JSON-Liste unter -`settings.nav_order`) und gilt global für alle Benutzer. Unabhängig von der -Reihenfolge sieht jeder Benutzer ohnehin nur die Punkte, für die er auch -berechtigt ist — ein Benutzer ohne jede Gruppe sieht z.B. nur „Dashboard“. -„Logs“ ist dabei eine aufklappbare Gruppe mit den Unterpunkten „Live-Log“ -und „Änderungslog“. +Die Sidebar ist in drei aufklappbare Gruppen strukturiert: + +- **Geräte** — Clients (die eigentlichen Devices), Switche, Zugangsdaten +- **Einstellungen** — Benutzer, Gruppen, Systemeinstellungen (Prüfintervall), + Im-/Export +- **Logs** — Live, Änderungen (Änderungslog) + +Die Reihenfolge dieser drei Gruppen (und von „Dashboard“) ist für Admins +unter **Mein Konto** (Zahnrad-Symbol in der Sidebar) per Auf-/Ab-Buttons +konfigurierbar (gespeichert als JSON-Liste unter `settings.nav_order`) und +gilt global für alle Benutzer. Unabhängig von der Reihenfolge sieht jeder +Benutzer ohnehin nur, wofür er berechtigt ist — sowohl auf Gruppenebene als +auch pro Unterpunkt: hat ein Benutzer z.B. nur `devices.view`, erscheint die +Gruppe „Geräte“ nur mit dem Unterpunkt „Clients“ (ohne Switche/ +Zugangsdaten); eine Gruppe verschwindet komplett, wenn kein Unterpunkt +sichtbar ist. ## Entwicklung unter WSL diff --git a/srv/poe_manager/app.py b/srv/poe_manager/app.py index cd8e5a7..095d765 100644 --- a/srv/poe_manager/app.py +++ b/srv/poe_manager/app.py @@ -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")) # --------------------------------------------------------------------------- diff --git a/srv/poe_manager/static/css/style.css b/srv/poe_manager/static/css/style.css index 427c961..6e4b93c 100644 --- a/srv/poe_manager/static/css/style.css +++ b/srv/poe_manager/static/css/style.css @@ -200,6 +200,14 @@ button { font-family: inherit; } .nav-group.expanded .nav-group-children { display: flex; } .nav-group-children .nav-item { font-size: 13px; padding: 8px 12px; } +/* Kacheln nebeneinander (z.B. Im-/Export, Konto-Seite) */ +.settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); + gap: 24px; + align-items: start; +} + /* Navbar-Reihenfolge (Settings) */ .nav-order-list { list-style: none; @@ -247,7 +255,9 @@ button { font-family: inherit; } flex-shrink: 0; } -.user-meta { overflow: hidden; } +.user-meta { flex: 1; min-width: 0; overflow: hidden; } +.user-chip .icon-btn { flex-shrink: 0; width: 30px; height: 30px; } +.user-chip .icon-btn svg { width: 15px; height: 15px; } .user-meta .u-name { font-size: 13px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .user-meta .u-role { font-size: 11px; color: var(--text-faint); } diff --git a/srv/poe_manager/templates/account.html b/srv/poe_manager/templates/account.html new file mode 100644 index 0000000..767ae4d --- /dev/null +++ b/srv/poe_manager/templates/account.html @@ -0,0 +1,99 @@ +{% extends "base.html" %} +{% block page_title %}Mein Konto{% endblock %} +{% block page_sub %}
{{ current_user.username }}
{% endblock %} + +{% block content %} + +{% if current_user.is_admin %} +
+
+
+

Navbar-Reihenfolge

+
Bestimmt, in welcher Reihenfolge die Menüpunkte in der Sidebar erscheinen. Jeder Benutzer sieht davon nur, wofür er auch berechtigt ist.
+
+
+ +
+{% endif %} + +
+ +
+
+
+

Profil

+
Name und Profilbild ändern sich in der Sidebar und im Änderungslog.
+
+
+ +
+ {% if current_user.avatar_url %} + + {% else %} +
{{ current_user.username[:2]|upper }}
+ {% endif %} +
+ + + +
+
+ +
+
+
+
+ +
+
+ +
+
+
+

Passwort ändern

+
Erfordert Eingabe des aktuellen Passworts.
+
+
+
+
+
+
+ +
+
+ +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/srv/poe_manager/templates/activity_log.html b/srv/poe_manager/templates/activity_log.html index 9ae9272..404ea49 100644 --- a/srv/poe_manager/templates/activity_log.html +++ b/srv/poe_manager/templates/activity_log.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {% set active_page = "logs" %} -{% block page_title %}Änderungslog{% endblock %} +{% block page_title %}Änderungen{% endblock %} {% block page_sub %}
{{ entries|length }} Einträge (letzte 500)
{% endblock %} {% block content %} diff --git a/srv/poe_manager/templates/base.html b/srv/poe_manager/templates/base.html index 3459458..121f0c6 100644 --- a/srv/poe_manager/templates/base.html +++ b/srv/poe_manager/templates/base.html @@ -21,6 +21,8 @@ "history": '', "sliders": '', "logout": '', + "gear": '', + "transfer": '', } %}
@@ -65,7 +67,7 @@
- - - {% endif %}
@@ -171,12 +133,6 @@ {% endwith %} - {% block scripts %}{% endblock %} diff --git a/srv/poe_manager/templates/devices.html b/srv/poe_manager/templates/devices.html index 783541b..14a3c77 100644 --- a/srv/poe_manager/templates/devices.html +++ b/srv/poe_manager/templates/devices.html @@ -6,7 +6,7 @@ {% set can_delete = current_user.has_permission('devices.delete') %} {% set show_actions_col = can_edit or can_delete %} {% set col_count = 5 + (1 if can_toggle else 0) + (1 if show_actions_col else 0) %} -{% block page_title %}Geräte{% endblock %} +{% block page_title %}Clients{% endblock %} {% block page_sub %}
{{ devices|length }} Geräte
{% endblock %} {% block topbar_right %} {% if can_create %} diff --git a/srv/poe_manager/templates/logs.html b/srv/poe_manager/templates/logs.html index 035f20b..43b39d1 100644 --- a/srv/poe_manager/templates/logs.html +++ b/srv/poe_manager/templates/logs.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {% set active_page = "logs" %} -{% block page_title %}Live-Log{% endblock %} +{% block page_title %}Live{% endblock %} {% block page_sub %}
{{ log_name or "kein Logfile" }}
{% endblock %} {% block topbar_right %} Update in --s diff --git a/srv/poe_manager/templates/settings.html b/srv/poe_manager/templates/settings.html index a22eb03..7114a5f 100644 --- a/srv/poe_manager/templates/settings.html +++ b/srv/poe_manager/templates/settings.html @@ -1,41 +1,10 @@ {% extends "base.html" %} -{% set active_page = "settings" %} -{% block page_title %}Einstellungen{% endblock %} +{% set active_page = "settings_system" %} +{% block page_title %}Systemeinstellungen{% endblock %} {% block page_sub %}
Prüfintervall für das Monitoring
{% endblock %} {% block content %}
-
-
-

Navbar-Reihenfolge

-
Bestimmt, in welcher Reihenfolge die Menüpunkte in der Sidebar erscheinen. Jeder Benutzer sieht davon nur, wofür er auch berechtigt ist.
-
-
- -
- -

Prüfintervall

@@ -54,60 +23,4 @@
- -
-
-
-

Export

-
Geräte, Switche und Zugangsdaten als verschlüsseltes Bundle sichern — z.B. für einen Umzug auf eine neue Umgebung.
-
-
-
-
- - -
Wird zum Verschlüsseln der Export-Datei benötigt — für den späteren Import dieselbe Passphrase erneut eingeben.
-
- -
-
- -
-
-
-

Import

-
Ein zuvor exportiertes Bundle einlesen. Bestehende Einträge mit gleichem Namen/Hostname/MAC werden aktualisiert, neue werden angelegt.
-
-
-
-
- - -
-
- - -
- -
-
-{% endblock %} - -{% block scripts %} - {% endblock %} diff --git a/srv/poe_manager/templates/settings_import_export.html b/srv/poe_manager/templates/settings_import_export.html new file mode 100644 index 0000000..a30a3b9 --- /dev/null +++ b/srv/poe_manager/templates/settings_import_export.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} +{% set active_page = "settings_importexport" %} +{% block page_title %}Im-/Export{% endblock %} +{% block page_sub %}
Umzug auf eine neue Umgebung
{% endblock %} + +{% block content %} +
+ +
+
+
+

Export

+
Geräte, Switche und Zugangsdaten als verschlüsseltes Bundle sichern — z.B. für einen Umzug auf eine neue Umgebung.
+
+
+
+
+ + +
Wird zum Verschlüsseln der Export-Datei benötigt — für den späteren Import dieselbe Passphrase erneut eingeben.
+
+ +
+
+ +
+
+
+

Import

+
Ein zuvor exportiertes Bundle einlesen. Bestehende Einträge mit gleichem Namen/Hostname/MAC werden aktualisiert, neue werden angelegt.
+
+
+
+
+ + +
+
+ + +
+ +
+
+ +
+{% endblock %}