255 lines
31 KiB
Markdown
255 lines
31 KiB
Markdown
---
|
||
phase: 16
|
||
slug: ad-gruppen-synchronisation
|
||
status: draft
|
||
shadcn_initialized: false
|
||
preset: none
|
||
created: 2026-08-06
|
||
---
|
||
|
||
# Phase 16 — UI Design Contract
|
||
|
||
> Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
|
||
|
||
---
|
||
|
||
## Design System
|
||
|
||
| Property | Value |
|
||
|----------|-------|
|
||
| Tool | none — kein `components.json`, keine shadcn-CLI im Repo (verifiziert: `find`/`ls` liefert keinen Treffer) |
|
||
| Preset | not applicable |
|
||
| Component library | none — alle betroffenen Admin-Seiten (`ldap`, `groups`, `modules/grants`, `users`) sind handgebaute Tailwind-Komponenten mit einem konsistenten, projekteigenen Vokabular (Tabellen, Modal-Dialoge, Checkbox-Listen, Badge-Chips) |
|
||
| Icon library | none — Icons sind handgeschriebene Inline-`<svg>` im Lucide-Stil (`viewBox="0 0 24 24"`, `stroke="currentColor"`, `strokeWidth="2"`, kein `fill`). Phase 16 führt **kein neues Icon** ein — die neue Import-Sektion nutzt reinen Text/Badge-Vokabular wie die bestehende Sektion 2.55 |
|
||
| Font | Inter via `--font-sans` (`apps/web/src/app/globals.css:33`), system-ui-Fallback |
|
||
|
||
**Shadcn-Gate-Entscheidung:** identisch zu Phase 15 — kein `components.json`, Stack ist Next.js. Phase 15 hat diese Frage bereits mit dem Projekt-Owner geklärt: 15 vorherige Phasen haben ein durchgängiges handgebautes Tailwind-System etabliert (5+ Admin-Seiten). Phase 16 ist eine reine Erweiterung von zwei bereits bestehenden Admin-Seiten (`admin/ldap`, `admin/groups`) — shadcn jetzt einzuführen würde einen zweiten, inkonsistenten Styling-Ansatz mitten in dieselben Seiten schaffen, die diese Phase erweitert. **Tool bleibt `none`, Registry-Safety-Gate ist damit nicht anwendbar.**
|
||
|
||
---
|
||
|
||
## Spacing Scale
|
||
|
||
Declared values (must be multiples of 4), identisch mit dem bereits im Projekt etablierten Raster — keine Abweichung in dieser Phase:
|
||
|
||
| Token | Value | Usage |
|
||
|-------|-------|-------|
|
||
| xs | 4px | Badge-Innenabstand-Feinjustierung (`gap-1`), Checkbox-zu-Label-Abstand |
|
||
| sm | 8px | Kompakte Element-Abstände (`gap-2`/`gap-3`), Badge-Polsterung (`px-1.5 py-0.5`/`px-2 py-0.5`) |
|
||
| md | 16px | Standard-Formularabstand (`space-y-4`), Tabellenzellen-/Listenzeilen-Polsterung (`px-4 py-2`/`px-4 py-3`) |
|
||
| lg | 24px | Card-/Section-Innenpolsterung (`p-6`), Abstand zwischen Seitenabschnitten (`space-y-6`/`space-y-8`) |
|
||
| xl | 32px | reserviert, in dieser Phase nicht benötigt |
|
||
| 2xl | 48px | reserviert, in dieser Phase nicht benötigt |
|
||
| 3xl | 64px | reserviert, in dieser Phase nicht benötigt |
|
||
|
||
**Exceptions:**
|
||
- Die neue Import-Sektion in `admin/ldap/page.tsx` übernimmt exakt das Zeilenlayout aus der bestehenden Sektion 2.55 (Einzelbenutzer suchen & importieren, Zeilen 808–910): `max-h-64 overflow-y-auto` Liste, `px-4 py-2` Zeilenpolsterung. Kein neues Maß.
|
||
- Icon-lose Textbuttons ("Bearbeiten", "Mitglieder", "Löschen" in der Gruppentabelle) bleiben beim bestehenden `px-2 py-1`-Maß (Desktop-Admin-Oberfläche, kein 44px-Touch-Target — identische Begründung wie in `15-UI-SPEC.md`).
|
||
|
||
---
|
||
|
||
## Typography
|
||
|
||
Übernommene, in Phase 15 bereits vom Projekt-Owner genehmigte Werte (siehe `15-UI-SPEC.md`, Abschnitt Typography, genehmigt 2026-08-04). Phase 16 führt **keine neue Größe und kein neues Gewicht** ein — jede neue Fläche dieser Phase (Import-Sektion, gesperrtes Namensfeld, interner-Name-Feld, Sync-Bericht-Zeilen) verwendet ausschließlich die vier bereits genehmigten Kombinationen:
|
||
|
||
| Role | Size | Weight | Line Height |
|
||
|------|------|--------|-------------|
|
||
| Label / Badge | 12px (`text-xs`) | 500 medium | 1.4 |
|
||
| Body / Table / Form | 14px (`text-sm`) | 400 regular | 1.5 |
|
||
| Section Heading | 18px (`text-lg`) | 600 semibold | 1.3 |
|
||
| Page Heading | 24px (`text-2xl`) | 700 bold | 1.2 |
|
||
|
||
**Fortführung der Owner-genehmigten Ausnahme (kein neuer Genehmigungsschritt nötig):** Die vier Gewichte 400/500/600/700 sind seit Phase 2 projektweites Bestandsvokabular und wurden am 2026-08-04 explizit vom Projekt-Owner für die Überschreitung des UI-SPEC-Standardlimits (2 Gewichte) freigegeben — mit der ausdrücklichen Bindung, dass nachfolgende Phasen **kein fünftes Gewicht** einführen. Phase 16 hält sich daran: die Import-Sektion, das gesperrte Namensfeld und der erweiterte Sync-Bericht nutzen ausschließlich `text-sm`/`font-medium` für Labels und `text-sm` ohne Gewichtsklasse für Fließtext, identisch zu jeder bestehenden Sektion in `admin/ldap/page.tsx`.
|
||
|
||
---
|
||
|
||
## Color
|
||
|
||
| Role | Value | Usage |
|
||
|------|-------|-------|
|
||
| Dominant (60%) | `--background` / `--card` | Seitenhintergrund, Card-/Section-Flächen (`rounded-lg border border-border p-6`) |
|
||
| Secondary (30%) | `--secondary` / `--muted` / `--sidebar` | Tabellenkopf (`bg-muted/50`), Sync-Bericht-Container (`bg-muted/30`), sekundäre Buttons (`border border-border`) |
|
||
| Accent (10%) | `--primary` (OKLCH `0.91 0.19 102`, gelb) | **ausschließlich**: Primär-Button "Ausgewählte importieren", Primär-Button "Speichern" im `GroupFormModal`, Fokus-Ring (`ring-ring`) |
|
||
| Destructive | `--destructive` (OKLCH `0.55 0.2 27`, rot) | **ausschließlich**: bestehender "Löschen"-Button in `DeleteGroupDialog` (unverändert), Fehlermeldungstext (Namenskollision, Import-Fehler) |
|
||
|
||
Accent reserved for: Button "Ausgewählte importieren (N)", Button "Speichern" im Gruppen-Formular, Fokus-Ring. **Niemals** für Badges oder informative Hinweise.
|
||
|
||
**Bestehende informelle Badge-Palette (kein Token, projektweit etabliert — jede Instanz trägt Light-/Dark-Variante, siehe `15-UI-SPEC.md`):**
|
||
|
||
| Badge-Zweck | Klassen | Verwendung in Phase 16 |
|
||
|---|---|---|
|
||
| Info/System (blau) | `bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400` | **Unverändert wiederverwendet** als das bestehende `boundBadge` ("AD-gebunden") in `admin/groups/page.tsx` — wird durch D-07 faktisch zum alleinigen visuellen Marker "importiert vs. lokal" (siehe Surface Contract 3). Kein neues Badge nötig. |
|
||
| Neutral/Inaktiv (grau) | `bg-muted text-muted-foreground` bzw. `bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-500` | "Bereits importiert"-Badge in der neuen Gruppen-Discovery-Liste — identische Klassen wie das bestehende `userSearch.alreadyImported`-Badge (`admin/ldap/page.tsx:870`), keine neue Farbe |
|
||
|
||
**Entscheidung zur Discretion-Frage "Unterscheidbarkeit lokal vs. importiert":** Die Trennung ist bereits heute sichtbar (`boundBadge`/`manualBadge`-Spalte in `admin/groups/page.tsx:180-189`). Nach D-07 (keine manuelle AD-Bindung mehr möglich) bedeutet dieses Badge ab dieser Phase eindeutig "vom Gruppen-Sync verwaltet" statt nur "irgendwie mit AD verknüpft" — die bestehende Spalte erfüllt den in `<specifics>` geforderten Anspruch ohne neue Komponente. Kein neues Badge, keine neue Farbe, keine neue Spalte für diesen Zweck.
|
||
|
||
---
|
||
|
||
## Copywriting Contract
|
||
|
||
| Element | Copy |
|
||
|---------|------|
|
||
| Primary CTA — Gruppen-Import | "Ausgewählte importieren ({N})" (`admin.ldap.groupImport.importSelected`) — identisches Muster zu `userSearch.importSelected`, disabled bei leerer Auswahl |
|
||
| Sekundär-Aktion — Gruppen-Discovery | "AD-Gruppen suchen" (`admin.ldap.groupImport.discover`) |
|
||
| Empty state — keine AD-Gruppen gefunden | "Keine AD-Gruppen gefunden." (`admin.ldap.groupImport.noneFound`) |
|
||
| Empty state — Suchfilter ohne Treffer | "Keine Treffer für diese Suche." (`admin.ldap.groupImport.noMatches`, reuse-Wortlaut von `groupFilter.noMatches`) |
|
||
| Badge — bereits importiert | "Bereits importiert" (`admin.ldap.groupImport.alreadyImported`, identischer Wortlaut wie `userSearch.alreadyImported`) |
|
||
| Erfolgsmeldung — Import abgeschlossen | "{imported} importiert, {skipped} übersprungen" + optional ", {errors} Fehler" (`admin.ldap.groupImport.resultSummary`, identisches Muster wie `userImportResult`) |
|
||
| Hinweis nach Import | "Mitgliedschaften werden beim nächsten Sync-Lauf automatisch befüllt (manuell oder nach Intervall)." (`admin.ldap.groupImport.membershipHint`) — löst die Discretion-Frage "sofortiger Sync vs. nächster Lauf" zugunsten "kein Sofort-Trigger" auf und sagt dem Admin explizit, was als Nächstes passiert |
|
||
| Fehler — Namenskollision beim Import (Discretion) | "Gruppe „{name}" konnte nicht importiert werden: Der Name ist bereits vergeben. Benenne die bestehende lokale Gruppe um oder vergib ihr einen internen Namen." (`admin.ldap.groupImport.nameCollisionError`) — erscheint als Fehlerzeile im Ergebnisblock, bricht den Import der übrigen ausgewählten Gruppen nicht ab (Pitfall 4) |
|
||
| Sync-Bericht — Benutzer (unverändert) | "Erstellt: {created}, aktualisiert: {updated}, deaktiviert: {deactivated}" (`admin.ldap.sync.result`, bestehend) |
|
||
| Sync-Bericht — Gruppenmitgliedschaften (bestehende Backend-Lücke wird geschlossen, Pitfall 3) | "Gruppenmitgliedschaften: {groupMembershipsAdded} hinzugefügt, {groupMembershipsRemoved} entfernt" (`admin.ldap.sync.resultGroupMemberships`, NEU — Felder existieren im Backend seit D-21, waren im Frontend nie verdrahtet) |
|
||
| Sync-Bericht — Gruppen (NEU, Phase 16) | "AD-Gruppen: {groupsImported} importiert, {groupsRenamed} umbenannt, {groupsDeleted} gelöscht" (`admin.ldap.sync.resultGroups`) |
|
||
| Sync-Bericht — Standardmarkierung verschoben (D-06, nur bei >0 sichtbar) | "Standardgruppen-Markierung musste neu vergeben werden ({defaultMarkerMoved}×)." (`admin.ldap.sync.defaultMarkerMoved`), Ton informativ-warnend (siehe Surface Contract 2 für Styling), nicht destruktiv |
|
||
| Gesperrtes Namensfeld (D-03) | Hinweistext unter dem deaktivierten Namensfeld: "Von der AD-Gruppe übernommen. Wird beim nächsten Sync automatisch aktualisiert." (`admin.groups.nameLockedHint`) |
|
||
| AD-Herkunft im Bearbeiten-Dialog (D-04, Debug-Zeile) | "AD-DN: {ldapDn}" (`admin.groups.adDnLabel`), `font-mono text-xs text-muted-foreground`, read-only |
|
||
| Label — interner Name (D-04) | "Interner Name" (`admin.groups.internalName`) |
|
||
| Hinweis — interner Name | "Wird von der Synchronisation nie verändert. Bleibt das Feld leer, zeigt die Oberfläche stattdessen den AD-Namen." (`admin.groups.internalNameHint`) |
|
||
| Hinweis — Create-Dialog, keine AD-Bindung mehr möglich (D-07) | "AD-Gruppen werden im LDAP-Bereich importiert, nicht hier angelegt." (`admin.groups.createLdapHint`) + Link "Zum LDAP-Bereich" (`admin.groups.goToLdap`) → `/admin/ldap` |
|
||
| Destructive confirmation — Gruppe manuell löschen (unverändert, gilt weiterhin auch für importierte Gruppen bei manuellem Löschen über die UI) | siehe `15-UI-SPEC.md`, `admin.groups.deleteConfirm.title`/`.body` — **keine neue Copy**, D-05 betrifft nur den automatischen Sync-Löschpfad (kein Dialog dort, bewusst laut Kontext) |
|
||
| 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: {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
|