26 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: {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, F3GroupFormModalfü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 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: {groupsImported} importiert, {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, 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.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