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

32 KiB
Raw Blame History

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-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