Files
tessera-ctl/.planning/phases/16-ad-gruppen-synchronisation/16-05-PLAN.md
T

218 lines
19 KiB
Markdown

---
phase: 16-ad-gruppen-synchronisation
plan: 05
type: execute
wave: 4
depends_on: [16-03, 16-04]
files_modified:
- apps/web/src/app/(portal)/admin/ldap/page.tsx
- apps/web/src/app/(portal)/admin/modules/grants/page.tsx
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
autonomous: true
requirements: [PERM-02]
estimate:
tokens: 44000
raw_tokens: 44000
tasks: 2
confidence: low
must_haves:
truths:
- "Der Sync-Bericht zeigt neben den Benutzerzahlen auch die Gruppenmitgliedschafts-Zahlen (Backend-Luecke seit D-21) und die Gruppen-Zahlen aus dieser Phase — kein vom Backend geliefertes Zaehlfeld bleibt unangezeigt."
- "Wurde die Standardgruppen-Markierung im Lauf neu vergeben, erscheint dazu eine eigene, informativ-warnende Zeile — nur dann, nicht als Dauerzeile mit 0 (D-06)."
- "Ein insgesamt fehlgeschlagener Sync-Request zeigt einen sichtbaren Fehlerzustand statt eines Berichts mit Nullen."
- "Die Spaltenkoepfe der Freigabe-Matrix zeigen internalName ?? name als Text und als title (D-04)."
- "Die Suche ueber den Gruppen-Spaltenfilter findet eine Gruppe sowohl ueber ihren internen als auch ueber ihren AD-Namen."
- "UI E2/empty: Vor dem ersten Sync-Lauf wird der Ergebnis-Container gar nicht gerendert — kein leerer Bericht mit Nullen."
- "UI E2/loading: Waehrend des Laufs traegt der Sync-Button seinen Ladetext und ist deaktiviert; der Bericht erscheint erst nach Abschluss, ohne Zwischenzustand mit Teilzahlen."
- "UI E2/error: Schlaegt der Sync-Request insgesamt fehl, erscheint ein sichtbarer Fehlerzustand statt eines stillen No-ops."
- "UI E2/populated: Der Bericht rendert die drei festen Zahlenzeilen (Benutzer, Gruppenmitgliedschaften, AD-Gruppen) immer, auch bei Werten von 0."
- "UI E2/partial: Ein teilweise fehlgeschlagener Lauf zeigt Zahlenzeilen und Fehlerliste gleichzeitig — die Zahlen werden nicht unterdrueckt, wenn Fehler vorliegen."
- "UI E2/zero-one-many: Die Zeile zur verschobenen Standardmarkierung erscheint nur bei einem Wert groesser 0; die Fehlerliste nur bei nicht-leerer Fehlerliste."
- "UI E5/empty: Bestehender Empty-State der Matrix unveraendert; dieser Plan aendert nur den Textwert der Gruppen-Spaltenkoepfe."
- "UI E5/loading: Bestehender Ladezustand der Matrix unveraendert."
- "UI E5/error: Bestehendes Fehlerverhalten der Matrix unveraendert."
- "UI E5/populated: Jeder Gruppen-Spaltenkopf traegt den Anzeigenamen als Text und als title; das aria-label der Matrix-Checkboxen folgt automatisch demselben Wert."
- "UI E5/partial: Fehlt der interne Name, greift der Fallback auf den gespeicherten Namen — kein leerer Spaltenkopf."
- "UI E5/overflow: Bestehendes sticky-Verhalten mit horizontalem Scroll unveraendert; keine zusaetzliche Spalte."
- "UI E5/zero-one-many: Die Spaltenanzahl folgt der gefilterten Gruppenliste wie bisher, unveraendert."
- "UI E5/long-text: Die bestehende Breitenbegrenzung mit Abschneiden plus title-Tooltip wirkt jetzt auf den berechneten Anzeigenamen."
- statement: "UI E2/overflow: Eine lange Fehlerliste aus einem Lauf ueber viele Gruppen sprengt den Berichts-Container nicht."
verification: backstop
- statement: "UI E2/long-text: Eine lange Fehlermeldung mit langem Gruppennamen bricht im Bericht um statt horizontal ueberzulaufen."
verification: backstop
artifacts:
- "apps/web/src/app/(portal)/admin/ldap/page.tsx — erweitertes SyncResult-Interface, drei bis vier Berichtszeilen, sichtbarer Sync-Request-Fehler"
- "apps/web/src/app/(portal)/admin/modules/grants/page.tsx — Group-Interface um internalName, Spaltenkopf und Suchfilter mit Fallback"
- "apps/web/src/messages/de.json + en.json — admin.ldap.sync.resultGroupMemberships, .resultGroups, .defaultMarkerMoved, .requestError"
key_links:
- "LdapSyncResult (Backend, Plan 16-03) ↔ SyncResult (Frontend): beide muessen im selben Schritt um dieselben Feldnamen wachsen, sonst wiederholt sich die stille Luecke aus D-21"
- "GET /module-grants/matrix liefert vollstaendige Group-Zeilen ohne select — internalName kommt seit der Migration aus Plan 16-01 automatisch mit; ein spaeter eingefuegtes select wuerde den Spaltenkopf still auf den AD-Namen zuruecksetzen"
prohibitions:
- "Der Sync-Bericht darf kein vom Backend geliefertes Zaehlfeld unterschlagen — ein Feld, das der Bericht nicht anzeigt, ist genau die stumme Luecke, die seit D-21 offen war."
- "Die Meldung ueber eine neu vergebene Standardgruppen-Markierung darf nicht wie ein Fehler aussehen — sie ist ein bemerkenswerter, aber bewusst akzeptierter Vorgang."
- "Ein fehlgeschlagener Sync-Request darf nicht als Bericht mit lauter Nullen erscheinen — das wuerde einen nie gelaufenen Sync als erfolgreichen No-Op ausgeben."
---
<objective>
Der Sync-Bericht sagt endlich, was der Lauf getan hat. Er schliesst dabei zwei Luecken auf einmal: die bereits seit D-21 bestehende (das Frontend kennt die Mitgliedschafts-Zahlen des Backends nicht) und die neue aus dieser Phase (uebernommene, umbenannte, geloeschte Gruppen sowie eine neu vergebene Standardmarkierung). Das ist die einzige vorgesehene Massnahme gegen den in D-05 bewusst akzeptierten Zugriffsverlust: eine sync-getriebene Loeschung ist nirgends sonst sichtbar.
Zusaetzlich zieht die dritte Anzeigestelle des internen Namens aus D-04 nach: die Spaltenkoepfe der Freigabe-Matrix.
Purpose: Ohne diesen Plan klickt der Admin auf "Jetzt synchronisieren", sieht drei Benutzerzahlen und hat keine Ahnung, ob Gruppen geloescht wurden.
Output: Erweiterter Bericht mit sichtbarem Fehlerzustand, Anzeigename mit Fallback in der Freigabe-Matrix, vier neue i18n-Schluessel in beiden Sprachen.
</objective>
<artifacts_this_phase_produces>
Neu entstehende Symbole in diesem Plan:
| Art | Symbol |
|-----|--------|
| Interface-Felder | `SyncResult.groupMembershipsAdded`, `.groupMembershipsRemoved`, `.groupsAdopted`, `.groupsRenamed`, `.groupsDeleted`, `.defaultMarkerMoved` (`admin/ldap/page.tsx`) |
| Frontend-State | `syncRequestError` (`admin/ldap/page.tsx`) |
| Typfeld | `Group.internalName?: string \| null` (`admin/modules/grants/page.tsx`) |
| i18n-Schluessel | `admin.ldap.sync.resultGroupMemberships`, `admin.ldap.sync.resultGroups`, `admin.ldap.sync.defaultMarkerMoved`, `admin.ldap.sync.requestError` |
Aus Plan 16-03 uebernommen und hier vorausgesetzt: die vier neuen Zaehlfelder auf `LdapSyncResult`.
</artifacts_this_phase_produces>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/16-ad-gruppen-synchronisation/16-CONTEXT.md
@.planning/phases/16-ad-gruppen-synchronisation/16-UI-SPEC.md
@.planning/phases/16-ad-gruppen-synchronisation/16-PATTERNS.md
@.planning/phases/16-ad-gruppen-synchronisation/16-03-SUMMARY.md
@.planning/phases/16-ad-gruppen-synchronisation/16-04-SUMMARY.md
</context>
<decisions_recorded>
- **Abweichung vom Copywriting Contract, ausdruecklich vermerkt:** Die UI-SPEC formuliert Berichtszeile 3 als "AD-Gruppen: {groupsImported} importiert, {groupsRenamed} umbenannt, {groupsDeleted} geloescht". Der Sync **importiert** aber per D-02 nie eine Gruppe — Import ist ausschliesslich die ausdrueckliche Auswahl im Import-Bereich aus Plan 16-01. Eine Zahl, die nach jedem Lauf zwingend 0 ist, waere dauerhaft bedeutungslos. Das Feld heisst `groupsAdopted` (Plan 16-03) und die Zeile lautet "AD-Gruppen: {groupsAdopted} neu uebernommen, {groupsRenamed} umbenannt, {groupsDeleted} geloescht". Die uebrigen drei Zeilen folgen dem Contract woertlich.
</decisions_recorded>
<tasks>
<task type="auto">
<name>Task 1: Sync-Bericht vollstaendig verdrahten und Fehlschlag sichtbar machen (D-06, RESEARCH.md Pitfall 3)</name>
<files>apps/web/src/app/(portal)/admin/ldap/page.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json</files>
<read_first>
- `apps/web/src/app/(portal)/admin/ldap/page.tsx` — das `SyncResult`-Interface (Zeilen 54-59), `handleSync` (231-249) inklusive des heutigen catch-Zweigs, der ein dreifeldriges Ergebnisobjekt konstruiert, der Sync-Button (1059-1066) und der Ergebnis-Container (1068-1088)
- `apps/api/src/ldap/ldap.service.ts` — `LdapSyncResult` in der nach Plan 16-03 gueltigen Fassung, als verbindliche Feldnamensquelle
- `apps/web/src/messages/de.json` und `en.json` — der `admin.ldap.sync`-Block mit dem bestehenden Ergebnis-Schluessel als Formatvorbild
- `.planning/phases/16-ad-gruppen-synchronisation/16-UI-SPEC.md` — Surface Contract 2 und die E2-Zeilen im Abschnitt UI Considerations
- `.planning/phases/16-ad-gruppen-synchronisation/16-RESEARCH.md` — Pitfall 3
</read_first>
<action>
Erweitere das `SyncResult`-Interface additiv um die sechs Zahlenfelder, die das Backend heute schon liefert bzw. seit Plan 16-03 liefert: die beiden Mitgliedschafts-Zaehler aus D-21 und die vier Gruppen-Zaehler dieser Phase. Die Feldnamen muessen exakt denen auf `LdapSyncResult` entsprechen — lies sie dort ab, statt sie zu raten.
Neuer State `syncRequestError: string | null`. `handleSync` setzt ihn zu Beginn jedes Laufs auf `null`. Der heutige catch-Zweig konstruiert ein Ergebnisobjekt mit drei Nullen und einer Netzwerkfehlermeldung — das ist mit dem erweiterten Interface nicht mehr typkorrekt und inhaltlich irrefuehrend: es gibt einen nie gelaufenen Sync als erfolgreichen Lauf ohne Aenderungen aus. Ersetze ihn: sowohl im catch-Zweig als auch bei `!res.ok` wird `syncResult` auf `null` gesetzt und `syncRequestError` auf `admin.ldap.sync.requestError`. Rendere diesen Zustand als `text-sm text-destructive`-Zeile unterhalb des Sync-Buttons, anstelle des Berichts-Containers.
Baue den bestehenden Ergebnis-Container von einem Absatz auf mehrere um — derselbe Container (`rounded-md border border-border bg-muted/30 p-4`), nur mit mehreren Zeilen:
1. Die bestehende Benutzerzeile, unveraendert, in `text-sm font-medium text-foreground`.
2. Neu: die Mitgliedschaftszeile mit `admin.ldap.sync.resultGroupMemberships`, in `text-sm text-muted-foreground` — sekundaere Information gegenueber Zeile 1.
3. Neu: die Gruppenzeile mit `admin.ldap.sync.resultGroups`, ebenfalls `text-sm text-muted-foreground`, **immer sichtbar**, auch wenn alle drei Werte 0 sind. Dieselbe Transparenzhaltung wie bei Zeile 1: eine 0 ist eine Aussage, kein Grund zum Verschweigen.
4. Neu und **bedingt**: nur bei einem Wert groesser 0 die Zeile `admin.ldap.sync.defaultMarkerMoved` in `text-sm font-medium` mit `text-amber-700 dark:text-amber-400`. Bewusst nicht in der Destruktiv-Farbe: die Verschiebung der Standardmarkierung ist ein bemerkenswerter, aber kein fehlerhafter Vorgang.
5. Die bestehende Fehlerliste darunter, unveraendert in `text-xs text-destructive`. Gruppenbezogene Fehler tragen laut Backend-Muster bereits ein Gruppenpraefix und brauchen keine visuelle Sonderbehandlung.
Die Zahlenzeilen werden nicht unterdrueckt, wenn die Fehlerliste gefuellt ist — ein teilweise fehlgeschlagener Lauf zeigt beides.
i18n: die vier neuen Schluessel unter `admin.ldap.sync` in beiden Sprachdateien anlegen. Die deutschen Texte fuer die Mitgliedschaftszeile, die Markierungszeile und den Fehlertext folgen dem Copywriting Contract woertlich; die Gruppenzeile folgt der im Abschnitt `<decisions_recorded>` festgehaltenen, ausdruecklich vermerkten Abweichung. Englische Texte sinngleich. Die Interpolationsnamen entsprechen exakt den Feldnamen des Interface.
</action>
<verify>
<automated>cd apps/web &amp;&amp; npx tsc --noEmit</automated>
<automated>cd apps/web &amp;&amp; npx vitest run</automated>
<automated>node -e "const de=require('./apps/web/src/messages/de.json'),en=require('./apps/web/src/messages/en.json');for(const k of ['resultGroupMemberships','resultGroups','defaultMarkerMoved','requestError']){if(!de.admin.ldap.sync[k])throw new Error('de fehlt '+k);if(!en.admin.ldap.sync[k])throw new Error('en fehlt '+k)}"</automated>
</verify>
<acceptance_criteria>
- Das Frontend-Interface kennt alle sechs neuen Zahlenfelder: `awk '/interface SyncResult/,/^}/' "apps/web/src/app/(portal)/admin/ldap/page.tsx" | grep -c 'groupMembershipsAdded\|groupMembershipsRemoved\|groupsAdopted\|groupsRenamed\|groupsDeleted\|defaultMarkerMoved'` ergibt 6
- Der catch-Zweig konstruiert kein Ergebnisobjekt mehr: `awk '/const handleSync/,/^ };/' "apps/web/src/app/(portal)/admin/ldap/page.tsx" | grep -c 'deactivated: 0'` ergibt 0
- Der Fehlerzustand ist verdrahtet: `awk '/const handleSync/,/^ };/' "apps/web/src/app/(portal)/admin/ldap/page.tsx" | grep -c 'setSyncRequestError'` ist >= 3
- Die bedingte Markierungszeile haengt an einem Groesser-0-Test: `grep -c 'defaultMarkerMoved > 0' "apps/web/src/app/(portal)/admin/ldap/page.tsx"` ist >= 1
- Die Markierungszeile nutzt die Amber-Familie, nicht die Destruktiv-Farbe: `grep -c 'text-amber-700' "apps/web/src/app/(portal)/admin/ldap/page.tsx"` ist >= 1
- `cd apps/web && npx tsc --noEmit` ist fehlerfrei
- Der Node-Schluesselcheck oben laeuft ohne Wurf durch
</acceptance_criteria>
<done>Der Sync-Bericht zeigt Benutzer-, Mitgliedschafts- und Gruppenzahlen, meldet eine neu vergebene Standardmarkierung eigens und informativ, und ein insgesamt fehlgeschlagener Lauf erscheint als sichtbarer Fehler statt als Bericht mit Nullen.</done>
</task>
<task type="auto">
<name>Task 2: Anzeigename mit Fallback in den Spaltenkoepfen der Freigabe-Matrix (D-04)</name>
<files>apps/web/src/app/(portal)/admin/modules/grants/page.tsx</files>
<read_first>
- `apps/web/src/app/(portal)/admin/modules/grants/page.tsx` — das lokale `Group`-Interface (Zeilen 16-19), `fetchMatrix` (55-70), das Suchfilter-Memo `filteredGroups` (140-143) und der Spaltenkopf-Block (206-213)
- `apps/api/src/groups/module-grants.service.ts` — `getMatrix()` (Zeilen 172-202); die Gruppen-Query nutzt kein `select`, das neue Feld kommt damit ohne Backend-Aenderung mit
- `.planning/phases/16-ad-gruppen-synchronisation/16-UI-SPEC.md` — Surface Contract 5 und die E5-Zeilen im Abschnitt UI Considerations
</read_first>
<action>
Erweitere das lokale `Group`-Interface in `apps/web/src/app/(portal)/admin/modules/grants/page.tsx` um `internalName?: string | null`. Der Matrix-Endpunkt liefert vollstaendige Gruppenzeilen ohne Feldauswahl, das Feld ist seit der Migration aus Plan 16-01 also bereits in der Antwort enthalten — es fehlt nur der Typ.
Im Spaltenkopf-Block sowohl den `title`-Wert als auch den gerenderten Text auf den internen Namen mit Rueckfall auf den gespeicherten Namen umstellen. Kein Layoutunterschied: dieselbe Zelle mit ihrer Breitenbegrenzung, ihrem Abschneiden und ihrem Tooltip-Mechanismus, nur der zugrundeliegende Textwert aendert sich. Das `aria-label` der Matrix-Checkboxen interpoliert bereits denselben Wert und folgt damit automatisch.
Passe zusaetzlich das Suchfilter-Memo an: es filtert heute ausschliesslich ueber den gespeicherten Namen. Nach dieser Aenderung sieht der Admin einen anderen Text, als er durchsucht. Lass den Filter auf **beide** Werte matchen — Anzeigename und gespeicherter Name — damit weder die Suche nach dem internen noch die nach dem AD-Namen ins Leere laeuft. Verwende an beiden Stellen die Nullish-Variante, nicht die Oder-Variante.
Diese Datei bekommt in diesem Plan keine neuen Uebersetzungsschluessel.
</action>
<verify>
<automated>cd apps/web &amp;&amp; npx tsc --noEmit</automated>
<automated>cd apps/web &amp;&amp; npx vitest run</automated>
</verify>
<acceptance_criteria>
- `grep -q "internalName" "apps/web/src/app/(portal)/admin/modules/grants/page.tsx"` trifft
- Spaltenkopf-Text und Tooltip nutzen beide den Fallback: `awk '/filteredGroups.map/,/<\/th>/' "apps/web/src/app/(portal)/admin/modules/grants/page.tsx" | grep -c 'internalName ?? g.name'` ist >= 2
- Keine Truthiness-Variante: `grep -c "internalName ||" "apps/web/src/app/(portal)/admin/modules/grants/page.tsx"` ergibt 0
- Der Suchfilter beruecksichtigt beide Namen: `awk '/const filteredGroups/,/\);/' "apps/web/src/app/(portal)/admin/modules/grants/page.tsx" | grep -c 'internalName'` ist >= 1
- Die Zellklassen sind unveraendert: `grep -c 'min-w-\[120px\] max-w-\[160px\] truncate' "apps/web/src/app/(portal)/admin/modules/grants/page.tsx"` ergibt weiterhin 1
- `cd apps/web && npx vitest run` ist vollstaendig gruen
</acceptance_criteria>
<done>Die Spaltenkoepfe der Freigabe-Matrix zeigen den internen Namen, sobald einer gesetzt ist, der AD-Name bleibt im Tooltip, und die Spaltensuche findet eine Gruppe unter beiden Namen.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| API → Browser (Sync-Bericht, Matrix) | Vom Backend gelieferte Zahlen, Fehlermeldungen und Gruppennamen werden angezeigt; Fehlermeldungen koennen AD-gelieferte Gruppennamen enthalten |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-16-12 | Cross-Site Scripting | Fehlerzeilen im Sync-Bericht, Spaltenkoepfe der Matrix | medium | mitigate | Alle Werte werden als React-Textknoten gerendert; in beiden Dateien wird keine Roh-HTML-Einbettung verwendet |
| T-16-14 | Information Disclosure | Fehlerzeilen enthalten Gruppennamen und ggf. DN-Fragmente aus dem Verzeichnis | low | accept | Der Bericht ist ausschliesslich fuer ADMIN und SUPER_ADMIN desselben Mandanten erreichbar; die enthaltenen Werte stammen aus dem Verzeichnis dieses Mandanten und sind fuer diese Rolle ohnehin sichtbar |
| T-16-05 | Repudiation | Sichtbarkeit sync-getriebener Loeschungen | high | mitigate | Genau dieser Plan ist die in D-05 vorgesehene Massnahme: die Zahlen fuer geloeschte Gruppen und verschobene Standardmarkierung erscheinen im Bericht. Ohne sie waere der akzeptierte Zugriffsverlust vollstaendig unsichtbar |
| T-16-SC | Tampering | Paketinstallation | low | accept | Keine neuen Pakete in diesem Plan |
</threat_model>
<verification>
1. `cd apps/web && npx vitest run` — vollstaendig gruen
2. `cd apps/web && npx tsc --noEmit` — fehlerfrei
3. Beide Sprachdateien tragen unter `admin.ldap.sync` denselben Schluesselsatz
4. Die Feldnamen im Frontend-Interface stimmen zeichengenau mit denen auf `LdapSyncResult` ueberein
**Manuell nachzuholen (Browser):** Sync ausloesen und pruefen, dass alle drei Zahlenzeilen erscheinen; einen Lauf mit verschobener Standardmarkierung provozieren und pruefen, dass die vierte Zeile in Amber erscheint; die Freigabe-Matrix mit einer Gruppe mit gesetztem internem Namen oeffnen und die Spaltensuche unter beiden Namen probieren.
</verification>
<success_criteria>
- Kein vom Backend geliefertes Zaehlfeld bleibt im Sync-Bericht unangezeigt
- Eine sync-getriebene Loeschung und eine verschobene Standardmarkierung sind im Bericht sichtbar (D-05-Massnahme, D-06)
- Ein fehlgeschlagener Sync-Request erscheint als Fehler, nicht als Bericht mit Nullen
- Die Spaltenkoepfe der Freigabe-Matrix zeigen den internen Namen (D-04, dritte Anzeigestelle)
</success_criteria>
<output>
Create `.planning/phases/16-ad-gruppen-synchronisation/16-05-SUMMARY.md` when done
</output>