docs(16): UI design contract

This commit is contained in:
2026-08-06 10:34:48 +02:00
parent c5ce07afab
commit 092f4f6068
@@ -0,0 +1,220 @@
---
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: {groupsImported} importiert, {groupsRenamed} umbenannt, {groupsDeleted} gelöscht" (`admin.ldap.sync.resultGroups`) |
| 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) |
**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: 16 Zeilen — 11 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).
| 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 `<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) |
| 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 |
---
## 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: {groupsImported} importiert, {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`, `groupsImported`, `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