32 KiB
phase, slug, status, shadcn_initialized, preset, created
| phase | slug | status | shadcn_initialized | preset | created |
|---|---|---|---|---|---|
| 16 | ad-gruppen-synchronisation | draft | false | none | 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-autoListe,px-4 py-2Zeilenpolsterung. 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 in15-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 erkanntelist-collectionbei keinem der fünf Listen-Elemente und liefe E6 gar nicht klassifiziert). Fehlerzustände auf/admin/ldapund imGroupFormModalwurden am 2026-08-06 vom Owner entschieden: sichtbarer Fehler statt des dort bisher üblichen stummencatch {}.Elemente: E1 Import-Sektion
/admin/ldap· E2 erweiterter Sync-Bericht · E3 Gruppenliste/admin/groups· E4GroupFormModal· E5 Spaltenköpfe der Freigabe-Matrix · E6 Chips imUserAccessModal.
| 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 gemeinsameGET /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 zumuserSearchResults-Zeilen-Markup (Zeilen 841–876): Checkbox · Name (font-medium) · DN (font-mono text-xs muted, truncate) · rechtsbündiges "Bereits importiert"-Badge beialreadyImported === true, Checkbox in diesem Falldisabled+ Zeileopacity-60. - Primär-Button "Ausgewählte importieren ({N})", identisches Markup/Verhalten wie
userSearch.importSelected(Zeilen 885–896):disabledbei 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 wieuserImportResult(Zeilen 898–908), darunter bei Fehlern eine Liste vontext-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()), damitalreadyImported-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:
- Zeile 1 (bestehend, unverändert): "Erstellt: {created}, aktualisiert: {updated}, deaktiviert: {deactivated}" — 14px,
font-medium text-foreground. - 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(nichtfont-medium— sekundäre Information gegenüber Zeile 1, gleiche Gewichtsabstufung wie zwischenh2und Body üblich in dieser Datei). - 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). - Zeile 4 (NEU, Phase 16, bedingt — nur wenn
defaultMarkerMoved > 0): "Standardgruppen-Markierung musste neu vergeben werden ({defaultMarkerMoved}×)." — 14px,font-medium, Farbetext-amber-700 dark:text-amber-400(reuse der in15-UI-SPEC.mdetablierten Amber-Familie für "Informationszustand, kein Fehler" — D-06 ist ein bewusst akzeptierter, aber bemerkenswerter Vorgang, kein Fehler und keine normale Routine-Zahl). - Fehlerliste (bestehend, unverändert):
syncResult.errors.map(...)darunter,text-xs text-destructive. Gruppen-bezogene Fehler tragen laut RESEARCH-Muster das PräfixGruppe {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.namein der bestehendenfont-medium text-foreground-Zelle (Zeile 179). Zusätzlichtitle={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 weiterhingroup.name(den aktuellen AD-Namen) — visuell durch die bestehendedisabled:opacity-50/bg-muted-Konvention erkennbar deaktiviert (identisch zum bestehendendisabled-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). handleSubmitsendet in diesem Zustand ausschließlich{ internalName }perPATCH /groups/{id}— niename, 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