Fix: Benutzer-Löschen im Browser kaputt (JS), Gruppen-Rechtetabelle überarbeitet

Bugfix (von dir gemeldet, live auf Port 5000 gefunden):
- initConfirmables() in app.js rief beim bestätigten Absenden
  form.requestSubmit() OHNE den ursprünglichen Submitter auf. Laut Spec
  wird dann KEIN Submit-Button-Name/Value mitgeschickt. users.html trug
  die Benutzer-ID als name/value des Löschen-Buttons selbst (statt eines
  verstecken Inputs wie überall sonst) — dadurch kam "delete_user" nie
  im Request an, der Klick tat scheinbar nichts. Fix: e.submitter beim
  ersten Klick merken und an requestSubmit(submitter) weiterreichen
  (behebt es grundsätzlich für alle Formulare dieser Art); zusätzlich
  users.html auf das robustere hidden-input-Muster umgestellt wie
  credentials/devices/groups. Mit echtem Playwright-Browserklick
  (nicht nur curl) verifiziert.

Gruppen-Rechtetabelle, mehrere Korrekturrunden nach deinem Mockup:
- Bereichsname + Kill-Switch-Kästchen jetzt eng nebeneinander in der
  Tabellen-Kopfzeile (nicht mehr an den Rand gestellt), R/W/E/D-Spalten
  in derselben Zeile statt einer zweiten.
- Nur die von einem Bereich tatsächlich genutzten Spalten werden gezeigt
  (GROUP_ROW_TYPES) — Logs zeigt nur R, Einstellungen nur R/W/E, nur
  Geräte zeigt zusätzlich D.
- Bugfix: D-Spalte bei Geräte verschwand durch zu schmale Flex-Breite der
  Spalte im Overflow, obwohl serverseitig korrekt gerendert — behoben
  durch flex:0 0 auto statt fester Breite.
- Checkbox-Zentrierung robust per display:block+margin:auto statt sich
  auf native Checkbox-Ränder zu verlassen (waren leicht asymmetrisch).
- Unterpunkte deutlich weiter eingerückt (34px) für klare Hierarchie.
- Sichtbares Ausgrauen (nicht nur natives disabled) der Unterpunkte,
  solange der Bereich-Kill-Switch aus ist (JS setzt .permission-locked).
- Legende jetzt UNTER der Rechtetabelle statt darüber.
- Rechte lassen sich jetzt direkt beim Anlegen einer neuen Gruppe setzen
  (Modal zeigt dieselbe Rechtetabelle, add_group-Route verarbeitet
  mitgesendete permissions[] im selben Request) statt sie erst danach
  über die Gruppentabelle nachzupflegen.

