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

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