diff --git a/README.md b/README.md index d54dfba..fe641f4 100644 --- a/README.md +++ b/README.md @@ -109,43 +109,41 @@ Ein Benutzer kann mehreren Gruppen angehören, die Rechte addieren sich ### Zweistufiger Rechtebaum -Der Baum hat genau zwei Ebenen, exakt gespiegelt an den drei Sidebar-Gruppen. -Pro Bereich gibt es eine kompakte Tabelle: Zeilen = der Bereich selbst -„(gesamt)“ + je ein Unterpunkt, Spalten = die Rechte **R**(ead/Lesen), -**W**(rite/Schreiben, Anlegen), **E**(dit/Ändern) nebeneinander. **„Ändern“ -deckt Löschen überall mit ab** — es gibt bewusst kein eigenes -Löschen-Recht. Eine vierte Spalte **D** existiert nur bei Clients und -bedeutet dort **nicht** Löschen, sondern *PoE-Neustart über das -Dashboard-Popup auslösen* (automatische Neustarts bei Ausfall über -`poe.sh` sind davon komplett unabhängig und laufen immer). Für Zeilen/Spalten -ohne passendes Recht (z.B. „W“ beim Bereich selbst, oder „D“ bei -Switche/Zugangsdaten/Benutzer/Gruppen) wird eine ausgegraute, nicht -anklickbare Checkbox angezeigt statt die Zelle wegzulassen: +Der Baum hat genau zwei Ebenen, exakt gespiegelt an den drei Sidebar-Gruppen +(Geräte/Logs/Einstellungen). Auf der Gruppen-Seite stehen sie als drei +kompakte Spalten nebeneinander, mit einer kurzen Legende direkt darüber. +Jede Spalte ist eine kleine Tabelle: die Kopfzeile enthält in EINER Zeile +sowohl den Bereichsnamen mit seinem Kästchen (dem Kill-Switch, dicht +nebeneinander statt an den Rand gestellt) als auch die Buchstaben-Spalten +**R**(ead/Lesen), **W**(rite/Schreiben, Anlegen), **E**(dit/Ändern) — nur +die Rechte, die dieser Bereich tatsächlich nutzt. Darunter je eine +eingerückte Zeile pro Unterpunkt. **„Ändern“ deckt Löschen überall mit +ab** — es gibt bewusst kein eigenes Löschen-Recht. Eine vierte Spalte **D** +gibt es nur bei Geräte → Clients, und sie bedeutet dort **nicht** Löschen, +sondern *PoE-Neustart über das Dashboard-Popup auslösen* (automatische +Neustarts bei Ausfall über `poe.sh` sind davon komplett unabhängig und +laufen immer). Für Zeilen ohne passendes Recht (z.B. „D“ bei Switche/ +Zugangsdaten/Benutzer/Gruppen) wird eine ausgegraute, nicht anklickbare +Checkbox angezeigt statt die Zelle wegzulassen: ``` -Geräte (gesamt) R - ├─ Clients R · W · E · D (D = PoE-Neustart über Dashboard) - ├─ Switche R · W · E - └─ Zugangsdaten R · W · E -Logs (gesamt) R - ├─ Live R - └─ Änderungen R -Einstellungen (gesamt) R - ├─ Benutzer R · W · E - ├─ Gruppen R · W · E - ├─ Systemeinstellungen R · E - └─ Im-/Export R (Export) · E (Import) +☑ Geräte R W E D ☑ Logs R W E ☑ Einstellungen R W E + Clients ☑ ☑ ☑ ☑ Live ☑ ☑ ☑ Benutzer ☑ ☑ ☑ + Switche ☑ ☑ ☑ ▢ Änd. ☑ ☑ ☑ Gruppen ☑ ☑ ☑ + Zugangsd. ☑ ☑ ☑ ▢ Systemeinst. ☑ ▢ ☑ + Im-/Export ☑ ▢ ☑ ``` -Das jeweilige „Bereich an/aus“-Recht (`devices_group.view` / -`logs_group.view` / `settings_group.view`, die „(gesamt)“-Zeile) wirkt als -**Kill-Switch**: ist es für eine Gruppe nicht gesetzt, greift kein einziges -Recht darunter mehr — selbst wenn z.B. `devices.view` einzeln noch angehakt -ist. So lässt sich ein ganzer Bereich mit einem Klick sperren, ohne jedes -Unterrecht einzeln zurücknehmen zu müssen (`User.has_permission()` in -`app.py`). Auf der Gruppen-Seite sind die Unterpunkt-Zeilen einer Tabelle per -JavaScript so lange gesperrt (ausgegraut, nicht anklickbar), bis die -„(gesamt)“-Zeile angehakt ist. +Das jeweilige „Bereich sichtbar“-Recht (`devices_group.view` / +`logs_group.view` / `settings_group.view`, das Kästchen in der Kopfzeile +einer Spalte) wirkt als **Kill-Switch**: ist es für eine Gruppe nicht +gesetzt, greift kein einziges Recht darunter mehr — selbst wenn z.B. +`devices.view` einzeln noch angehakt ist. So lässt sich ein ganzer Bereich +mit einem Klick sperren, ohne jedes Unterrecht einzeln zurücknehmen zu +müssen (`User.has_permission()` in `app.py`). Auf der Gruppen-Seite sind +die Unterpunkt-Zeilen einer Spalte per JavaScript so lange gesperrt UND +sichtbar ausgegraut (nicht nur das dezente native disabled-Styling), bis +das Kopfzeilen-Kästchen angehakt ist. ### Systemgruppen diff --git a/srv/poe_manager/app.py b/srv/poe_manager/app.py index 76cb602..cafd6db 100644 --- a/srv/poe_manager/app.py +++ b/srv/poe_manager/app.py @@ -268,6 +268,18 @@ for _group in PERMISSIONS.values(): ALL_PERMISSION_KEYS.append(_key) PERMISSION_PARENT_GROUP[_key] = _group["view_key"] +# Welche Spalten (R/W/E/D) für einen Bereich überhaupt vorkommen — z.B. hat +# nur "Geräte" (wegen Clients' Dashboard-Neustart) eine D-Spalte, Logs/ +# Einstellungen brauchen sie gar nicht erst anzuzeigen. Auf der Gruppen- +# Seite bekommt so jeder Bereich nur die Spalten, die er tatsächlich nutzt, +# statt überall gleich viele (teils komplett ungenutzte) Spalten zu zeigen. +GROUP_ROW_TYPES = {} +for _group_key, _group in PERMISSIONS.items(): + _used_row_keys = {"view"} + for _child in _group["children"].values(): + _used_row_keys.update(_child["rows"].keys()) + GROUP_ROW_TYPES[_group_key] = [rt for rt in PERMISSION_ROW_TYPES if rt[0] in _used_row_keys] + DEFAULT_GROUP_NAME = "Benutzer" DEFAULT_GROUP_PERMISSIONS = [ "devices_group.view", "devices.view", "switches.view", @@ -2738,9 +2750,18 @@ def groups(): name = request.form.get("name", "").strip() if name: try: - conn.execute("INSERT INTO groups (name) VALUES (?)", (name,)) + cur = conn.execute("INSERT INTO groups (name) VALUES (?)", (name,)) + # Rechte direkt beim Anlegen setzbar (dieselbe Tabelle wie + # beim Bearbeiten, hier ohne eigenes "_submitted"-Flag, da + # eine neue Gruppe ohnehin noch keine Rechte hat). + selected_permissions = set(request.form.getlist("permissions")) & set(ALL_PERMISSION_KEYS) + if selected_permissions: + conn.executemany( + "INSERT INTO group_permissions (group_id, permission) VALUES (?, ?)", + [(cur.lastrowid, p) for p in selected_permissions], + ) conn.commit() - log_action("group.create", name) + log_action("group.create", name, f"{len(selected_permissions)} Rechte" if selected_permissions else None) flash(f"Gruppe '{name}' angelegt.", "success") except sqlite3.IntegrityError: flash("Eine Gruppe mit diesem Namen existiert bereits!", "danger") @@ -2906,6 +2927,7 @@ def groups(): all_users_all=all_users_all, permission_catalog=PERMISSIONS, permission_row_types=PERMISSION_ROW_TYPES, + group_row_types=GROUP_ROW_TYPES, permission_labels=PERMISSION_LABELS, ) diff --git a/srv/poe_manager/static/css/style.css b/srv/poe_manager/static/css/style.css index 7d8e10a..e0af12e 100644 --- a/srv/poe_manager/static/css/style.css +++ b/srv/poe_manager/static/css/style.css @@ -1057,23 +1057,55 @@ select { margin-bottom: 8px; } -/* Rechte-Tabelle (Gruppen-Detailansicht): eine kompakte Tabelle pro - Top-Level-Bereich (Geräte/Logs/Einstellungen). Erste Zeile = der Bereich - selbst "(gesamt)" (nur die R-Spalte aktiv, das ist der Kill-Switch), - weitere Zeilen = je ein Unterpunkt (Clients, Switche, ...). Spalten = - R/W/E/D nebeneinander (siehe Legende im Seitenkopf). */ -.permission-area { margin-bottom: 20px; } -.permission-area:last-child { margin-bottom: 0; } +/* Rechte-Übersicht (Gruppen-Detailansicht): die Top-Level-Bereiche + (Geräte/Logs/Einstellungen) stehen als kompakte, schmale Spalten + nebeneinander statt untereinander. Kopfzeile der Tabelle = Bereichsname + + Kill-Switch-Kästchen (eng beieinander, linksbündig) in der ERSTEN + Zelle, gefolgt von den Buchstaben-Spalten (R/W/E/D) — Name/Kästchen und + Buchstaben stehen also in derselben Zeile. Tabellenkörper = Unterpunkte + als eingerückte Zeilen, Spalten je Bereich nur die dort tatsächlich + genutzten Rechte. Legende steht direkt unter der Zeile (.permission-legend). */ +.permission-legend { + font-size: 12px; + color: var(--text-faint); + margin-top: 14px; +} +.permission-groups-row { + display: flex; + flex-wrap: wrap; + gap: 28px; +} +.permission-group-col { + /* Nicht auf eine feste Breite zwingen — Bereiche mit mehr Spalten (z.B. + Geräte mit R/W/E/D) brauchen mehr Platz als welche mit weniger (z.B. + Logs mit nur R). Feste/zu schmale Breiten haben die D-Spalte bei + Geräte zuvor lautlos in den Overflow geschoben. */ + flex: 0 0 auto; + padding-right: 28px; + border-right: 1px solid var(--border-soft); +} +.permission-group-col:last-child { border-right: none; padding-right: 0; } +.permission-group-header-cell { padding-left: 0 !important; } +.permission-group-toggle { + display: inline-flex; + align-items: center; + gap: 7px; + cursor: pointer; +} +.permission-group-name { font-size: 14px; font-weight: 650; color: var(--text); text-transform: none; letter-spacing: normal; font-family: inherit; } .permission-table { border-collapse: collapse; font-size: 13px; white-space: nowrap; } .permission-table th, .permission-table td { - padding: 7px 14px; - text-align: center; + padding: 5px 10px; border-bottom: 1px solid var(--border-soft); } +.permission-table th:not(:first-child), .permission-table td:not(:first-child) { + text-align: center; +} +.permission-table thead th { padding-bottom: 10px; border-bottom: 1px solid var(--border-soft); } .permission-table th { font-size: 11.5px; font-weight: 650; @@ -1083,14 +1115,26 @@ select { font-family: var(--font-mono); } .permission-table th:first-child { text-transform: none; font-family: inherit; letter-spacing: normal; } -.permission-table td:first-child, .permission-table th:first-child { text-align: left; padding-right: 20px; } +.permission-table td:first-child, .permission-table th:first-child { text-align: left; padding-right: 14px; padding-left: 0; } .permission-table tbody tr:last-child td { border-bottom: none; } -.permission-row-label { color: var(--text-dim); } +.permission-row-label { color: var(--text-dim); padding-left: 34px !important; } +/* Sichtbares Ausgrauen, solange der Kill-Switch des Bereichs aus ist — + zusätzlich zum disabled-Attribut, das der Browser nur dezent abblendet + (siehe applyPermissionGating() im Script-Block). */ +.permission-table tbody.permission-locked { opacity: 0.35; } .permission-table input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; } +/* Explizit zentrieren (statt sich auf text-align + native Checkbox-Ränder + zu verlassen, die je nach Browser/OS leicht asymmetrisch sind) — nur für + die Rechte-Checkboxen im Tabellenkörper, nicht für das Bereich-Kästchen + in der Kopfzeile (das steht bewusst inline neben dem Namen). */ +.permission-table tbody input[type="checkbox"] { + display: block; + margin: 0 auto; +} .permission-table input[type="checkbox"]:disabled { cursor: not-allowed; } .permission-cb-na { opacity: 0.25; accent-color: var(--text-faint) !important; } diff --git a/srv/poe_manager/static/js/app.js b/srv/poe_manager/static/js/app.js index 6b3abf2..dfb1128 100644 --- a/srv/poe_manager/static/js/app.js +++ b/srv/poe_manager/static/js/app.js @@ -160,9 +160,16 @@ form.addEventListener("submit", function (e) { if (form.dataset.confirmed === "1") return; e.preventDefault(); + // Welcher Button hat den Submit ausgelöst? Muss beim erneuten + // requestSubmit() explizit mitgegeben werden — sonst geht bei + // Formularen, die ihre ID über name/value des Submit-Buttons + // transportieren (statt über ein ), dieses + // Feld beim Neu-Absenden stillschweigend verloren (requestSubmit() + // ohne Argument zählt als "kein Button aktiviert"). + const submitter = e.submitter; window.confirmAction(form.getAttribute("data-confirm"), () => { form.dataset.confirmed = "1"; - form.requestSubmit ? form.requestSubmit() : form.submit(); + form.requestSubmit ? form.requestSubmit(submitter) : form.submit(); }, form.getAttribute("data-confirm-title")); }); }); diff --git a/srv/poe_manager/templates/groups.html b/srv/poe_manager/templates/groups.html index 68b15b0..5307d1d 100644 --- a/srv/poe_manager/templates/groups.html +++ b/srv/poe_manager/templates/groups.html @@ -5,53 +5,50 @@ {% block content %} -{# Rendert einen Bereich (Geräte/Logs/Einstellungen) als kompakte Tabelle: - Zeilen = Bereich selbst (nur die R-Spalte aktiv, das ist der Kill-Switch) - + je eine Zeile pro Unterpunkt (Clients, Switche, ...). Spalten = die - kanonischen Rechte R/W/E/D nebeneinander (permission_row_types). +{# Rendert einen Bereich (Geräte/Logs/Einstellungen) als kompakte, schmale + Spalte: eigene Kopfzeile mit Bereichsname + einem Kästchen (der + Kill-Switch, "Bereich sichtbar"), darunter eine kleine Tabelle mit den + Unterpunkten (Clients, Switche, ...) als Zeilen und nur den für diesen + Bereich tatsächlich genutzten Rechten (group_row_types[group_key]) als + Spalten — Logs/Einstellungen zeigen z.B. nur R/W/E, nur Geräte zeigt + zusätzlich D (PoE-Neustart über Dashboard, ausschließlich bei Clients). "Ändern" (E) deckt Löschen überall mit ab — es gibt nirgends ein eigenes - Löschen-Recht. "D" existiert nur bei Clients und bedeutet dort NICHT - Löschen, sondern PoE-Neustart über das Dashboard-Popup. Zellen ohne - passendes Recht für die jeweilige Zeile (z.B. "W" bei "Geräte" selbst, - oder "D" bei Switche/Zugangsdaten) werden als ausgegraute, nicht - anklickbare Checkbox dargestellt statt zu fehlen — dadurch bleiben alle - Spalten bündig untereinander. readonly=true zeigt nur den aktuellen Stand + Löschen-Recht. Zellen ohne passendes Recht für die jeweilige Zeile (z.B. + "D" bei Switche/Zugangsdaten) werden als ausgegraute, nicht anklickbare + Checkbox dargestellt statt zu fehlen — dadurch bleiben alle Spalten + bündig untereinander. readonly=true zeigt nur den aktuellen Stand (Admin/Systemgruppen bzw. fehlendes groups.edit). Die JS-Funktion applyPermissionGating() sperrt Kind-Rechte, solange das Bereich-Lesen - (Kill-Switch) nicht gesetzt ist. #} -{% macro permission_table(group, checked_keys, readonly) %} -