Alles inkl. Erstellen-mit-Rechten und Bearbeiten-Regression live per
Playwright verifiziert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 09:52:01 +02:00
co-authored by Claude Sonnet 5
parent 8e4a874943
commit 7d7239e1af
6 changed files with 172 additions and 91 deletions
+31 -33
View File
@@ -109,43 +109,41 @@ Ein Benutzer kann mehreren Gruppen angehören, die Rechte addieren sich
### Zweistufiger Rechtebaum ### Zweistufiger Rechtebaum
Der Baum hat genau zwei Ebenen, exakt gespiegelt an den drei Sidebar-Gruppen. Der Baum hat genau zwei Ebenen, exakt gespiegelt an den drei Sidebar-Gruppen
Pro Bereich gibt es eine kompakte Tabelle: Zeilen = der Bereich selbst (Geräte/Logs/Einstellungen). Auf der Gruppen-Seite stehen sie als drei
„(gesamt)“ + je ein Unterpunkt, Spalten = die Rechte **R**(ead/Lesen), kompakte Spalten nebeneinander, mit einer kurzen Legende direkt darüber.
**W**(rite/Schreiben, Anlegen), **E**(dit/Ändern) nebeneinander. **„Ändern“ Jede Spalte ist eine kleine Tabelle: die Kopfzeile enthält in EINER Zeile
deckt Löschen überall mit ab** — es gibt bewusst kein eigenes sowohl den Bereichsnamen mit seinem Kästchen (dem Kill-Switch, dicht
Löschen-Recht. Eine vierte Spalte **D** existiert nur bei Clients und nebeneinander statt an den Rand gestellt) als auch die Buchstaben-Spalten
bedeutet dort **nicht** Löschen, sondern *PoE-Neustart über das **R**(ead/Lesen), **W**(rite/Schreiben, Anlegen), **E**(dit/Ändern) — nur
Dashboard-Popup auslösen* (automatische Neustarts bei Ausfall über die Rechte, die dieser Bereich tatsächlich nutzt. Darunter je eine
`poe.sh` sind davon komplett unabhängig und laufen immer). Für Zeilen/Spalten eingerückte Zeile pro Unterpunkt. **„Ändern“ deckt Löschen überall mit
ohne passendes Recht (z.B. „W“ beim Bereich selbst, oder „D“ bei ab** — es gibt bewusst kein eigenes Löschen-Recht. Eine vierte Spalte **D**
Switche/Zugangsdaten/Benutzer/Gruppen) wird eine ausgegraute, nicht gibt es nur bei Geräte → Clients, und sie bedeutet dort **nicht** Löschen,
anklickbare Checkbox angezeigt statt die Zelle wegzulassen: 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 Geräte R W E D ☑ Logs R W E ☑ Einstellungen R W E
├─ Clients R · W · E · D (D = PoE-Neustart über Dashboard) Clients ☑ ☑ ☑ ☑ Live ☑ ☑ ☑ Benutzer ☑ ☑ ☑
├─ Switche R · W · E Switche ☑ ☑ ☑ ▢ Änd. ☑ ☑ ☑ Gruppen ☑ ☑ ☑
└─ Zugangsdaten R · W · E Zugangsd. ☑ ☑ ☑ ▢ Systemeinst. ☑ ▢ ☑
Logs (gesamt) R Im-/Export ☑ ▢ ☑
├─ Live R
└─ Änderungen R
Einstellungen (gesamt) R
├─ Benutzer R · W · E
├─ Gruppen R · W · E
├─ Systemeinstellungen R · E
└─ Im-/Export R (Export) · E (Import)
``` ```
Das jeweilige „Bereich an/aus“-Recht (`devices_group.view` / Das jeweilige „Bereich sichtbar“-Recht (`devices_group.view` /
`logs_group.view` / `settings_group.view`, die „(gesamt)“-Zeile) wirkt als `logs_group.view` / `settings_group.view`, das Kästchen in der Kopfzeile
**Kill-Switch**: ist es für eine Gruppe nicht gesetzt, greift kein einziges einer Spalte) wirkt als **Kill-Switch**: ist es für eine Gruppe nicht
Recht darunter mehr — selbst wenn z.B. `devices.view` einzeln noch angehakt gesetzt, greift kein einziges Recht darunter mehr — selbst wenn z.B.
ist. So lässt sich ein ganzer Bereich mit einem Klick sperren, ohne jedes `devices.view` einzeln noch angehakt ist. So lässt sich ein ganzer Bereich
Unterrecht einzeln zurücknehmen zu müssen (`User.has_permission()` in mit einem Klick sperren, ohne jedes Unterrecht einzeln zurücknehmen zu
`app.py`). Auf der Gruppen-Seite sind die Unterpunkt-Zeilen einer Tabelle per müssen (`User.has_permission()` in `app.py`). Auf der Gruppen-Seite sind
JavaScript so lange gesperrt (ausgegraut, nicht anklickbar), bis die die Unterpunkt-Zeilen einer Spalte per JavaScript so lange gesperrt UND
„(gesamt)“-Zeile angehakt ist. sichtbar ausgegraut (nicht nur das dezente native disabled-Styling), bis
das Kopfzeilen-Kästchen angehakt ist.
### Systemgruppen ### Systemgruppen
+24 -2
View File
@@ -268,6 +268,18 @@ for _group in PERMISSIONS.values():
ALL_PERMISSION_KEYS.append(_key) ALL_PERMISSION_KEYS.append(_key)
PERMISSION_PARENT_GROUP[_key] = _group["view_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_NAME = "Benutzer"
DEFAULT_GROUP_PERMISSIONS = [ DEFAULT_GROUP_PERMISSIONS = [
"devices_group.view", "devices.view", "switches.view", "devices_group.view", "devices.view", "switches.view",
@@ -2738,9 +2750,18 @@ def groups():
name = request.form.get("name", "").strip() name = request.form.get("name", "").strip()
if name: if name:
try: 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() 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") flash(f"Gruppe '{name}' angelegt.", "success")
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
flash("Eine Gruppe mit diesem Namen existiert bereits!", "danger") flash("Eine Gruppe mit diesem Namen existiert bereits!", "danger")
@@ -2906,6 +2927,7 @@ def groups():
all_users_all=all_users_all, all_users_all=all_users_all,
permission_catalog=PERMISSIONS, permission_catalog=PERMISSIONS,
permission_row_types=PERMISSION_ROW_TYPES, permission_row_types=PERMISSION_ROW_TYPES,
group_row_types=GROUP_ROW_TYPES,
permission_labels=PERMISSION_LABELS, permission_labels=PERMISSION_LABELS,
) )
+55 -11
View File
@@ -1057,23 +1057,55 @@ select {
margin-bottom: 8px; margin-bottom: 8px;
} }
/* Rechte-Tabelle (Gruppen-Detailansicht): eine kompakte Tabelle pro /* Rechte-Übersicht (Gruppen-Detailansicht): die Top-Level-Bereiche
Top-Level-Bereich (Geräte/Logs/Einstellungen). Erste Zeile = der Bereich (Geräte/Logs/Einstellungen) stehen als kompakte, schmale Spalten
selbst "(gesamt)" (nur die R-Spalte aktiv, das ist der Kill-Switch), nebeneinander statt untereinander. Kopfzeile der Tabelle = Bereichsname
weitere Zeilen = je ein Unterpunkt (Clients, Switche, ...). Spalten = + Kill-Switch-Kästchen (eng beieinander, linksbündig) in der ERSTEN
R/W/E/D nebeneinander (siehe Legende im Seitenkopf). */ Zelle, gefolgt von den Buchstaben-Spalten (R/W/E/D) — Name/Kästchen und
.permission-area { margin-bottom: 20px; } Buchstaben stehen also in derselben Zeile. Tabellenkörper = Unterpunkte
.permission-area:last-child { margin-bottom: 0; } 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 { .permission-table {
border-collapse: collapse; border-collapse: collapse;
font-size: 13px; font-size: 13px;
white-space: nowrap; white-space: nowrap;
} }
.permission-table th, .permission-table td { .permission-table th, .permission-table td {
padding: 7px 14px; padding: 5px 10px;
text-align: center;
border-bottom: 1px solid var(--border-soft); 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 { .permission-table th {
font-size: 11.5px; font-size: 11.5px;
font-weight: 650; font-weight: 650;
@@ -1083,14 +1115,26 @@ select {
font-family: var(--font-mono); font-family: var(--font-mono);
} }
.permission-table th:first-child { text-transform: none; font-family: inherit; letter-spacing: normal; } .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-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"] { .permission-table input[type="checkbox"] {
width: 16px; height: 16px; width: 16px; height: 16px;
accent-color: var(--accent); accent-color: var(--accent);
cursor: pointer; 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-table input[type="checkbox"]:disabled { cursor: not-allowed; }
.permission-cb-na { opacity: 0.25; accent-color: var(--text-faint) !important; } .permission-cb-na { opacity: 0.25; accent-color: var(--text-faint) !important; }
+8 -1
View File
@@ -160,9 +160,16 @@
form.addEventListener("submit", function (e) { form.addEventListener("submit", function (e) {
if (form.dataset.confirmed === "1") return; if (form.dataset.confirmed === "1") return;
e.preventDefault(); 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 <input type="hidden">), 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"), () => { window.confirmAction(form.getAttribute("data-confirm"), () => {
form.dataset.confirmed = "1"; form.dataset.confirmed = "1";
form.requestSubmit ? form.requestSubmit() : form.submit(); form.requestSubmit ? form.requestSubmit(submitter) : form.submit();
}, form.getAttribute("data-confirm-title")); }, form.getAttribute("data-confirm-title"));
}); });
}); });
+52 -43
View File
@@ -5,53 +5,50 @@
{% block content %} {% block content %}
{# Rendert einen Bereich (Geräte/Logs/Einstellungen) als kompakte Tabelle: {# Rendert einen Bereich (Geräte/Logs/Einstellungen) als kompakte, schmale
Zeilen = Bereich selbst (nur die R-Spalte aktiv, das ist der Kill-Switch) Spalte: eigene Kopfzeile mit Bereichsname + einem Kästchen (der
+ je eine Zeile pro Unterpunkt (Clients, Switche, ...). Spalten = die Kill-Switch, "Bereich sichtbar"), darunter eine kleine Tabelle mit den
kanonischen Rechte R/W/E/D nebeneinander (permission_row_types). 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 "Ä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-Recht. Zellen ohne passendes Recht für die jeweilige Zeile (z.B.
Löschen, sondern PoE-Neustart über das Dashboard-Popup. Zellen ohne "D" bei Switche/Zugangsdaten) werden als ausgegraute, nicht anklickbare
passendes Recht für die jeweilige Zeile (z.B. "W" bei "Geräte" selbst, Checkbox dargestellt statt zu fehlen — dadurch bleiben alle Spalten
oder "D" bei Switche/Zugangsdaten) werden als ausgegraute, nicht bündig untereinander. readonly=true zeigt nur den aktuellen Stand
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 (Admin/Systemgruppen bzw. fehlendes groups.edit). Die JS-Funktion
applyPermissionGating() sperrt Kind-Rechte, solange das Bereich-Lesen applyPermissionGating() sperrt Kind-Rechte, solange das Bereich-Lesen
(Kill-Switch) nicht gesetzt ist. #} (Kill-Switch) nicht gesetzt ist. Mehrere Bereiche stehen per Flexbox
{% macro permission_table(group, checked_keys, readonly) %} nebeneinander (siehe .permission-groups-row), damit die ganze
<div class="permission-area"> Rechteübersicht einer Gruppe in einer kompakten Zeile Platz hat. #}
{% macro permission_table(group, group_key, checked_keys, readonly) %}
{% set row_types = group_row_types[group_key] %}
<div class="permission-group-col">
<div style="overflow-x:auto;"> <div style="overflow-x:auto;">
<table class="permission-table"> <table class="permission-table">
<thead> <thead>
<tr> <tr>
<th>{{ group['label'] }}</th> <th class="permission-group-header-cell">
{% for row_key, row_letter, row_label in permission_row_types %} <label class="permission-group-toggle">
<input type="checkbox" name="permissions" value="{{ group['view_key'] }}"
title="{{ permission_labels.get(group['view_key'], group['label']) }}"
{% if group['view_key'] in checked_keys %}checked{% endif %}
{% if readonly %}disabled{% endif %}
class="permission-area-toggle-cb">
<span class="permission-group-name">{{ group['label'] }}</span>
</label>
</th>
{% for row_key, row_letter, row_label in row_types %}
<th title="{{ row_label }}">{{ row_letter }}</th> <th title="{{ row_label }}">{{ row_letter }}</th>
{% endfor %} {% endfor %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr>
<td class="permission-row-label">{{ group['label'] }} (gesamt)</td>
{% for row_key, row_letter, row_label in permission_row_types %}
<td>
{% if row_key == 'view' %}
<input type="checkbox" name="permissions" value="{{ group['view_key'] }}"
title="{{ permission_labels.get(group['view_key'], group['label']) }}"
{% if group['view_key'] in checked_keys %}checked{% endif %}
{% if readonly %}disabled{% endif %}
class="permission-area-toggle-cb">
{% else %}
<input type="checkbox" disabled class="permission-cb-na" tabindex="-1">
{% endif %}
</td>
{% endfor %}
</tr>
{% for child_key, child in group['children'].items() %} {% for child_key, child in group['children'].items() %}
<tr> <tr>
<td class="permission-row-label">{{ child['label'] }}</td> <td class="permission-row-label">{{ child['label'] }}</td>
{% for row_key, row_letter, row_label in permission_row_types %} {% for row_key, row_letter, row_label in row_types %}
{% set perm_key = child['rows'].get(row_key) %} {% set perm_key = child['rows'].get(row_key) %}
<td> <td>
{% if perm_key %} {% if perm_key %}
@@ -74,9 +71,16 @@
{% endmacro %} {% endmacro %}
{% macro permission_tree(checked_keys, readonly) %} {% macro permission_tree(checked_keys, readonly) %}
<div class="permission-groups-row">
{% for group_key, group in permission_catalog.items() %} {% for group_key, group in permission_catalog.items() %}
{{ permission_table(group, checked_keys, readonly) }} {{ permission_table(group, group_key, checked_keys, readonly) }}
{% endfor %} {% endfor %}
</div>
<div class="permission-legend">
<strong>R</strong> = Read (Lesen) · <strong>W</strong> = Write (Anlegen) ·
<strong>E</strong> = Edit (Ändern, inkl. Löschen) ·
<strong>D</strong> = Dashboard (PoE-Neustart über das Dashboard-Popup, nur bei Clients)
</div>
{% endmacro %} {% endmacro %}
<div class="section-head"> <div class="section-head">
@@ -86,12 +90,9 @@
Über Gruppen lassen sich Rechte für jeden Bereich der App granular vergeben. Über Gruppen lassen sich Rechte für jeden Bereich der App granular vergeben.
Ein Benutzer kann mehreren Gruppen angehören — die Rechte addieren sich. Auf „Rechte“ klicken, Ein Benutzer kann mehreren Gruppen angehören — die Rechte addieren sich. Auf „Rechte“ klicken,
um eine Gruppe aufzuklappen und die Berechtigungen im Detail zu sehen bzw. zu ändern. um eine Gruppe aufzuklappen und die Berechtigungen im Detail zu sehen bzw. zu ändern.
„Admin“ und „Benutzer“ sind feste Systemgruppen und nicht veränderbar. Die Zeile „... (gesamt)“ „Admin“ und „Benutzer“ sind feste Systemgruppen und nicht veränderbar. Das Kästchen neben dem
muss gelesen werden dürfen, bevor die Zeilen darunter greifen — ausgegraute Kästchen gelten für Bereichsnamen (Geräte/Logs/Einstellungen) muss gesetzt sein, bevor die Rechte darunter greifen —
die jeweilige Zeile nicht. automatische Neustarts bei Ausfall bleiben von „D“ unberührt. Legende direkt bei den Rechten.
<strong>R</strong> = Lesen (Read) · <strong>W</strong> = Schreiben/Anlegen (Write) ·
<strong>E</strong> = Ändern inkl. Löschen (Edit) ·
<strong>D</strong> = Dashboard-Neustart (nur bei Clients; automatische Neustarts bei Ausfall bleiben davon unberührt)
</div> </div>
</div> </div>
{% if current_user.has_permission('groups.create') %} {% if current_user.has_permission('groups.create') %}
@@ -263,9 +264,11 @@
</div> </div>
{% endfor %} {% endfor %}
<!-- Modal: Neue Gruppe --> <!-- Modal: Neue Gruppe — Rechte direkt beim Anlegen setzbar, Mitglieder
werden danach über die Gruppentabelle zugeordnet (die Gruppe muss
dafür erst existieren). -->
<div class="modal-overlay" id="addGroupModal"> <div class="modal-overlay" id="addGroupModal">
<div class="modal" style="max-width:380px;"> <div class="modal" style="max-width:900px;">
<form method="post"> <form method="post">
<div class="modal-header"> <div class="modal-header">
<h3>Neue Gruppe</h3> <h3>Neue Gruppe</h3>
@@ -276,8 +279,9 @@
<div class="field"> <div class="field">
<label>Name</label> <label>Name</label>
<input type="text" name="name" required placeholder="z.B. Facility-Team"> <input type="text" name="name" required placeholder="z.B. Facility-Team">
<div class="field-hint">Rechte und Mitglieder werden danach über die Gruppentabelle eingestellt.</div> <div class="field-hint">Mitglieder werden danach über die Gruppentabelle zugeordnet.</div>
</div> </div>
{{ permission_tree([], false) }}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button> <button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
@@ -306,15 +310,20 @@ function toggleDetail(id) {
// (Admin/Systemgruppen) bleiben unangetastet, deren Checkboxen sind ohnehin // (Admin/Systemgruppen) bleiben unangetastet, deren Checkboxen sind ohnehin
// alle disabled. // alle disabled.
function applyPermissionGating() { function applyPermissionGating() {
document.querySelectorAll(".permission-area").forEach(function (area) { document.querySelectorAll(".permission-group-col").forEach(function (area) {
const toggle = area.querySelector(".permission-area-toggle-cb"); const toggle = area.querySelector(".permission-area-toggle-cb");
if (!toggle || toggle.disabled) return; if (!toggle || toggle.disabled) return;
const tbody = area.querySelector("tbody");
const children = area.querySelectorAll(".permission-child-cb"); const children = area.querySelectorAll(".permission-child-cb");
const sync = function () { const sync = function () {
children.forEach(function (cb) { children.forEach(function (cb) {
cb.disabled = !toggle.checked; cb.disabled = !toggle.checked;
if (!toggle.checked) cb.checked = false; if (!toggle.checked) cb.checked = false;
}); });
// Zusätzlich zum disabled-Attribut (das der Browser nur dezent
// abblendet) sichtbar ausgrauen, wie gefordert — sonst fällt
// "Bereich gesperrt" auf den ersten Blick kaum auf.
if (tbody) tbody.classList.toggle("permission-locked", !toggle.checked);
}; };
toggle.addEventListener("change", sync); toggle.addEventListener("change", sync);
sync(); sync();
+2 -1
View File
@@ -56,7 +56,8 @@
{% endif %} {% endif %}
{% if current_user.has_permission('users.edit') and may_touch_target %} {% if current_user.has_permission('users.edit') and may_touch_target %}
<form method="post" data-confirm="Willst du „{{ u['username'] }}“ wirklich löschen?"> <form method="post" data-confirm="Willst du „{{ u['username'] }}“ wirklich löschen?">
<button type="submit" name="delete_user" value="{{ u['id'] }}" class="icon-btn" style="color:var(--danger);" title="Löschen"> <input type="hidden" name="delete_user" value="{{ u['id'] }}">
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
</button> </button>
</form> </form>