Files
tessera-ctl/.planning/phases/16-ad-gruppen-synchronisation/16-UI-SPEC.md
T
2026-08-06 10:34:48 +02:00

221 lines
26 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: {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