Files
tessera-ctl/.planning/phases/16-ad-gruppen-synchronisation/16-UI-SPEC.md
T
2026-08-06 14:42:44 +02:00

255 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 16
slug: ad-gruppen-synchronisation
status: draft
shadcn_initialized: false
preset: none
created: 2026-08-06
---
# Phase 16 — UI Design Contract
> Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none — kein `components.json`, keine shadcn-CLI im Repo (verifiziert: `find`/`ls` liefert keinen Treffer) |
| Preset | not applicable |
| Component library | none — alle betroffenen Admin-Seiten (`ldap`, `groups`, `modules/grants`, `users`) sind handgebaute Tailwind-Komponenten mit einem konsistenten, projekteigenen Vokabular (Tabellen, Modal-Dialoge, Checkbox-Listen, Badge-Chips) |
| Icon library | none — Icons sind handgeschriebene Inline-`<svg>` im Lucide-Stil (`viewBox="0 0 24 24"`, `stroke="currentColor"`, `strokeWidth="2"`, kein `fill`). Phase 16 führt **kein neues Icon** ein — die neue Import-Sektion nutzt reinen Text/Badge-Vokabular wie die bestehende Sektion 2.55 |
| Font | Inter via `--font-sans` (`apps/web/src/app/globals.css:33`), system-ui-Fallback |
**Shadcn-Gate-Entscheidung:** identisch zu Phase 15 — kein `components.json`, Stack ist Next.js. Phase 15 hat diese Frage bereits mit dem Projekt-Owner geklärt: 15 vorherige Phasen haben ein durchgängiges handgebautes Tailwind-System etabliert (5+ Admin-Seiten). Phase 16 ist eine reine Erweiterung von zwei bereits bestehenden Admin-Seiten (`admin/ldap`, `admin/groups`) — shadcn jetzt einzuführen würde einen zweiten, inkonsistenten Styling-Ansatz mitten in dieselben Seiten schaffen, die diese Phase erweitert. **Tool bleibt `none`, Registry-Safety-Gate ist damit nicht anwendbar.**
---
## Spacing Scale
Declared values (must be multiples of 4), identisch mit dem bereits im Projekt etablierten Raster — keine Abweichung in dieser Phase:
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Badge-Innenabstand-Feinjustierung (`gap-1`), Checkbox-zu-Label-Abstand |
| sm | 8px | Kompakte Element-Abstände (`gap-2`/`gap-3`), Badge-Polsterung (`px-1.5 py-0.5`/`px-2 py-0.5`) |
| md | 16px | Standard-Formularabstand (`space-y-4`), Tabellenzellen-/Listenzeilen-Polsterung (`px-4 py-2`/`px-4 py-3`) |
| lg | 24px | Card-/Section-Innenpolsterung (`p-6`), Abstand zwischen Seitenabschnitten (`space-y-6`/`space-y-8`) |
| xl | 32px | reserviert, in dieser Phase nicht benötigt |
| 2xl | 48px | reserviert, in dieser Phase nicht benötigt |
| 3xl | 64px | reserviert, in dieser Phase nicht benötigt |
**Exceptions:**
- Die neue Import-Sektion in `admin/ldap/page.tsx` übernimmt exakt das Zeilenlayout aus der bestehenden Sektion 2.55 (Einzelbenutzer suchen & importieren, Zeilen 808–910): `max-h-64 overflow-y-auto` Liste, `px-4 py-2` Zeilenpolsterung. Kein neues Maß.
- Icon-lose Textbuttons ("Bearbeiten", "Mitglieder", "Löschen" in der Gruppentabelle) bleiben beim bestehenden `px-2 py-1`-Maß (Desktop-Admin-Oberfläche, kein 44px-Touch-Target — identische Begründung wie in `15-UI-SPEC.md`).
---
## Typography
Übernommene, in Phase 15 bereits vom Projekt-Owner genehmigte Werte (siehe `15-UI-SPEC.md`, Abschnitt Typography, genehmigt 2026-08-04). Phase 16 führt **keine neue Größe und kein neues Gewicht** ein — jede neue Fläche dieser Phase (Import-Sektion, gesperrtes Namensfeld, interner-Name-Feld, Sync-Bericht-Zeilen) verwendet ausschließlich die vier bereits genehmigten Kombinationen:
| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Label / Badge | 12px (`text-xs`) | 500 medium | 1.4 |
| Body / Table / Form | 14px (`text-sm`) | 400 regular | 1.5 |
| Section Heading | 18px (`text-lg`) | 600 semibold | 1.3 |
| Page Heading | 24px (`text-2xl`) | 700 bold | 1.2 |
**Fortführung der Owner-genehmigten Ausnahme (kein neuer Genehmigungsschritt nötig):** Die vier Gewichte 400/500/600/700 sind seit Phase 2 projektweites Bestandsvokabular und wurden am 2026-08-04 explizit vom Projekt-Owner für die Überschreitung des UI-SPEC-Standardlimits (2 Gewichte) freigegeben — mit der ausdrücklichen Bindung, dass nachfolgende Phasen **kein fünftes Gewicht** einführen. Phase 16 hält sich daran: die Import-Sektion, das gesperrte Namensfeld und der erweiterte Sync-Bericht nutzen ausschließlich `text-sm`/`font-medium` für Labels und `text-sm` ohne Gewichtsklasse für Fließtext, identisch zu jeder bestehenden Sektion in `admin/ldap/page.tsx`.
---
## Color
| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | `--background` / `--card` | Seitenhintergrund, Card-/Section-Flächen (`rounded-lg border border-border p-6`) |
| Secondary (30%) | `--secondary` / `--muted` / `--sidebar` | Tabellenkopf (`bg-muted/50`), Sync-Bericht-Container (`bg-muted/30`), sekundäre Buttons (`border border-border`) |
| Accent (10%) | `--primary` (OKLCH `0.91 0.19 102`, gelb) | **ausschließlich**: Primär-Button "Ausgewählte importieren", Primär-Button "Speichern" im `GroupFormModal`, Fokus-Ring (`ring-ring`) |
| Destructive | `--destructive` (OKLCH `0.55 0.2 27`, rot) | **ausschließlich**: bestehender "Löschen"-Button in `DeleteGroupDialog` (unverändert), Fehlermeldungstext (Namenskollision, Import-Fehler) |
Accent reserved for: Button "Ausgewählte importieren (N)", Button "Speichern" im Gruppen-Formular, Fokus-Ring. **Niemals** für Badges oder informative Hinweise.
**Bestehende informelle Badge-Palette (kein Token, projektweit etabliert — jede Instanz trägt Light-/Dark-Variante, siehe `15-UI-SPEC.md`):**
| Badge-Zweck | Klassen | Verwendung in Phase 16 |
|---|---|---|
| Info/System (blau) | `bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400` | **Unverändert wiederverwendet** als das bestehende `boundBadge` ("AD-gebunden") in `admin/groups/page.tsx` — wird durch D-07 faktisch zum alleinigen visuellen Marker "importiert vs. lokal" (siehe Surface Contract 3). Kein neues Badge nötig. |
| Neutral/Inaktiv (grau) | `bg-muted text-muted-foreground` bzw. `bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-500` | "Bereits importiert"-Badge in der neuen Gruppen-Discovery-Liste — identische Klassen wie das bestehende `userSearch.alreadyImported`-Badge (`admin/ldap/page.tsx:870`), keine neue Farbe |
**Entscheidung zur Discretion-Frage "Unterscheidbarkeit lokal vs. importiert":** Die Trennung ist bereits heute sichtbar (`boundBadge`/`manualBadge`-Spalte in `admin/groups/page.tsx:180-189`). Nach D-07 (keine manuelle AD-Bindung mehr möglich) bedeutet dieses Badge ab dieser Phase eindeutig "vom Gruppen-Sync verwaltet" statt nur "irgendwie mit AD verknüpft" — die bestehende Spalte erfüllt den in `<specifics>` geforderten Anspruch ohne neue Komponente. Kein neues Badge, keine neue Farbe, keine neue Spalte für diesen Zweck.
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Primary CTA — Gruppen-Import | "Ausgewählte importieren ({N})" (`admin.ldap.groupImport.importSelected`) — identisches Muster zu `userSearch.importSelected`, disabled bei leerer Auswahl |
| Sekundär-Aktion — Gruppen-Discovery | "AD-Gruppen suchen" (`admin.ldap.groupImport.discover`) |
| Empty state — keine AD-Gruppen gefunden | "Keine AD-Gruppen gefunden." (`admin.ldap.groupImport.noneFound`) |
| Empty state — Suchfilter ohne Treffer | "Keine Treffer für diese Suche." (`admin.ldap.groupImport.noMatches`, reuse-Wortlaut von `groupFilter.noMatches`) |
| Badge — bereits importiert | "Bereits importiert" (`admin.ldap.groupImport.alreadyImported`, identischer Wortlaut wie `userSearch.alreadyImported`) |
| Erfolgsmeldung — Import abgeschlossen | "{imported} importiert, {skipped} übersprungen" + optional ", {errors} Fehler" (`admin.ldap.groupImport.resultSummary`, identisches Muster wie `userImportResult`) |
| Hinweis nach Import | "Mitgliedschaften werden beim nächsten Sync-Lauf automatisch befüllt (manuell oder nach Intervall)." (`admin.ldap.groupImport.membershipHint`) — löst die Discretion-Frage "sofortiger Sync vs. nächster Lauf" zugunsten "kein Sofort-Trigger" auf und sagt dem Admin explizit, was als Nächstes passiert |
| Fehler — Namenskollision beim Import (Discretion) | "Gruppe „{name}" konnte nicht importiert werden: Der Name ist bereits vergeben. Benenne die bestehende lokale Gruppe um oder vergib ihr einen internen Namen." (`admin.ldap.groupImport.nameCollisionError`) — erscheint als Fehlerzeile im Ergebnisblock, bricht den Import der übrigen ausgewählten Gruppen nicht ab (Pitfall 4) |
| Sync-Bericht — Benutzer (unverändert) | "Erstellt: {created}, aktualisiert: {updated}, deaktiviert: {deactivated}" (`admin.ldap.sync.result`, bestehend) |
| Sync-Bericht — Gruppenmitgliedschaften (bestehende Backend-Lücke wird geschlossen, Pitfall 3) | "Gruppenmitgliedschaften: {groupMembershipsAdded} hinzugefügt, {groupMembershipsRemoved} entfernt" (`admin.ldap.sync.resultGroupMemberships`, NEU — Felder existieren im Backend seit D-21, waren im Frontend nie verdrahtet) |
| Sync-Bericht — Gruppen (NEU, Phase 16) | "AD-Gruppen: {groupsAdopted} neu übernommen, {groupsRenamed} umbenannt, {groupsDeleted} gelöscht" (`admin.ldap.sync.resultGroups`) — *Feld am 2026-08-06 beim Planen von `groupsImported` zu `groupsAdopted` umbenannt: der Sync importiert per D-02 nie selbst, die Zahl wäre nach jedem Lauf zwingend 0. Sie zählt stattdessen Alt-Bindungen, die der Sync neu unter seine Verwaltung nimmt (GUID-Backfill).* |
| Sync-Bericht — Standardmarkierung verschoben (D-06, nur bei >0 sichtbar) | "Standardgruppen-Markierung musste neu vergeben werden ({defaultMarkerMoved}×)." (`admin.ldap.sync.defaultMarkerMoved`), Ton informativ-warnend (siehe Surface Contract 2 für Styling), nicht destruktiv |
| Gesperrtes Namensfeld (D-03) | Hinweistext unter dem deaktivierten Namensfeld: "Von der AD-Gruppe übernommen. Wird beim nächsten Sync automatisch aktualisiert." (`admin.groups.nameLockedHint`) |
| AD-Herkunft im Bearbeiten-Dialog (D-04, Debug-Zeile) | "AD-DN: {ldapDn}" (`admin.groups.adDnLabel`), `font-mono text-xs text-muted-foreground`, read-only |
| Label — interner Name (D-04) | "Interner Name" (`admin.groups.internalName`) |
| 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.
---
## UI Considerations
Applicable state considerations resolved: 44 — 39 covered, 5 backstop, 0 unresolved
> 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 | 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 `<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 |
---
## Surface Contracts
### 1. `/admin/ldap` — Neue Sektion "AD-Gruppen importieren" (D-01, D-02, PERM-02-Nachfolge)
Platzierung: neue Sektion zwischen der bestehenden Sektion 2.5 "Import-Filter (Gruppen/OUs)" (Zeilen 684–806) und Sektion 2.55 "Einzelbenutzer suchen & importieren" (Zeilen 808–910) — gruppiert die beiden gruppenbezogenen Bereiche nebeneinander, bevor die benutzerbezogenen Sektionen folgen. Gleiches `rounded-lg border border-border p-6`-Card-Muster wie jede andere Sektion auf dieser Seite.
- `h2` "AD-Gruppen importieren" (18px/600) + Beschreibungstext direkt darunter (14px, `text-muted-foreground`): "Ausgewählte AD-Gruppen werden als Tessera-Gruppen angelegt und danach bei jeder Synchronisation automatisch nachgeführt — Name, Mitgliedschaft und Löschung im AD ziehen nach." (`admin.ldap.groupImport.description`).
- Discover-Button "AD-Gruppen suchen", identisches Markup wie `groupFilter.discover` (Zeilen 694–703) — **eigener** Ladezustand/eigenes State-Objekt, **kein** geteilter State mit Sektion 2.5 in dieser Phase (löst RESEARCH Open Question 2 zugunsten der einfacheren, unabhängigen Variante; das gemeinsame `GET /ldap/groups`-Caching bleibt eine spätere Optimierung, kein Blocker).
- **Wichtiger Unterschied zu Sektion 2.5:** die Liste zeigt **ausschließlich Einträge vom Typ `group`** — OUs sind für den Gruppen-Import nicht wählbar und werden aus der Anzeige gefiltert (nur AD-Gruppen können zu Tessera-Gruppen werden, keine Organisationseinheiten). Kein Typ-Badge nötig, da die Liste homogen ist.
- Suchfeld (`discoverSearch`-Äquivalent) direkt über der Ergebnisliste, identisches Input-Markup wie Zeile 707–713.
- Ergebnisliste: `max-h-64 overflow-y-auto rounded-md border border-border divide-y divide-border`, jede Zeile identisch zum `userSearchResults`-Zeilen-Markup (Zeilen 841–876): Checkbox · Name (font-medium) · DN (font-mono text-xs muted, truncate) · rechtsbündiges "Bereits importiert"-Badge bei `alreadyImported === true`, Checkbox in diesem Fall `disabled` + Zeile `opacity-60`.
- Primär-Button "Ausgewählte importieren ({N})", identisches Markup/Verhalten wie `userSearch.importSelected` (Zeilen 885–896): `disabled` bei leerer Auswahl oder während des Imports.
- Nach erfolgreichem Import: Ergebniszeile "{imported} importiert, {skipped} übersprungen[, {errors} Fehler]" im identischen `<p className="mt-3 text-sm text-muted-foreground">`-Muster wie `userImportResult` (Zeilen 898–908), darunter bei Fehlern eine Liste von `text-xs text-destructive`-Zeilen (identisch zum bestehenden Fehler-Rendering), plus der statische Hinweis "Mitgliedschaften werden beim nächsten Sync-Lauf automatisch befüllt (manuell oder nach Intervall)." darunter.
- Nach Abschluss: Discovery-Liste wird neu geladen (identisches Refresh-Verhalten wie `handleImportUsers` → `handleSearchUsers()`), damit `alreadyImported`-Flags sofort nachziehen.
### 2. `/admin/ldap` — Sync-Bericht erweitert (Sektion 3, D-06, Pitfall 3 aus RESEARCH.md)
Der bestehende Ergebnis-Container (Zeilen 1069–1088, `rounded-md border border-border bg-muted/30 p-4`) bekommt zusätzliche Zeilen statt eines einzelnen Absatzes — Struktur bleibt derselbe Container, nur mit mehreren `<p>`-Zeilen statt einer:
1. Zeile 1 (bestehend, unverändert): "Erstellt: {created}, aktualisiert: {updated}, deaktiviert: {deactivated}" — 14px, `font-medium text-foreground`.
2. Zeile 2 (NEU — schließt die bereits vor Phase 16 bestehende Backend/Frontend-Lücke aus D-21, Pitfall 3): "Gruppenmitgliedschaften: {groupMembershipsAdded} hinzugefügt, {groupMembershipsRemoved} entfernt" — 14px, `text-muted-foreground` (nicht `font-medium` — sekundäre Information gegenüber Zeile 1, gleiche Gewichtsabstufung wie zwischen `h2` und Body üblich in dieser Datei).
3. Zeile 3 (NEU, Phase 16): "AD-Gruppen: {groupsAdopted} neu übernommen, {groupsRenamed} umbenannt, {groupsDeleted} gelöscht" — 14px, `text-muted-foreground`, immer sichtbar (auch bei allen Werten = 0, identische Transparenz-Haltung wie Zeile 1).
4. Zeile 4 (NEU, Phase 16, **bedingt** — nur wenn `defaultMarkerMoved > 0`): "Standardgruppen-Markierung musste neu vergeben werden ({defaultMarkerMoved}×)." — 14px, `font-medium`, Farbe `text-amber-700 dark:text-amber-400` (reuse der in `15-UI-SPEC.md` etablierten Amber-Familie für "Informationszustand, kein Fehler" — D-06 ist ein bewusst akzeptierter, aber bemerkenswerter Vorgang, kein Fehler und keine normale Routine-Zahl).
5. Fehlerliste (bestehend, unverändert): `syncResult.errors.map(...)` darunter, `text-xs text-destructive`. Gruppen-bezogene Fehler tragen laut RESEARCH-Muster das Präfix `Gruppe {name}: ...` — bereits durch das bestehende generische Fehlerzeilen-Rendering abgedeckt, keine visuelle Sonderbehandlung nötig.
Frontend-`SyncResult`-Interface (Zeile 54–59) wächst additiv um `groupMembershipsAdded`, `groupMembershipsRemoved`, `groupsAdopted`, `groupsRenamed`, `groupsDeleted`, `defaultMarkerMoved` — alle `number`, analog zum bestehenden Pattern.
### 3. `/admin/groups` — Namensanzeige, Create-Flow ohne AD-Bindung (D-04, D-07, D-13-Anschluss)
- **Namensspalte:** zeigt `group.internalName ?? group.name` in der bestehenden `font-medium text-foreground`-Zelle (Zeile 179). Zusätzlich `title={group.name}` auf dem umschließenden `<span>`, damit der AD-Name bei gesetztem internem Namen per Hover nachvollziehbar bleibt (kostenloser Zusatz zum literalen D-04-Anspruch "im Bearbeiten-Dialog sichtbar" — verstärkt Nachvollziehbarkeit, ersetzt sie nicht).
- **AD-Bindung-Spalte:** unverändert (`boundBadge`/`manualBadge`, Zeilen 180–189) — wird durch D-07 faktisch zum alleinigen "importiert"-Marker (siehe Color-Abschnitt).
- **"Gruppe erstellen"-Button und Modal:** öffnet ab dieser Phase ausschließlich den lokalen Erstellungs-Flow — kein zweistufiger Create-dann-PATCH-Bind-Ablauf mehr (D-07 entfernt diesen Pfad vollständig aus `GroupFormModal.tsx`). Siehe Surface Contract 4 für das Modal-Detail.
### 4. `GroupFormModal.tsx` — Entfernung der AD-Radio-Auswahl, gesperrtes Namensfeld, interner Name (D-03, D-04, D-07)
Der komplette AD-Bindungs-Block (Zeilen 147–227: Discovery-State, Radio-Liste, Bind/Unbind-Logik, `fetchLdapGroups`-Effekt, der zweistufige Create-dann-PATCH-Flow in `handleSubmit`) wird entfernt. Drei verbleibende, klar getrennte Modal-Zustände:
**a) Create (kein `group`-Prop):** Nur noch das Feld "Name" (Pflicht, editierbar) — identisch zum bisherigen Namensfeld-Markup (Zeilen 136–145). Darunter ein neuer Hinweis-Absatz (14px, `text-muted-foreground`): "AD-Gruppen werden im LDAP-Bereich importiert, nicht hier angelegt." mit Inline-Link "Zum LDAP-Bereich" → `/admin/ldap` (`text-primary hover:underline`, identischer Link-Stil wie `emptyModulesLink` in `modules/grants/page.tsx:184-189`). `handleSubmit` reduziert sich auf einen einzelnen `POST /groups`-Call ohne Folge-PATCH.
**b) Edit, lokale Gruppe (`group.ldapDn === null`):** unverändert gegenüber heute minus dem entfernten AD-Block — Name-Feld bleibt frei editierbar, kein interner-Name-Feld (für lokale Gruppen ist `name` bereits der volle Anzeigename, ein zweites Namensfeld wäre redundant und nicht durch eine Entscheidung gedeckt).
**c) Edit, importierte Gruppe (`group.ldapDn !== null`):**
- Namensfeld: `disabled`, zeigt weiterhin `group.name` (den aktuellen AD-Namen) — visuell durch die bestehende `disabled:opacity-50`/`bg-muted`-Konvention erkennbar deaktiviert (identisch zum bestehenden `disabled`-Zustand anderer Inputs in diesem Projekt, z. B. `bindPassword`-Feld-Verhalten). Darunter Hinweistext "Von der AD-Gruppe übernommen. Wird beim nächsten Sync automatisch aktualisiert." (`admin.groups.nameLockedHint`, 12px, `text-xs text-muted-foreground`).
- Debug-Zeile darunter: "AD-DN: {ldapDn}" (`admin.groups.adDnLabel`), `font-mono text-xs text-muted-foreground` — reine Anzeige, kein Steuerelement, keine Unbind-Aktion mehr (der bisherige "Bindung entfernen"-Link entfällt vollständig mit D-07; eine importierte Gruppe kann nur noch über das Verschwinden im AD selbst entbunden/gelöscht werden, D-05).
- Neues Feld "Interner Name" (optional, editierbar): Standard-Text-Input im bestehenden Feld-Markup (Zeile 136–145-Stil), Label "Interner Name" (`admin.groups.internalName`), Platzhaltertext leer, Hinweis darunter: "Wird von der Synchronisation nie verändert. Bleibt das Feld leer, zeigt die Oberfläche stattdessen den AD-Namen." (`admin.groups.internalNameHint`).
- `handleSubmit` sendet in diesem Zustand ausschließlich `{ internalName }` per `PATCH /groups/{id}` — nie `name`, da das Feld gesperrt ist und serverseitig laut RESEARCH Open Question 1 ohnehin abgelehnt werden sollte.
Modal-Container, Overlay, Buttons (Abbrechen/Speichern) bleiben exakt das bestehende Muster (`fixed inset-0 bg-black/50` → `max-w-md rounded-lg border border-border bg-card p-6 shadow-lg`) — keine Größenänderung, keine neue Dialog-Variante.
### 5. `/admin/modules/grants` — Spaltenkopf-Namensanzeige (D-04)
`filteredGroups.map(...)`-Block (Zeilen 206–213): `title={g.name}` und `{g.name}` werden zu `title={g.internalName ?? g.name}` und `{g.internalName ?? g.name}`. Kein Layout-Unterschied — dieselbe `sticky top-0 z-10 min-w-[120px] max-w-[160px] truncate`-Zelle, derselbe Truncate-mit-Tooltip-Mechanismus, nur der zugrundeliegende Textwert ändert sich. Die Matrix-Zellen-`aria-label` (`matrixCheckboxLabel`, interpoliert `group: g.name`) folgt automatisch demselben Wert, keine separate Anpassung nötig.
### 6. `UserAccessModal.tsx` — kein Frontend-Diff (D-04)
Diese Komponente ändert sich **nicht**. Sowohl die Gruppenmitgliedschafts-Chips (`groups`-Array, Zeilen 159–178) als auch die `viaGroups`-Chips je Modulzeile (Zeilen 209–224) rendern bereits nur den String, den das Backend liefert. Die Anforderung aus D-04 wird ausschließlich serverseitig erfüllt: `module-grants.service.ts` muss an den Stellen, wo heute `group.name` gelesen/selektiert wird (Zeile 249 `names.push(g.group.name)` und Zeile 264 `name: m.group.name`), stattdessen `g.group.internalName ?? g.group.name` bzw. `m.group.internalName ?? m.group.name` liefern. Kein neuer UI-Zustand, keine neue Komponente.
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|--------------|
| shadcn official | keine — Tool ist `none` | not required |
| Drittanbieter | keine | not required |
Kein Registry-Zugriff in dieser Phase — alle Komponenten sind handgebaut nach den in "Surface Contracts" referenzierten, bereits im Repo vorhandenen Mustern.
---
## Checker Sign-Off
- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS
**Approval:** pending