From 1c658a8ed7b37dc9a63af0b15c7606c1a1df6c9a Mon Sep 17 00:00:00 2001 From: Schalli Date: Thu, 6 Aug 2026 13:52:33 +0200 Subject: [PATCH] docs(16): UI design contract --- .../16-UI-SPEC.md | 74 ++++++++++++++----- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/.planning/phases/16-ad-gruppen-synchronisation/16-UI-SPEC.md b/.planning/phases/16-ad-gruppen-synchronisation/16-UI-SPEC.md index ee312a2..cfd3582 100644 --- a/.planning/phases/16-ad-gruppen-synchronisation/16-UI-SPEC.md +++ b/.planning/phases/16-ad-gruppen-synchronisation/16-UI-SPEC.md @@ -106,6 +106,10 @@ Accent reserved for: Button "Ausgewählte importieren (N)", Button "Speichern" i | Hinweis — interner Name | "Wird von der Synchronisation nie verändert. Bleibt das Feld leer, zeigt die Oberfläche stattdessen den AD-Namen." (`admin.groups.internalNameHint`) | | Hinweis — Create-Dialog, keine AD-Bindung mehr möglich (D-07) | "AD-Gruppen werden im LDAP-Bereich importiert, nicht hier angelegt." (`admin.groups.createLdapHint`) + Link "Zum LDAP-Bereich" (`admin.groups.goToLdap`) → `/admin/ldap` | | Destructive confirmation — Gruppe manuell löschen (unverändert, gilt weiterhin auch für importierte Gruppen bei manuellem Löschen über die UI) | siehe `15-UI-SPEC.md`, `admin.groups.deleteConfirm.title`/`.body` — **keine neue Copy**, D-05 betrifft nur den automatischen Sync-Löschpfad (kein Dialog dort, bewusst laut Kontext) | +| Fehler — AD-Gruppensuche fehlgeschlagen (Owner-Entscheidung 2026-08-06) | "AD-Gruppen konnten nicht abgerufen werden. Prüfe die LDAP-Verbindung und versuche es erneut." (`admin.ldap.groupImport.discoverError`) — `text-sm text-destructive` unterhalb des Discover-Buttons; ersetzt das stumme `catch {}` für diesen neuen Aufruf | +| Fehler — Import-Request insgesamt fehlgeschlagen (Owner-Entscheidung 2026-08-06) | "Der Import konnte nicht ausgeführt werden. Es wurde keine Gruppe angelegt." (`admin.ldap.groupImport.importError`) — `text-sm text-destructive` anstelle des Ergebnisblocks; klar abgegrenzt vom Teilfehler-Fall, bei dem der Ergebnisblock mit Fehlerzeilen erscheint | +| Fehler — Sync-Lauf insgesamt fehlgeschlagen (Owner-Entscheidung 2026-08-06) | "Die Synchronisation konnte nicht ausgeführt werden." (`admin.ldap.sync.requestError`) — `text-sm text-destructive` anstelle des Berichts-Containers | +| Fehler — Speichern im Gruppen-Dialog fehlgeschlagen (Owner-Entscheidung 2026-08-06) | "Speichern fehlgeschlagen. Bitte erneut versuchen." (`admin.groups.saveError`) — `text-sm text-destructive` im Dialog über den Buttons, Dialog bleibt offen, Eingaben bleiben erhalten. Meldet der Server eine Namenskollision, tritt stattdessen "Der Name ist bereits vergeben." (`admin.groups.saveErrorNameTaken`) an dieselbe Stelle | **Zu entfernende Copy (D-07-Aufräumarbeit, Pitfall/Assumption A3 aus RESEARCH.md):** `admin.groups.ldapBind.*` (6 Keys: `hint`, `bound`, `unbind`, `searchPlaceholder`, `discoverError`, `noResults`) in **beiden** `de.json` (Zeilen 391–398) und `en.json` — verwaisen vollständig, sobald die Radio-Auswahl aus `GroupFormModal.tsx` entfernt ist. Der Grants-Matrix-Checkbox-Key `admin.groups.grants.matrixCheckboxLabel` und alle übrigen `admin.groups.*`-Keys bleiben unverändert bestehen. @@ -113,28 +117,58 @@ Accent reserved for: Button "Ausgewählte importieren (N)", Button "Speichern" i ## UI Considerations -Applicable state considerations resolved: 16 Zeilen — 11 covered, 5 backstop, 0 unresolved +Applicable state considerations resolved: 44 — 39 covered, 5 backstop, 0 unresolved -> Quelle: manuelle Anwendung des UI-Consideration-Probe-Rasters auf vier neue/geänderte Surfaces dieser Phase (F1 Gruppen-Import-Sektion in `admin/ldap`, F2 erweiterter Sync-Bericht, F3 `GroupFormModal` für importierte vs. lokale Gruppen, F4 Namensanzeige in Gruppenliste/Matrix/Chips). +> Quelle: `ui-consideration-probe.cjs` über sechs Elemente dieser Phase, mit vom Nutzer bestätigten Element-Art-Overrides (der Prosa-Klassifikator erkannte `list-collection` bei keinem der fünf Listen-Elemente und liefe E6 gar nicht klassifiziert). Fehlerzustände auf `/admin/ldap` und im `GroupFormModal` wurden am 2026-08-06 vom Owner entschieden: sichtbarer Fehler statt des dort bisher üblichen stummen `catch {}`. +> +> Elemente: **E1** Import-Sektion `/admin/ldap` · **E2** erweiterter Sync-Bericht · **E3** Gruppenliste `/admin/groups` · **E4** `GroupFormModal` · **E5** Spaltenköpfe der Freigabe-Matrix · **E6** Chips im `UserAccessModal`. -| Category | Element(s) | Status | Resolution / Reason | -|----------|------------|--------|---------------------| -| empty | F1 — keine AD-Gruppen im Verzeichnis gefunden | ✅ covered | Dokumentierte Copy "Keine AD-Gruppen gefunden." (Copywriting Contract) | -| empty | F1 — Suchfilter ohne Treffer innerhalb der geladenen Liste | ✅ covered | Dokumentierte Copy "Keine Treffer für diese Suche.", identisches Verhalten zur bestehenden `groupFilter`-Suche | -| loading | F1 — Discovery-Anfrage läuft | ✅ covered | Bestehender `tCommon('loading')`-Text auf dem Discover-Button, identisches Muster zu Sektion 2.5/2.55 | -| loading | F1 — Import-Anfrage läuft | ✅ covered | Bestehender `importingUsers`-Ladezustand-Analogon auf dem Import-Button, Button-Text wechselt zu `tCommon('loading')` | -| error | F1 — Discovery-Aufruf schlägt fehl (Netzwerk/Server) | 🧪 backstop | Kein dokumentiertes Fehler-UI für diesen konkreten Aufruf — Sektion 2.5 (`groupFilter.discover`) hat ebenfalls keins (`silently fail`-Präzedenzfall). Verifikation prüft, ob dieses Silent-Fail-Muster hier ebenfalls akzeptabel ist oder ob der Planer einen sichtbaren Fehler-Div ergänzt (Empfehlung: sichtbarer Fehler, da diese Aktion Gruppen mit realem Zugriffs-Impact erzeugt — abweichend vom reinen Lese-Discovery-Fall) | -| error | F1 — Namenskollision bei einzelnen ausgewählten Gruppen | ✅ covered | Dokumentierte Fehlerzeile pro betroffener Gruppe im Ergebnisblock (Copywriting Contract), Import der übrigen Auswahl läuft weiter (Pitfall 4) | -| error | F1 — Import-Request insgesamt schlägt fehl (5xx/Netzwerk) | 🧪 backstop | Kein dokumentiertes Verhalten für den Totalausfall-Fall (nur Teil-Fehler pro Gruppe ist spezifiziert). Verifikation prüft auf einen sichtbaren Fehlerzustand statt eines stillen No-ops | -| populated | F1 — mehrere AD-Gruppen gefunden, gemischt aus bereits-importiert und neu | ✅ covered | Deaktivierte Zeile mit "Bereits importiert"-Badge für bestehende, normale Checkbox-Zeile für neue — identisches Muster zu `userSearch` | -| zero-one-many | F2 — Sync-Bericht: Standardmarkierung wurde 0, 1 oder mehrfach verschoben | ✅ covered | Zeile erscheint nur bei `defaultMarkerMoved > 0` (Copywriting Contract), kein leerer/verwirrender "0×"-Text im Normalfall | -| zero-one-many | F3 — `GroupFormModal`: lokale Gruppe / importierte Gruppe (Create hat nur noch einen Zustand, Edit hat zwei) | ✅ covered | Drei explizite Modal-Zustände dokumentiert (Surface Contract 3): Create (nur lokal, mit LDAP-Hinweis), Edit-lokal (unverändert), Edit-importiert (gesperrtes Namensfeld + interner Name + AD-DN) | -| partial | F4 — Gruppenname in Liste/Matrix/Chips, während `internalName` noch nicht gesetzt ist | ✅ covered | Fallback `internalName ?? name` an allen drei Stellen — kein Zustand, in dem ein leerer Name gerendert wird | -| long-text | F4 — langer interner Name oder langer AD-Name in der Matrix-Spaltenkopf-Zelle | ✅ covered | Bestehendes `truncate`+`title`-Muster aus `modules/grants/page.tsx:206-213` bleibt unverändert, wirkt jetzt auf den berechneten `internalName ?? name`-Wert statt auf `name` allein | -| long-text | F3 — sehr langer AD-Name im gesperrten, deaktivierten Namensfeld | 🧪 backstop | Kein Truncate für ``-Felder im Projekt üblich (native Input-Feld-Overflow-Verhalten). Verifikation prüft, dass das deaktivierte Feld bei langen AD-Namen nicht das Modal-Layout sprengt (`max-w-md`-Container bleibt fix) | -| overflow | F1 — sehr lange DN-Werte in der Discovery-Liste | ✅ covered | Bestehendes `font-mono text-xs text-muted-foreground truncate`-Muster aus Sektion 2.5/2.55, unverändert übernommen | -| loading | F2 — Sync-Bericht während `syncing === true` | ✅ covered | Bestehender Button-Ladezustand (`t('sync.syncing')`), Bericht selbst erscheint erst nach Abschluss — kein Zwischenzustand mit Teil-Zahlen (Backend liefert eine vollständige Antwort, kein Streaming) | -| error | F2 — Sync-Bericht enthält sowohl Benutzer- als auch Gruppen-Fehler in derselben `errors[]`-Liste | 🧪 backstop | RESEARCH dokumentiert Pro-Gruppe-Fehler im selben `errors`-Array wie Benutzer-Fehler (`Gruppe {name}: {msg}`-Präfix), aber kein visuelles Unterscheidungsmerkmal zwischen Benutzer- und Gruppen-Fehlerzeilen ist spezifiziert. Verifikation prüft, ob die bestehende einheitliche Fehlerliste (`syncResult.errors.map(...)`) für einen Admin ausreichend nachvollziehbar bleibt, oder ob eine Kategorie-Kennzeichnung nötig wird | +| Category | Element | Status | Resolution / Reason | +|----------|---------|--------|---------------------| +| empty | E1 | covered | Findet die AD-Suche keine Gruppe, zeigt die Sektion "Keine AD-Gruppen gefunden." (`admin.ldap.groupImport.noneFound`); filtert das Suchfeld alle geladenen Treffer weg, erscheint "Keine Treffer für diese Suche." (`admin.ldap.groupImport.noMatches`) | +| loading | E1 | covered | Während Discovery bzw. Import läuft, trägt der jeweilige Button `tCommon('loading')` statt seines Labels und ist disabled — identisch zu `groupFilter.discover` (`ldap/page.tsx:701`) und `userSearch.importSelected` (`:889-893`) | +| error | E1 | covered | Schlägt der Discovery- oder der Import-Request fehl, zeigt die Sektion einen sichtbaren Fehlerzustand (`text-sm text-destructive`) statt eines stillen No-ops; das stumme `catch {}`-Muster der übrigen Abschnitte von `ldap/page.tsx` wird für die neuen Aktionen NICHT fortgeführt (Owner-Entscheidung). Vorbild: Fehlerbanner `groups/page.tsx:135-140` | +| populated | E1 | covered | Bereits importierte AD-Gruppen erscheinen in derselben Liste mit "Bereits importiert"-Badge, disabled Checkbox und `opacity-60` — sie werden nicht ausgeblendet (Discretion aufgelöst zugunsten Kennzeichnung) | +| partial | E1 | covered | Schlägt der Import einzelner ausgewählter Gruppen fehl (z. B. Namenskollision), läuft der Import der übrigen Auswahl weiter; der Ergebnisblock nennt `{imported} importiert, {skipped} übersprungen, {errors} Fehler` plus eine Fehlerzeile je betroffener Gruppe | +| overflow | E1 | covered | Die Ergebnisliste ist auf `max-h-64 overflow-y-auto` begrenzt; DN-Werte tragen `font-mono text-xs text-muted-foreground truncate` | +| zero-one-many | E1 | covered | Der Import-Button trägt die Auswahlzahl im Label ("Ausgewählte importieren ({N})") und ist bei leerer Auswahl disabled — identisch zu `userSearch.importSelected` | +| long-text | E1 | backstop | statement: Ein AD-Gruppenname, der breiter ist als die Listenzeile, sprengt das Zeilenlayout der Discovery-Liste nicht; verification: backstop — spezifiziert ist `truncate` nur auf dem DN, nicht auf dem Namen | +| empty | E2 | covered | Vor dem ersten Sync-Lauf (`syncResult === null`) wird der Ergebnis-Container gar nicht gerendert — kein leerer Bericht mit Nullen | +| loading | E2 | covered | Während `syncing === true` trägt der Sync-Button `t('sync.syncing')` und ist disabled (`ldap/page.tsx:1062-1065`); der Bericht erscheint erst nach Abschluss, kein Zwischenzustand mit Teilzahlen | +| error | E2 | covered | Schlägt der Sync-Request insgesamt fehl, erscheint ein sichtbarer Fehlerzustand statt eines stillen No-ops — dieselbe Owner-Entscheidung wie bei den Import-Aktionen | +| populated | E2 | covered | Der Bericht rendert die drei festen Zahlenzeilen (Benutzer, Gruppenmitgliedschaften, AD-Gruppen) immer, auch bei Werten von 0 | +| partial | E2 | covered | Ein teilweise fehlgeschlagener Lauf zeigt Zahlenzeilen und Fehlerliste gleichzeitig — die Zahlen werden nicht unterdrückt, wenn `errors[]` gefüllt ist | +| overflow | E2 | backstop | statement: Eine lange Fehlerliste aus einem Sync-Lauf über viele Gruppen sprengt den Berichts-Container nicht; verification: backstop — heute kein `max-h` auf `syncResult.errors` | +| zero-one-many | E2 | covered | Die Amber-Zeile zur verschobenen Standardmarkierung erscheint nur bei `defaultMarkerMoved > 0`; die Fehlerliste nur bei nicht-leerem `errors[]` | +| long-text | E2 | backstop | statement: Eine lange Fehlermeldung mit langem Gruppennamen bricht im Bericht um statt horizontal zu überlaufen; verification: backstop | +| empty | E3 | covered | Bestehender Empty-State (`groups.length === 0`, `groups/page.tsx:143`) unverändert — Phase 16 ändert nur den Textwert der Namensspalte | +| loading | E3 | covered | Bestehender `loading ? tCommon('loading')`-Zustand (`groups/page.tsx:141-142`) unverändert | +| error | E3 | covered | Bestehendes Fehlerbanner (`groups/page.tsx:135-140`, gespeist aus `setError`) unverändert | +| populated | E3 | covered | Die Namensspalte zeigt `group.internalName ?? group.name`; die AD-Bindungs-Spalte trägt weiterhin `boundBadge`/`manualBadge` und ist nach D-07 der alleinige "importiert vs. lokal"-Marker | +| partial | E3 | covered | Ist `internalName` einer importierten Gruppe nicht gesetzt, greift der Fallback auf `name` — es gibt keinen Zustand mit leerem Namen | +| overflow | E3 | covered | Bestehendes Tabellen-Scrollverhalten unverändert; Phase 16 fügt keine Spalte hinzu | +| zero-one-many | E3 | covered | Abgedeckt durch Empty-State und Listenrendering, beide unverändert | +| long-text | E3 | backstop | statement: Ein langer interner oder AD-Name in der Namensspalte sprengt die Tabellenzeile nicht; verification: backstop — spezifiziert ist nur `title={group.name}` als Tooltip, kein `truncate` auf dieser Zelle | +| empty | E4 | covered | Create-Zustand öffnet mit leerem Pflichtfeld "Name" und dem Hinweisabsatz "AD-Gruppen werden im LDAP-Bereich importiert, nicht hier angelegt." samt Link zu `/admin/ldap` | +| loading | E4 | covered | Bestehender `saving`-Zustand (`GroupFormModal.tsx:245-248`): Speichern-Button trägt `tCommon('loading')` und ist disabled | +| error | E4 | covered | Ein fehlgeschlagenes Speichern zeigt eine `text-sm text-destructive`-Zeile im Dialog; der Dialog bleibt offen, Eingaben bleiben erhalten. Bei Namenskollision greift die dafür vorgesehene Copy. Das stumme `catch {}` (`GroupFormModal.tsx:122`) wird ersetzt (Owner-Entscheidung) | +| partial | E4 | covered | Im Edit-Zustand einer importierten Gruppe ist `internalName` optional leer; das gesperrte Namensfeld zeigt weiterhin den AD-Namen, die AD-DN-Zeile bleibt sichtbar | +| long-text | E4 | backstop | statement: Ein sehr langer AD-Name im gesperrten `` sprengt den `max-w-md`-Modal-Container nicht; verification: backstop | +| empty | E5 | covered | Bestehender Empty-State der Matrix (`modules.length === 0`, `grants/page.tsx:181-189`) unverändert; Phase 16 ändert nur den Textwert der Gruppen-Spaltenköpfe | +| loading | E5 | covered | Bestehender Ladezustand der Matrix unverändert | +| error | E5 | covered | Bestehendes Fehlerverhalten der Matrix unverändert | +| populated | E5 | covered | Jeder Gruppen-Spaltenkopf trägt `g.internalName ?? g.name` als Text und als `title`; das `matrixCheckboxLabel`-`aria-label` folgt automatisch demselben Wert | +| partial | E5 | covered | Fehlt `internalName`, greift der Fallback auf `name` — kein leerer Spaltenkopf | +| overflow | E5 | covered | Bestehendes `sticky top-0 z-10`-Verhalten mit horizontalem Scroll unverändert; keine zusätzliche Spalte | +| zero-one-many | E5 | covered | Spaltenanzahl folgt `filteredGroups` wie bisher, unverändert | +| long-text | E5 | covered | Bestehendes `min-w-[120px] max-w-[160px] truncate` plus `title`-Tooltip (`grants/page.tsx:206-213`) wirkt jetzt auf den berechneten `internalName ?? name`-Wert | +| empty | E6 | covered | Bestehendes Verhalten bei leerem `groups`- bzw. `viaGroups`-Array unverändert — Phase 16 erzeugt keinen Frontend-Diff in dieser Komponente | +| loading | E6 | covered | Bestehender Ladezustand des Modals unverändert | +| error | E6 | covered | Bestehendes Fehlerverhalten des Modals unverändert | +| populated | E6 | covered | Die Chips rendern den vom Backend gelieferten String; `module-grants.service.ts` liefert an den Zeilen 249 und 264 künftig `internalName ?? name` | +| partial | E6 | covered | Der Fallback sitzt serverseitig (`g.group.internalName ?? g.group.name`) — ein nicht gesetzter interner Name erzeugt nie einen leeren Chip | +| overflow | E6 | covered | Bestehendes Chip-Umbruchverhalten unverändert | +| zero-one-many | E6 | covered | Unverändert; abgedeckt durch Empty-State und Chip-Rendering | ---