docs(quick-260910-krx): complete Bereich dashboard Etappe 2 plan

This commit is contained in:
2026-09-11 09:05:58 +02:00
parent 67b50240d6
commit b286bfb1a3
2 changed files with 224 additions and 5 deletions
@@ -0,0 +1,217 @@
---
phase: quick-260910-krx
plan: 01
subsystem: database
tags: [prisma, postgresql, rls, multi-tenancy, nestjs, vitest]
requires:
- phase: quick-260910-jab
provides: die drei geschlossenen Datenbankregeln (GroupMembership, ModuleGrant, TenderRssFeedSource) und den unveraendert strengen SearchProvider-Regelstand, gemessen in Migration 20260910120000
- phase: quick-260910-exd
provides: die bereits gebundene Modul-Zugriffsaufloesung (module-access.service.ts), die der Widget-Modulfilter dieses Bereichs ohne eigenes Zutun erbt
provides:
- Bereich dashboard vollstaendig umgestellt — zwoelf mandantengebundene Datenbankzugriffe ueber forTenant(), ein Katalogzugriff begruendet ungebunden
- Neunter Werkzeugabschnitt in rls-scratch-check.mjs (runDashboardAreaChecks), 13 neue Pruefungen, 87/87 gesamt
- Zwei-Klienten-TDD-Nachweis in dashboard.service.spec.ts, 8 -> 27 Testfaelle
- Vollstaendig nachgezogene Klassifikationsdatei (fuenf handgepflegte Stellen)
- Offener WINDOWS-Ledger-Eintrag #25 fuer die beweisvernichtende Fehlerrichtung dieses Bereichs
affects: [quick-260910-*, etappe-3-mandantentrennung, etappe-4-scharfschalten]
actuals:
tokens: 25254
tasks: 3
commits: 3
tech-stack:
added: []
patterns:
- "forTenant()-Bindung dienst-intern je Methode (kein req.tenantPrisma), wie alle sieben Bereiche vor diesem"
- "Zwei-Klienten-TDD-Nachweis via __makeBoundClient (Bindungsprotokoll: tenantId/Modell/Methode)"
key-files:
created: []
modified:
- apps/api/scripts/rls-scratch-check.mjs
- docs/mandantentrennung-etappe2-fehlerrichtung.md
- apps/api/src/dashboard/dashboard.service.ts
- apps/api/src/dashboard/dashboard.service.spec.ts
- apps/api/src/dashboard/dashboard.controller.ts
- docs/mandantentrennung-zugriffsklassifikation.md
- .planning/WINDOWS.md
key-decisions:
- "getLayout/saveLayout binden GEMEINSAM ueber denselben Klienten (Testfall festgenagelt) — nie getrennt auf gebunden/ungebunden, sonst geht die Konfliktpruefung gegen eine Zeile auf, die der Schreibzugriff nicht mehr sieht"
- "saveLayout uebersetzt eine PrismaClientUnknownRequestError (RLS-Konflikt) in eine deutsche ConflictException — NICHT das tenders-P2002-Muster, weil die gemessene Fehlerklasse eine andere ist"
- "Modulkatalog bleibt begruendet ungebunden, mit Messung/Bedingung getrennt, unter Berufung auf die bestehende module-registry-Pruefung statt einer neuen Behauptung"
- "Die drei Besitzpruefungen ueber die Benutzerkennung bleiben unveraendert bestehen — die Bindung ERGAENZT sie, ersetzt sie nicht (die Regeln dieses Bereichs kennen keine Benutzerdimension)"
patterns-established:
- "Wachhund-Testfall, der VOR der Protokollpruefung beweist, dass der zu schuetzende Codepfad ueberhaupt durchlaufen wurde"
requirements-completed: [WINDOWS-18, ETAPPE-2-DASHBOARD]
coverage:
- id: D1
description: "13 Datenbankzugriffe des Bereichs dashboard vollstaendig entschieden: 12 gebunden (forTenant), 1 begruendet ungebunden (Modulkatalog)"
requirement: "ETAPPE-2-DASHBOARD"
verification:
- kind: unit
ref: "apps/api/src/dashboard/dashboard.service.spec.ts (27 Faelle)"
status: pass
- kind: integration
ref: "apps/api/scripts/rls-scratch-check.mjs (87/87 Pruefungen, davon 13 neue im Abschnitt runDashboardAreaChecks)"
status: pass
human_judgment: false
- id: D2
description: "Kritikschrift fuer den Bereich dashboard inklusive der beweisvernichtenden Schleife und der eigenstaendig nachgepruften widerlegten SearchProvider-Praemisse"
requirement: "WINDOWS-18"
verification:
- kind: other
ref: "docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt '## Bereich dashboard' (w1)-(w5)"
status: pass
human_judgment: false
- id: D3
description: "Klassifikationsdatei an allen fuenf handgepflegten Stellen nachgezogen und maschinell gegen den Quelltext geprueft"
verification:
- kind: unit
ref: "apps/api/src/prisma/rls-access-inventory.spec.ts (10/10)"
status: pass
human_judgment: false
duration: 26min
completed: 2026-09-11
status: complete
---
# Quick 260910-krx: Mandantentrennung Etappe 2, Bereich dashboard — Summary
**Dreizehn Datenbankzugriffe des Bereichs `dashboard` (Widget-Anordnung, platzierte Widgets, eigene Suchmaschinen) vollständig entschieden: zwölf gebunden über `forTenant()`, ein Katalogzugriff begründet ungebunden — gemessen an den Regeln nach Migration 20260910120000, mit der beweisvernichtenden Fehlerrichtung dieses Bereichs vorab in der Kritikschrift festgehalten.**
## Performance
- **Duration:** 26 min
- **Started:** 2026-09-11T06:36:42Z
- **Completed:** 2026-09-11T09:02:xx (dritter Task-Commit)
- **Tasks:** 3
- **Files modified:** 7
## Accomplishments
- Neunter Abschnitt (`runDashboardAreaChecks`) in `apps/api/scripts/rls-scratch-check.mjs` mit 13 neuen, namentlich benannten Prüfungen gegen die aus der ausgelieferten Migration `20260909140000_rls_remaining_tenant_tables` geschnittenen Regeln für `DashboardLayout`, `WidgetInstance` und `SearchProvider`. Alle 87 Prüfungen bestehen (74 bisherige + 13 neue).
- Die Konfliktmessung (Befund K) ist gemessen, nicht angenommen: ein gebundenes `INSERT ... ON CONFLICT` auf eine unter dem Mandanten unsichtbare Zeile scheitert laut mit SQLSTATE 42501 — und am echten generierten Prisma Client (nicht nur an rohem SQL) gemessen: `prisma.dashboardLayout.upsert()` wirft `PrismaClientUnknownRequestError`, NICHT die bekannte `P2002`-Form, die der Bereich `tenders` abfängt.
- `dashboard.service.ts` vollständig umgestellt: `getLayout`/`saveLayout` gemeinsam gebunden; `getWidgets`/`addWidget`/`updateWidgetConfig`/`removeWidget` gebunden, die drei Besitzprüfungen über die Benutzerkennung bleiben unverändert bestehen; `getSearchProviders`/`addSearchProvider`/`removeSearchProvider` gebunden, die drei Vorgabe-Suchmaschinen aus der Konstante bleiben unverändert; der eine Katalogzugriff (`module`) bleibt begründet ungebunden.
- `dashboard.controller.ts` reicht den bereits aufgelösten Mandanten bei den fünf Handlern durch, die ihn zuvor verworfen hatten (`getLayout`, `updateWidgetConfig`, `removeWidget`, `getSearchProviders`, `removeSearchProvider`) — keine neue Vertrauensquelle, weiterhin ausschließlich aus `extractContext`/dem Sitzungsnachweis.
- `dashboard.service.spec.ts`: Zwei-Klienten-Nachweis über `__makeBoundClient` nach dem Muster von `module-access.service.spec.ts`, 8 → 27 Testfälle (19 neue, abgezählt), alle acht bestehenden Fälle unverändert grün.
- `docs/mandantentrennung-etappe2-fehlerrichtung.md`: neuer Abschnitt `## Bereich dashboard` mit (w1)-(w5), inklusive der beweisvernichtenden Schleife (leeres Dashboard → Neuaufbau → automatisches Zurückschreiben → überschriebene Anordnung, Widget-Dubletten) und einem Nachtrag im Abschnitt `## Bereich module-registry` zur geerbten Bindungsentlastung.
- `docs/mandantentrennung-zugriffsklassifikation.md`: alle fünf handgepflegten Stellen nachgezogen und maschinell gegen den Quelltext geprüft (`rls-access-inventory.spec.ts` grün).
- `.planning/WINDOWS.md`: neuer offener Eintrag #25 (Tabelle + JSON) für die beweisvernichtende Fehlerrichtung, mit der konkreten Vorabprüfung für Etappe 4 und dem Verweis auf #22 für die verwandte Eindeutigkeitsfrage.
- Drei Falsifizierungsnachweise durchgeführt, zurückgenommen und mit Testname/Fehlermeldung dokumentiert (siehe unten).
## Task Commits
Each task was committed atomically:
1. **Aufgabe 1: Die Fehlerrichtung messen und aufschreiben** - `6744918` (feat)
2. **Aufgabe 2: Anordnung und Widgets binden** - `e0ce594` (feat, TDD)
3. **Aufgabe 3: Suchmaschinen binden, Katalog begründet offen, Dokumente nachziehen** - `67b5024` (feat, TDD)
_Alle drei Tasks waren TDD-Tasks (Aufgabe 2/3) bzw. eine Messaufgabe (Aufgabe 1); jeder Task ist als ein Commit gelandet, weil RED/GREEN innerhalb desselben Arbeitsschritts vor dem Commit durchlaufen und verifiziert wurde._
## Files Created/Modified
- `apps/api/scripts/rls-scratch-check.mjs` - neunter Abschnitt `runDashboardAreaChecks`, 13 neue Prüfungen (74 → 87 gesamt)
- `docs/mandantentrennung-etappe2-fehlerrichtung.md` - neuer Abschnitt `## Bereich dashboard` (w1)-(w5) + Nachtrag in `## Bereich module-registry`
- `apps/api/src/dashboard/dashboard.service.ts` - alle 12 mandantengebundenen Zugriffe auf `forTenant()` umgestellt, Modulkatalog begründet ungebunden, Konfliktübersetzung in `saveLayout`
- `apps/api/src/dashboard/dashboard.service.spec.ts` - Zwei-Klienten-TDD-Nachweis, 8 → 27 Fälle
- `apps/api/src/dashboard/dashboard.controller.ts` - fünf Handler reichen den Mandanten durch
- `docs/mandantentrennung-zugriffsklassifikation.md` - alle fünf handgepflegten Stellen nachgezogen
- `.planning/WINDOWS.md` - neuer offener Eintrag #25
## Decisions Made
- **getLayout/saveLayout bewusst gemeinsam gebunden, nie getrennt** — ein eigener Testfall nagelt das fest, weil ein Auseinanderfallen von Lese- und Schreibhälfte die Konfliktprüfung auf eine Zeile aufgehen ließe, die der jeweils andere Halbschritt nicht mehr sieht (dieselbe Familie wie WINDOWS #22 im Bereich `user`).
- **Konfliktübersetzung über `Prisma.PrismaClientUnknownRequestError`, nicht `.code === 'P2002'`.** Gemessen in Aufgabe 1 am echten generierten Prisma Client gegen eine Wegwerf-Datenbank: `dashboardLayout.upsert()` wirft bei einem RLS-Konflikt auf eine unsichtbare, aber physisch vorhandene Zeile eine andere Prisma-Fehlerklasse als der Eindeutigkeitsfehler, den der Bereich `tenders` abfängt. Das `tenders`-Muster ließ sich deshalb nicht wörtlich übernehmen — dokumentiert in (w1)/(w4) der Kritikschrift, damit die Abweichung nicht stillschweigend untergeht.
- **Modulkatalog begründet ungebunden, mit Messung und Bedingung getrennt**, unter Berufung auf die bestehende Werkzeugprüfung `module-tabelle-traegt-keinen-zeilenschutz` aus dem Bereich `module-registry` statt einer neu behaupteten Messung.
- **Die drei Besitzprüfungen über die Benutzerkennung bleiben unverändert bestehen** — die Bindung ergänzt sie, ersetzt sie nicht. Zwei Testfälle je Prüfung nageln das fest (Besitzprüfung greift weiterhin bei fremdem Widget/fremder Suchmaschine).
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Verify-Schwellwert von Aufgabe 2 korrigiert (B ≥ 9 → B ≥ 8)**
- **Found during:** Aufgabe 1 (Zaehlkontrolle) und bestätigt bei der Verify-Ausführung von Aufgabe 2
- **Issue:** Befund A des Plans sagte für `widgetInstance` sieben Rohtreffer über sechs Zeilen voraus ("eine Zeile trägt zwei Vorkommen"). Tatsächlich gemessen (`grep -o "this\.prisma\.widgetInstance" apps/api/src/dashboard/dashboard.service.ts | wc -l`): genau sechs, jede Zeile genau ein Vorkommen. Zusammen mit `dashboardLayout` (2) ergibt das für Aufgabe 2 acht zu bindende Rohtreffer, nicht neun — der Gesamtwert für den Bereich (dreizehn) hält trotzdem exakt, siehe Task-1-Messung.
- **Fix:** Der in Aufgabe 2 manuell ausgeführte Verify-Befehl wurde mit der korrigierten Schwelle (`B -ge 8`) statt der im Plantext genannten (`B -ge 9`) interpretiert — dieselbe Vollständigkeit (alle 8 real vorhandenen Zugriffe gebunden), nur der falsche Zahlenwert korrigiert. Die Plandatei selbst wurde nicht verändert.
- **Files modified:** keine zusätzlichen — betrifft nur die Interpretation des Verify-Befehls
- **Verification:** `B=8` gemessen und akzeptiert; `C=6` (forTenant-Aufrufstellen je Methode) unverändert exakt getroffen
- **Committed in:** `e0ce594` (Aufgabe-2-Commit, Deviation im Commit-Text dokumentiert)
**2. [Rule 3 - Blocking] Klassifikationsdatei bereits in Aufgabe 2 minimal nachgezogen**
- **Found during:** Aufgabe 2, beim ersten vollständigen `npm run test`
- **Issue:** `rls-access-inventory.spec.ts` vergleicht die `Stand`-Spalte der Klassifikationsdatei live gegen den Quelltext. Nach der Bindung von `dashboardLayout`/`widgetInstance` in Aufgabe 2 stand die Datei noch auf `ungebunden` (die vollständige Nachziehung war für Aufgabe 3 vorgesehen) — die Prüfung wäre am Ende von Aufgabe 2 rot gewesen, "Baseline gehalten" wäre verletzt. Exakter Präzedenzfall: 260910-exd, Aufgabe 2, dieselbe Deviation, dort ebenfalls dokumentiert.
- **Fix:** Nur die `Stand`-Spalte der zwei betroffenen Zeilen (`dashboardLayout`, `widgetInstance`) auf `gebunden` gesetzt, mit einem kurzen Verweis auf die vollständige Nachziehung in Aufgabe 3 — keine Zahlen, keine Übersichtszeile, keine Summenzeile in Aufgabe 2 angefasst.
- **Files modified:** `docs/mandantentrennung-zugriffsklassifikation.md`
- **Verification:** `npm --prefix apps/api run test` grün (851/851) nach dem minimalen Nachzug
- **Committed in:** `e0ce594` (Aufgabe-2-Commit)
**3. [Rule 3 - Blocking] Scope-Allowlist von Aufgabe 2 um die Klassifikationsdatei erweitert**
- **Found during:** Aufgabe 2, beim Ausführen der Erlaubnislisten-Prüfung des Plans
- **Issue:** Die im Plantext für Aufgabe 2 genannte Erlaubnisliste (`UNEXPECTED=...`) nennt `docs/mandantentrennung-zugriffsklassifikation.md` nicht — eine direkte Folge derselben Abweichung wie oben (Deviation 2). Ohne die Erweiterung hätte die Erlaubnislisten-Prüfung eine notwendige, dokumentierte Änderung als "unerwartet" gemeldet.
- **Fix:** Die Datei bei der manuellen Ausführung der Erlaubnislisten-Prüfung als erlaubt behandelt (sie steht ohnehin in der Gesamt-`files_modified`-Liste des Plans und im Umfang von Aufgabe 3) — dieselbe Begründung wie Deviation 2.
- **Files modified:** keine zusätzlichen
- **Verification:** Erlaubnislisten-Prüfung grün mit der Erweiterung; die plan-weite Erlaubnisliste (gegen alle sieben `files_modified`) stimmt am Ende von Aufgabe 3 exakt überein (verifiziert)
- **Committed in:** `e0ce594` (Aufgabe-2-Commit)
**4. [Rule 2 - Missing critical] Wachhund-Testfall für den Modulkatalog verschärft, bevor er real geprüft wurde**
- **Found during:** Aufgabe 3, bei der Vorbereitung des ersten Falsifizierungsnachweises
- **Issue:** Der ursprünglich geschriebene Wachhund-Test (`expectNeverBound(prisma, 'module')`) rief `getWidgets` mit leerer `mockMap` auf — der Katalogzugriff wurde dadurch nie erreicht (früher Rückgabepunkt bei `boundSlugs.length === 0`). Der Test hätte auch dann grün gemeldet, wenn der Katalogzugriff versehentlich gebunden worden wäre, solange er nie aufgerufen wird — eine wirkungslose Prüfung.
- **Fix:** Test um ein Widget mit zugeordnetem Modul-Slug und eine zugehörige Katalogzeile erweitert, plus eine explizite Prüfung `expect(prisma.module.findMany).toHaveBeenCalled()` VOR der Wachhund-Prüfung, die beweist, dass der Pfad tatsächlich durchlaufen wurde.
- **Files modified:** `apps/api/src/dashboard/dashboard.service.spec.ts`
- **Verification:** Der verschärfte Test besteht mit dem korrekten Setup und schlägt beim ersten Falsifizierungsnachweis (Katalog probeweise gebunden) tatsächlich fehl — siehe Falsifizierungsnachweise unten
- **Committed in:** `67b5024` (Aufgabe-3-Commit)
---
**Total deviations:** 4 auto-fixed (3× Rule 3 — blockierende Verify-/Scope-Korrekturen aufgrund eines Planungsfehlers in Befund A, 1× Rule 2 — verschärfter Wachhund-Test)
**Impact on plan:** Keine Funktionsänderung, keine Verwässerung der Prüftiefe — im Gegenteil, Deviation 4 hat eine bestehende Prüflücke geschlossen, bevor sie unbemerkt geblieben wäre. Deviations 1-3 korrigieren einen bereits im Plantext selbst als möglich benannten Zählfehler (Zaehlkontrolle, Befund A) auf den tatsächlich gemessenen, korrekten Wert.
## Falsifizierungsnachweise
Alle drei durchgeführt, zurückgenommen und mit Testname/Meldung dokumentiert (Rule 5, "Falsification proofs get forgotten"):
1. **Aufgabe 2 — Bindung probeweise zurückgebaut.** `tenantPrisma.widgetInstance.delete` in `removeWidget` auf `this.prisma.widgetInstance.delete` zurückgebaut. Test **"Widget entfernen: ebenso, beide Abfragen über denselben Klienten"** wurde rot mit: `AssertionError: erwarteter gebundener Aufruf widgetInstance.delete(tenant=tenant-1) fehlt im Protokoll: [{"tenantId":"tenant-1","model":"widgetInstance","method":"findUnique"}]: expected false to be true`. Rückbau zurückgenommen, Testlauf wieder grün (20/20).
2. **Aufgabe 3 — Katalogzugriff probeweise gebunden.** `this.prisma.module.findMany` in `getWidgets` auf `tenantPrisma.module.findMany` geändert (`module` ist bewusst nicht Teil der Testdouble-Bindungsliste `BOUND_MODEL_NAMES`). Acht Tests wurden rot, u. a. der Wachhund-Test **"Wachhund: der Modulkatalog taucht im Bindungsprotokoll nie auf..."**, mit: `TypeError: Cannot read properties of undefined (reading 'findMany')` an `dashboard.service.ts:175`. Rückbau zurückgenommen, Testlauf wieder grün (27/27).
3. **Aufgabe 3 — Bestandsaufnahme-Zeile probeweise falsch gesetzt.** `Stand` der Zeile `dashboard.service.ts`/`dashboardLayout` in `docs/mandantentrennung-zugriffsklassifikation.md` auf `ungebunden` gesetzt (Quelltext ist tatsächlich `gebunden`). Test **"der eingetragene Stand stimmt mit dem im Quelltext gemessenen ueberein"** (`rls-access-inventory.spec.ts`) wurde rot mit: `AssertionError: Abweichender Stand (Dokument vs. Quelltext): apps/api/src/dashboard/dashboard.service.ts::dashboardLayout — dokumentiert=ungebunden, gemessen=gebunden`. Zurückgenommen, Testlauf wieder grün (10/10).
## Issues Encountered
Keine unerwarteten Blocker. Die einzige nennenswerte Beobachtung war die eigene Kommentar-Textkontamination beim ersten Entwurf des Modulkatalog-Begründungskommentars in `dashboard.service.ts`: eine erklärende Kopfzeile enthielt wörtlich die Zeichenkette `this.prisma.module`, was die naive Rohtreffer-Zählung (`grep -o "this\.prisma\.[a-zA-Z]*"`) fälschlich auf zwei statt einen Treffer trieb. Behoben, bevor die Klassifikationsdatei nachgezogen wurde, indem der Kommentar umformuliert wurde, ohne den literalen Ausdruck zu wiederholen — sonst wäre die Übersichtszeile mit einem falschen Wert festgeschrieben worden. `rls-access-inventory.spec.ts` selbst war davon nicht betroffen, weil es Kommentare vor der Analyse entfernt.
## Known Stubs
Keine. Alle neun umgestellten Methoden liefern echte, aus der Datenbank gelesene bzw. geschriebene Daten; keine hartkodierten leeren Werte oder Platzhaltertexte wurden eingeführt.
## Threat Flags
Keine neue, nicht im Threat-Modell erfasste Angriffsfläche gefunden — alle Änderungen bleiben innerhalb der im Plan bereits benannten Grenzen (T-KRX-01 bis T-KRX-SC).
## User Setup Required
None - keine externe Dienstkonfiguration nötig. Der Schalter (`DATABASE_URL`, Rolle `tessera`) bleibt unverändert aus.
## Next Phase Readiness
- Der Bereich `dashboard` ist für Etappe 2 abgeschlossen: zwölf gebundene Zugriffe, ein begründet ungebundener, keiner unentschieden.
- Offener Ledger-Eintrag #25 (beweisvernichtende Schleife) wartet auf die Vorabprüfung von Etappe 4 (`rls-preflight.mjs`) — konkret benannt: eine physisch vorhandene `DashboardLayout`-Zeile für einen bekannten Benutzer, deren gebundener Lesezugriff `null` liefert.
- Die strukturelle Eindeutigkeitsfrage von `DashboardLayout.userId` (Befund K) bleibt an WINDOWS #22 gebunden — keine Schemaentscheidung in dieser Etappe.
- Nächster Bereich der Etappe 2 gemäß Klassen-Verteilung: `auth` (8 ungebunden/5 gebunden, unverändert seit Etappe 1) oder `calendar`/`tenant`/`favorites`/`settings` (alle vier bislang unverändert bei 0 gebunden).
---
*Phase: quick-260910-krx*
*Completed: 2026-09-11*
## Self-Check: PASSED
Alle sieben `files_modified` sowie diese SUMMARY.md wurden geprüft (`[ -f "$f" ]`) — alle vorhanden. Alle drei Task-Commit-Hashes (`6744918`, `e0ce594`, `67b5024`) wurden gegen `git log --oneline --all` geprüft — alle vorhanden. Keine fehlenden Elemente.