docs(16): UI design contract

This commit is contained in:
2026-08-06 13:52:33 +02:00
parent 092f4f6068
commit 1c658a8ed7
@@ -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 — 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` | | 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) | | 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. **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 ## 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 | | Category | Element | Status | Resolution / Reason |
|----------|------------|--------|---------------------| |----------|---------|--------|---------------------|
| empty | F1 — keine AD-Gruppen im Verzeichnis gefunden | ✅ covered | Dokumentierte Copy "Keine AD-Gruppen gefunden." (Copywriting Contract) | | 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`) |
| 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 | 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`) |
| loading | F1 — Discovery-Anfrage läuft | ✅ covered | Bestehender `tCommon('loading')`-Text auf dem Discover-Button, identisches Muster zu Sektion 2.5/2.55 | | 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` |
| loading | F1 — Import-Anfrage läuft | ✅ covered | Bestehender `importingUsers`-Ladezustand-Analogon auf dem Import-Button, Button-Text wechselt zu `tCommon('loading')` | | 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) |
| 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) | | 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 |
| 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) | | 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` |
| 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 | | 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` |
| 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` | | 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 |
| 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 | | empty | E2 | covered | Vor dem ersten Sync-Lauf (`syncResult === null`) wird der Ergebnis-Container gar nicht gerendert — kein leerer Bericht mit Nullen |
| 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) | | 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 |
| 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 | | 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 |
| 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 | | populated | E2 | covered | Der Bericht rendert die drei festen Zahlenzeilen (Benutzer, Gruppenmitgliedschaften, AD-Gruppen) immer, auch bei Werten von 0 |
| long-text | F3 — sehr langer AD-Name im gesperrten, deaktivierten Namensfeld | 🧪 backstop | Kein Truncate für `<input disabled>`-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) | | partial | E2 | covered | Ein teilweise fehlgeschlagener Lauf zeigt Zahlenzeilen und Fehlerliste gleichzeitig — die Zahlen werden nicht unterdrückt, wenn `errors[]` gefüllt ist |
| 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 | | 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` |
| 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) | | zero-one-many | E2 | covered | Die Amber-Zeile zur verschobenen Standardmarkierung erscheint nur bei `defaultMarkerMoved > 0`; die Fehlerliste nur bei nicht-leerem `errors[]` |
| 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 | | 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 `<input disabled>` 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 |
--- ---