From cc26197fa1cf59cc0807facf54096d7eb951493b Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 11 Sep 2026 14:18:11 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20Etappe=202=20der=20Mandantentrennung=20?= =?UTF-8?q?abgeschlossen=20=E2=80=94=20alle=20zwoelf=20Bereiche=20gebunden?= =?UTF-8?q?=20und=20verifiziert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .planning/.continue-here.md | 41 +-- .planning/HANDOFF.json | 31 ++- .planning/STATE.md | 3 +- .../260911-gwh-SUMMARY.md | 250 ++++++++++++++++++ .../260911-gwh-VERIFICATION.md | 121 +++++++++ 5 files changed, 416 insertions(+), 30 deletions(-) create mode 100644 .planning/quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/260911-gwh-SUMMARY.md create mode 100644 .planning/quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/260911-gwh-VERIFICATION.md diff --git a/.planning/.continue-here.md b/.planning/.continue-here.md index bed2267..ea7b191 100644 --- a/.planning/.continue-here.md +++ b/.planning/.continue-here.md @@ -1,13 +1,13 @@ --- context: default -phase: mandantentrennung-etappe-2 +phase: mandantentrennung-etappe-3 task: null -total_tasks: 3 +total_tasks: 5 status: paused -last_updated: 2026-09-09T09:16:11.548Z +last_updated: 2026-09-11T13:00:00.000Z --- -# Wiedereinstieg — Mandantentrennung, vor Etappe 2 +# Wiedereinstieg — Mandantentrennung, ETAPPE 2 ABGESCHLOSSEN, vor WINDOWS #27 und Etappe 3 ## Critical Anti-Patterns @@ -21,15 +21,21 @@ Alle vier stammen aus tatsaechlichen Fehlschlaegen dieser Sitzung, nicht aus Vor | Zeichensatz beim Veroeffentlichen angenommen | Die Handbuch-Webseite ging mit zerlegten Umlauten live ("Für" statt "Fuer"), weil im lokalen Test der Zeichensatz fehlte und ich annahm, das Veroeffentlichen setze ihn schon richtig. | advisory | Seiten mit deutschem Text als reines ASCII ausliefern (Sonderzeichen als `\uXXXX` in den Daten). Dann kann kein Zeichensatz sie falsch auslegen. Die fertige Datei mit `all(ord(c)<128 ...)` pruefen. | -Etappe 1 der Mandantentrennung ist abgeschlossen, committet und gepusht (`5228f28`). -Der Arbeitsbaum ist sauber, die CI gruen, 701 Tests gruen. +**Etappe 2 ist am 2026-09-11 abgeschlossen.** Alle zwoelf Bereiche sind gebunden und +einzeln verifiziert (ldap, groups, tenders, dkv, user, module-registry, dashboard, +calendar, tenant, auth, favorites+settings); die drei Datenbankregeln wurden auf +Anweisung des Users vorgezogen (260910-jab). Endstand: 994 Tests in 62 Dateien +(Ausgang 701/53), 137 Live-Pruefungen (Ausgang 8), 65 Paare / 68 ungebunden / +178 gebunden — jeder ungebundene Zugriff liegt auf einer plattformglobalen Tabelle +oder einem benannten Startpfad. Alles gepusht, Arbeitsbaum sauber. -Die Sitzung lief ueber Quick-Tasks, nicht ueber Phasen — es gibt daher kein -aktives Phasenverzeichnis. Der Meilenstein v1.2 ist seit dem 2026-09-07 zu, ein -neuer wurde nicht begonnen. +**Der Umstellungsschalter ist AUS.** `DATABASE_URL` zeigt weiter auf die Rolle +`tessera` mit BYPASSRLS. Der User hat ausdruecklich verlangt, beim Scharfschalten +angehalten und gefragt zu werden. -Der Umstellungsschalter ist AUS: `DATABASE_URL` zeigt weiterhin auf die Rolle -`tessera` mit BYPASSRLS. Das ist Absicht — siehe Sperrgrund unten. +Die maschinelle Bestandsaufnahme hat eine bekannte Blindstelle (WINDOWS #27): +`include:`/`_count:` in fremd geschuetzte Tabellen sieht sie nicht. Alle heutigen +Instanzen sind einzeln geprueft; der Mechanismus muss VOR Etappe 4 geschlossen werden. @@ -132,10 +138,15 @@ dafuer ist jeder Code, der Leere als Abwesenheit deutet und daraufhin loescht. V Umstellung eines Bereichs ist zu pruefen, ob er solchen Code enthaelt. -Etappe 2 beginnen, und zwar NICHT mit dem groessten Bereich. Einstieg ist `ldap` -(21 Zugriffe, davon 9 bereits mandantengebunden): dort sitzt der gefaehrlichste -Loeschzweig, die Wirkung ist dort am besten pruefbar, und der Bereich ist klein genug -fuer einen Durchlauf. Danach `groups` (37), dann `tenders` (62). +1. WINDOWS #27 schliessen — Detektor in `rls-access-inventory.spec.ts` um + `include:`/`select:`/`_count:` auf Modellnamen erweitern, Zieltabelle als eigene + Fundstelle fuehren. Zwingend vor Etappe 4. +2. Etappe 3 planen (drei Teile): (a) Anmeldenamen pro Mandant — Schema-Aenderung, + Anmeldeweg muss den Mandanten VOR der Suche kennen, SECURITY-DEFINER-Funktionen + mit zwei Gleichheitsbedingungen; (b) Benutzerdimension — `app.current_user`, + `current_user_id()`, forTenant() erweitern, Regeln der zehn nutzerbezogenen + Tabellen; (c) Systemkontext fuer die sechs Hintergrunddienst-Faelle. +3. Etappe 4 — Scharfschalten. NUR nach Rueckfrage beim User. Frische Sitzung, dann `/gsd-resume-work`. diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json index edea266..ef96bd9 100644 --- a/.planning/HANDOFF.json +++ b/.planning/HANDOFF.json @@ -1,6 +1,6 @@ { "version": "1.0", - "timestamp": "2026-09-09T09:16:11.548Z", + "timestamp": "2026-09-11T13:00:00.000Z", "phase": null, "phase_name": "Mandantentrennung wirksam machen (Etappenarbeit ausserhalb der Phasen, ueber Quick-Tasks)", "phase_dir": null, @@ -9,27 +9,30 @@ "total_tasks": 4, "status": "paused", "completed_tasks": [ - {"id": 1, "name": "Etappe 1 / forTenant() auf eine Verbindung zwingen, live nachgewiesen", "status": "done", "commit": "bbf1795"}, - {"id": 2, "name": "Etappe 1 / Anmeldeweg ueber drei SECURITY-DEFINER-Funktionen", "status": "done", "commit": "de50297"}, - {"id": 3, "name": "Etappe 1 / alle 227 Zugriffe klassifiziert, maschinell abgesichert", "status": "done", "commit": "5f3a39c"}, - {"id": 4, "name": "Etappe 1 / Browser-Gegenprobe der Anmeldung (lokal)", "status": "done", "commit": "da0ac04"} + {"id": 1, "name": "Etappe 1 / forTenant() repariert, Anmeldeweg ueber SECURITY DEFINER, 227 Zugriffe klassifiziert", "status": "done", "commit": "5228f28"}, + {"id": 2, "name": "Etappe 2 / alle zwoelf Bereiche gebunden, jeder einzeln verifiziert (260909-ipc .. 260911-gwh)", "status": "done", "commit": "1240932"}, + {"id": 3, "name": "Zwischendurch auf Anweisung des Users: drei Datenbankregeln geschlossen (260910-jab)", "status": "done", "commit": "03fb3bf"} ], "remaining_tasks": [ - {"id": 5, "name": "Etappe 2: 31 Einheiten vollstaendig + 9 teilweise auf forTenant umstellen, nach Bereichen gebuendelt (tenders 62, groups 37, ldap 21, dkv 21, user 17, module-registry 17, dashboard 13, calendar 12, tenant 8, favorites 7, settings 4)", "status": "not_started"}, - {"id": 6, "name": "Etappe 3: Systemkontext fuer Hintergrundlaeufe plus WINDOWS #19 (nullable tenantId bei SearchProvider und TenderRssFeedSource)", "status": "not_started"}, - {"id": 7, "name": "Etappe 4: Scharfschalten (DATABASE_URL auf tessera_app) mit Vorabpruefung und dokumentiertem Rueckweg", "status": "not_started"} + {"id": 4, "name": "WINDOWS #27 schliessen: Relations-Blindstelle der Bestandsaufnahme (include:/_count: in fremde Tabellen) — ZWINGEND vor Etappe 4", "status": "not_started"}, + {"id": 5, "name": "Etappe 3a: Anmeldenamen pro Mandant eindeutig (Produktentscheidung User 2026-09-10) — Schema @@unique([tenantId, username/email]), Anmeldeweg kennt Mandant VOR der Suche, SECURITY-DEFINER-Funktionen mit zwei Gleichheitsbedingungen", "status": "not_started"}, + {"id": 6, "name": "Etappe 3b: Benutzerdimension in den Regeln (Produktentscheidung User 2026-09-10) — app.current_user/current_user_id(), forTenant() um userId erweitern, Regeln der zehn nutzerbezogenen Tabellen", "status": "not_started"}, + {"id": 7, "name": "Etappe 3c: Systemkontext fuer die sechs Hintergrunddienst-Faelle (#21 dkv, #30 settings, vier beides-Uebergaben aus tenders/ldap)", "status": "not_started"}, + {"id": 8, "name": "Etappe 4: Scharfschalten (DATABASE_URL auf tessera_app) mit rls-preflight.mjs — Vorabpruefung muss die stillen Leere-Faelle #23/#25/#26/#28/#31/#32 abdecken. USER WILL HIER GEFRAGT WERDEN.", "status": "not_started"} ], "blockers": [ - {"description": "Etappe 4 darf erst nach Etappe 2 und 3 laufen. Wird vorher scharf geschaltet, liefern die noch nicht umgestellten Abfragen null Zeilen statt zu vieler.", "type": "technical", "workaround": "Reihenfolge einhalten; rls-preflight.mjs vor dem Umschalten laufen lassen"} + {"description": "Etappe 4 darf erst nach Etappe 3 und nach Schliessen von WINDOWS #27 laufen. Der User hat ausdruecklich verlangt, beim Scharfschalten angehalten und gefragt zu werden.", "type": "process", "workaround": "Reihenfolge einhalten"} ], "async_jobs": [], "human_actions_pending": [], "decisions": [ - {"decision": "Anmeldeweg ueber SECURITY-DEFINER-Funktionen statt Policy oder zweiter Rolle", "rationale": "Eine Policy ist ein Zeilenpraedikat und haette zwangslaeufig die ganze Benutzertabelle freigegeben. Die Funktion pinnt die Ausnahme auf feste Spaltenliste, Gleichheitsbedingung und LIMIT 1.", "phase": "Etappe 1"}, - {"decision": "Benanntes Volume fuer user-files statt Bind-Mount", "rationale": "Das Image uebereignet /app/user-files an uid 1001; ein frisch angelegtes Host-Verzeichnis gehoert root und haette aus dem Datenverlust einen kaputten Upload gemacht.", "phase": "Quick 260909-cx0"}, - {"decision": "Datenverlust in der Datenbank ist derzeit hinnehmbar", "rationale": "Ausdrueckliche Aussage des Users am 2026-09-09: nichts laeuft produktiv. Erlaubt beim Scharfschalten den direkten Weg. Gilt nur, solange das so bleibt.", "phase": "Etappe 4 (Vorgriff)"} + {"decision": "Anmeldenamen pro Mandant eindeutig, nicht plattformweit", "rationale": "Produktentscheidung des Users am 2026-09-10 — m.schmidt darf es bei Firma A und B geben", "phase": "Etappe 3"}, + {"decision": "Kollegen derselben Firma strikt getrennt — Benutzerdimension in die Datenbankregeln", "rationale": "Produktentscheidung des Users am 2026-09-10; heute trennt nur der Anwendungscode, die Datenbank kennt nur den Mandanten", "phase": "Etappe 3"}, + {"decision": "req.tenantPrisma entfernt, Middleware geloescht", "rationale": "Bei jeder Anfrage gebaut, nirgends gelesen; Middleware war nirgends registriert; neun Bereiche haben dienst-internes forTenant() als Konvention festgelegt", "phase": "260911-e2s"}, + {"decision": "Datenbankregeln VOR Abschluss von Etappe 2 vorgezogen", "rationale": "Ausdrueckliche Anweisung des Users am 2026-09-10 — offene Loecher werden vergessen", "phase": "260910-jab"}, + {"decision": "Datenverlust in der Datenbank ist derzeit hinnehmbar", "rationale": "User 2026-09-09: nichts laeuft produktiv. Gilt nur solange das so bleibt.", "phase": "Etappe 4 (Vorgriff)"} ], "uncommitted_files": [], - "next_action": "Etappe 2 beginnen: docs/mandantentrennung-zugriffsklassifikation.md lesen und den ersten Bereich buendeln. Sinnvoller Einstieg ist NICHT der groesste Bereich, sondern ldap (21 Zugriffe, 9 davon bereits mandantengebunden) — dort sitzt der gefaehrlichste Loeschzweig, und die Wirkung ist dort am besten pruefbar.", - "context_notes": "Die Sitzung lief ueber Quick-Tasks, nicht ueber Phasen; es gibt daher kein aktives Phasenverzeichnis. Der entscheidende Fund war, dass forTenant() selbst kaputt war (Kontext auf einer Verbindung, Abfrage auf einer anderen) — die Mandantentrennung hat nie funktioniert. Das ist behoben und live belegt. Wichtig fuer die Fortsetzung: erst pruefen, ob das Fundament traegt, bevor darauf gebaut wird; genau das hat hier einen stillen Datenverlust verhindert. Der Arbeitsbaum ist sauber, alles ist gepusht, die CI ist gruen." + "next_action": "WINDOWS #27 schliessen (Relations-Blindstelle), dann Etappe 3 planen. NICHT direkt scharfschalten.", + "context_notes": "Etappe 2 lief ueber zwoelf Quick-Tasks plus einen Regel-Durchlauf, jeder mit Planer, Plan-Pruefer, Executor, Verifizierer. ZEHN Lieferungen wurden vom jeweils NAECHSTEN Schritt gefangen, nie vom eigenen: vier geschrumpfte Zaehlungen, zwei nicht committete Messungen, zwei Zusammenfassungen mit N statt N-1, handgepflegte Dokumentstellen uebersprungen, Falsifizierungsnachweise nur in Commit-Nachrichten, eine Wegwerf-Tabelle ohne createdAt/updatedAt, ein Selbstwiderspruch, ein Pruefer der etwas als plausibel durchwinkte, ein still fehlgeschlagener git add, und zwei Agenten die am Sitzungslimit NACH getaner Arbeit abbrachen (dkv-Executor, gwh-Planer — beide Male lag die Arbeit vollstaendig auf der Platte; git status ist die Wahrheit, nicht der Bericht). Die vollstaendige Liste steht in jedem Planer-Auftrag der spaeten Bereiche. Nebenfunde ohne Mandantenbezug, alle behoben: DKV-Download-Luecke, drohender DKV-Passwortverlust, Selbstloesch-Riegel der nie griff, Startfehler bei Neuinstallation, adminResetPassword ohne Mandanten- und Rollenpruefung, Widget-Besitzriegel bei Favoriten, sechs luegende Kommentare, und fuenf Bereiche ohne jede Testdatei." } diff --git a/.planning/STATE.md b/.planning/STATE.md index 549e34d..6c4f67a 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -389,6 +389,7 @@ None yet. | 260911-cwh | Mandantentrennung Etappe 2, Bereich calendar — alle 12 Zugriffe gebunden, ein Klient je Methode, sechs Methoden. Bereich hatte KEINE Testdatei (dkv-Form); `calendar.service.spec.ts` neu mit 23 Tests. **Gespeicherte Zugangsdaten zu fremden Kalender-Servern** — ein Fremdzugriff waere hier der Schluessel zu einem fremden Exchange/CalDAV. **Der dkv-Passwortverlust-Fall existiert hier NICHT**, in beiden Haelften belegt: der Dienst schreibt `encryptedPassword` nur bei `dto.password !== undefined`, und `calendar-source-form.tsx` laesst ein leeres Feld WEG statt einen leeren Text zu schicken; als drei Tests festgenagelt, weil ein nicht festgenagelter Freispruch still aufhoeren kann zu gelten. **Zwischenspeicher-Schluessel `userId:from:to` ohne Mandantenanteil ist sicher:** `User.id` ist `@default(uuid())`, Kette Schema -> `auth.service.ts sub: user.id` -> `JwtStrategy.validate` -> `extractContext` Glied fuer Glied belegt; die Etappe-3-Entscheidung (Anmeldenamen pro Mandant) beruehrt `username`/`email`, nicht `id`. **Eigener Gefahrenfall — halb gebundene Aggregationsschleife:** `fetchAndCacheEvents` liest Quellen und schreibt den Synchronstatus auf Erfolgs- UND Fehlerpfad zurueck, innerhalb von `Promise.allSettled`; gebundene Lesung mit ungebundenem Rueckschreiben haette Ereignisse still fallen lassen — beide Rueckschreibungen gebunden und als Tests festgenagelt, eines davon vom Pruefer eigenhaendig zurueckgebaut (genau 1 von 23 rot, exakte Meldung). **Frontend macht aus lauten Fehlern stille:** `calendar-widget.tsx` und `calendar-settings-panel.tsx` fangen jeden Fehler in denselben leeren Zustand — ein 403 sieht aus wie ein leerer Kalender; NICHT angefasst, als WINDOWS #26 offen festgehalten. Besitzpruefungen in allen drei Pfaden echt (403, nicht 404). Keine Eindeutigkeitskette, daher keine Konfliktuebersetzung, die nichts uebersetzt. **Lehre aus dashboard angewandt:** 4 der 13 neuen Pruefungen laufen ueber den GENERIERTEN Client, mit Laufzeitvergleich der Wegwerf-Tabelle gegen `schema.prisma` (17 = 17 Spalten). **Verifiziert 12/12** (883/883 Tests, 57 Dateien, Typpruefung sauber, 101/101 Live-Pruefungen; Klassenverteilung 31/17/13/2 = 63 und Ledger-Zaehler vom Pruefer nachgerechnet). Ein Selbstwiderspruch in der Zusammenfassung ('keine Abweichung' vs. 'kein TDD-Zyklus') berichtigt | 2026-09-11 | bf5fc4d,77cb124,e0e163e | [260911-cwh-mandantentrennung-etappe-2-bereich-calen](./quick/260911-cwh-mandantentrennung-etappe-2-bereich-calen/) | | 260911-e2s | Mandantentrennung Etappe 2, Bereich tenant — von anderer Art: alle 8 Zugriffe gehen auf die Mandantentabelle SELBST, die per Definition keinen Mandanten hat. **'Nichts zu binden' war trotzdem falsch, und der Grund ist der wichtigste Fund seit dem kaputten Helfer in Etappe 1:** drei der acht Zugriffe (`findAll`, `findOne`, `remove` im Controller) zaehlen ueber `include: { _count: { select: { users } } }` in die GESCHUETZTE Tabelle `User` hinein — Prisma 6.19 rendert das als `LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)`, das unter DEREN Regel laeuft. Nach dem Scharfschalten haette die Mandantenliste des Plattform-Admins fuer jeden Mandanten 0 Benutzer gezeigt, und der Loeschriegel T-02-09 waere vakuum geworden (der Fremdschluessel faengt es noch, aber als 500 statt 400). Behoben per Fan-out je Mandant ueber gebundenen Client, Muster aus `UserService.findAllForPlatformAdmin`. **Die Bestandsaufnahme ist fuer Relationszugriffe strukturell blind** — sie sieht nur `this.prisma.`, nicht was ein `include:` in eine zweite Tabelle hineinrechnet. Alle 19 `include:`-Stellen und alle `_count`-Stellen einzeln beurteilt, vom Orchestrator UND vom Verifizierer unabhaengig gegengeprueft (der Plan-Pruefer hatte diesen Punkt als 'plausibel' durchgewinkt statt ihn zu pruefen): nur diese drei waren gefaehrlich. Der MECHANISMUS bleibt offen und ist als WINDOWS #27 festgehalten — der Planer wollte keinen Eintrag, weil die Instanz behoben ist; Orchestrator und Verifizierer sahen das anders, weil eine Luecke im Messwerkzeug, die nachweislich einen echten Defekt verborgen hat, genau dafuer ins Ledger gehoert. **Die seit Etappe 1 offene Architekturfrage ist entschieden:** `req.tenantPrisma` wurde bei jeder Anfrage gebaut und NIRGENDS gelesen; neun Bereiche haben die Konvention auf dienst-internes `forTenant()` festgelegt. Middleware geloescht (sie war nirgends registriert — der Auftrag irrte bei `app.module.ts:57`, dort ist der Guard verdrahtet), Guard ohne Prisma-Abhaengigkeit, setzt nur noch `req.tenantId` (22 Leser in 9 Dateien) und den `x-tenant-id`-Wechsel fuer SUPER_ADMIN (4 Frontend-Stellen) — beides erstmals getestet; Guard und Middleware hatten NIE Tests, 'ihre Tests' in Etappe 1 war eine Annahme. Totes Kabel, das wie eine Sicherung aussieht, ist schlimmer als keins. Ausnahmeliste in `rls-access-inventory.spec.ts` geleert und mit Wachhund versehen. Drei Kommentare berichtigt, die `TenantMiddleware`/`req.tenantPrisma` als lebendig beschrieben. Executor fing einen still fehlgeschlagenen `git add` (2 von 7 Dateien) selbst an `git status` und lieferte nach. **Verifiziert 10/10 mit vier eigenhaendigen Falsifizierungen** (Header-Wechsel zweimal gebrochen, Fan-out gebrochen, Wachhund ausgeloest — je exakt die benannten Tests rot; 911/911 Tests, 59 Dateien, Typpruefung sauber, 110/110 Live-Pruefungen; 64 Paare und Klassenverteilung 32/17/13/2 nachgerechnet) | 2026-09-11 | 652e762,11f5731,17dca0d,c8de72e | [260911-e2s-mandantentrennung-etappe-2-bereich-tenan](./quick/260911-e2s-mandantentrennung-etappe-2-bereich-tenan/) | | 260911-fh9 | Mandantentrennung Etappe 2, Bereich auth — die drei in Etappe 1 bewusst ausgelassenen Wege (`getMe`, `changePassword`, `adminResetPassword`) gebunden, alle drei brauchten neue Signaturen (nahmen nur `userId`). Der Anmeldeweg ueber die drei SECURITY-DEFINER-Funktionen NICHT angefasst, per `pg_proc` belegt (weiterhin genau 9 Spalten, auch nachdem die Wegwerf-Tabelle `User` 5 fehlende Spalten bekam). Verbleibende 3 'ungebundene' Stellen sind die `$queryRaw`-Anmeldesuchen, keine Modellzugriffe. **Falle, die der Auftrag selbst gestellt hatte:** Selbstbedienung darf NICHT an `req.tenantId` binden — der Guard laesst SUPER_ADMIN diese Kennung per `x-tenant-id` umschalten (Marktplatz), 'mein Profil' haette ihn sich selbst gegenueber unsichtbar gemacht; gebunden wird an den Mandanten aus dem Sitzungsnachweis (`@CurrentUser().tenantId`), der Controller enthaelt null Verweise auf `req.tenantId`/`x-tenant-id`. **Zwei Loecher in `adminResetPassword` geschlossen, keines davon ein Mandantenproblem:** der Weg pruefte weder den Mandanten des Ziels noch dessen Rolle — ein ADMIN konnte das Passwort eines SUPER_ADMIN ueberschreiben. Beides jetzt dicht, SUPER_ADMIN-Pfad ueber `UserService.findByIdForPlatformAdmin`; `AuthModule` importiert `UserModule`, zyklusfrei. Der Schwesterweg `PATCH /users/:id` hat dieselbe Rollenluecke (T-02-08 prueft nur das ZUWEISEN der Rolle, nicht die bestehende Rolle des Ziels) — ausserhalb der Erlaubnisliste, als WINDOWS #29 festgehalten. **Umgekehrte Fehlerrichtung ist hier leise, nicht laut:** `getMe`-Leere wird zu 200 mit leerem Rumpf, `header.tsx` tut bei `if (u)` nichts — 'nicht angemeldet' und 'Zeile unsichtbar' sind derselbe Wert (WINDOWS #28); `changePassword`-Leere liest sich als `networkError`. Identitaets-Attrappe (ldap-Form) durch asymmetrischen Doppel ersetzt: ungebundener Nachbau ohne Modelle, gebundener ohne `$queryRaw` — beide Grenzen einzeln falsifizierbar. **Verifiziert 8/8** (951/951 Tests, 60 Dateien, Typpruefung sauber, 120/120 Live-Pruefungen; zwei Falsifizierungen vom Pruefer eigenhaendig reproduziert — genau 4 bzw. 2 benannte Tests rot) | 2026-09-11 | 9782bea,92aa8c4,f68beb3 | [260911-fh9-mandantentrennung-etappe-2-bereich-auth-](./quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/) | +| 260911-gwh | **Mandantentrennung Etappe 2, Bereiche favorites + settings — LETZTER Durchlauf, Etappe 2 abgeschlossen.** 7 `favoriteLink`-Zugriffe und 3 `smtpConfig`-Anfragepfade gebunden; genau ein `smtpConfig`-Zugriff bleibt bewusst offen: der Startpfad, umbenannt in `loadAnySmtpConfigForStartupTransport()` — SECHSTER Fall der Hintergrunddienst-Falle (`findFirst()` ohne Mandanten beim Hochfahren in `mail.module.ts`; heute bedient er einen willkuerlichen Mandanten, nach dem Scharfschalten null), beide Zustaende am Ort, WINDOWS #30. **Befund K geschlossen:** `getDecryptedSmtpConfig(tenantId)` bindet — die Reihenfolgebedingung fuer Etappe 4 aus dem tenders-Lauf ist erfuellt und in Kritikschrift (t4)/(d4) und Klassifikation als erfuellt vermerkt. **Widget-Besitzriegel in `favorites.create()` eingebaut, weil GEMESSEN noetig:** Pruefung 7 zeigt, dass ein gebundenes Anlegen mit fremder `widgetId` GELINGT — die Fremdschluessel-Pruefung umgeht den Zeilenschutz; vom Verifizierer live reproduziert und der Riegel durch Rueckbau falsifiziert (genau 4 Tests rot). Beide Bereiche hatten keine Testdatei fuer ihren Dienst; `favorites.service.spec.ts` (23) und `settings.service.spec.ts` (20) neu, `nodemailer` gemockt. Ledger #31/#32 fuer die stille Leere (leere Favoritenleiste = 'nie etwas gespeichert'; fehlende SMTP-Konfiguration = 'nicht eingerichtet', obwohl die Zugangsdaten da sind). Der Planer scheiterte am Sitzungslimit NACH dem Schreiben des Plans, VOR der Rueckmeldung — Plan lag vollstaendig auf der Platte (1226 Zeilen, Struktur gueltig), vom Orchestrator committet, vom Pruefer als Erstleser gegen den Baum gehalten. **Verifiziert 9/9** (994/994 Tests, 62 Dateien, Typpruefung sauber, 137/137 Live-Pruefungen; Uebersicht 68/178, Klassenverteilung 33+17+13+2=65 und Migrations-Zaehlung 4+3+16=23 vom Pruefer nachgerechnet) | 2026-09-11 | 88896d3,8f2c13a,b5f22e2,1240932 | [260911-gwh-mandantentrennung-etappe-2-bereiche-favo](./quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/) | | 260909-eor | Etappe 1 der Mandantentrennung: Anmeldeweg mandantenfaehig gemacht und alle Zugriffe klassifiziert. **Kernfund (#20):** `forTenant()` setzte den Mandantenkontext per set_config auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client — empirisch reproduziert (set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL). Die Trennung hat damit nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzten; nach dem Scharfschalten haetten diese Abfragen NULL Zeilen geliefert, was der LDAP-Loeschzweig als 'Gruppe im Verzeichnis verschwunden' gedeutet und geloescht haette. Behoben und live nachgewiesen. Der Anmeldeweg bekam drei SECURITY-DEFINER-Funktionen als schmale Ausnahme (feste Spaltenliste, Gleichheitsbedingung, LIMIT 1) — eine Policy haette nicht gereicht, weil sie zwangslaeufig die ganze Tabelle freigibt. Browser-Gegenprobe lokal bestanden: Anmeldung laedt das Portal, falsches Kennwort verraet weiterhin nicht welches Feld, Kennwort-vergessen laeuft durch (der einzige Protokollfehler war ein lokal fehlender Mailserver, also NACH dem Datenbankzugriff). Klassifikation aller 227 Zugriffe in 59 Einheiten, maschinell gegen Abdriften abgesichert: 31 muessen mandantengebunden werden, 9 teilweise, 16 betreffen keine mandantengebundene Tabelle, 3 bleiben bewusst uebergreifend. 701 Tests gruen | 2026-09-09 | da0ac04 | [260909-eor-anmeldeweg-mandantenfaehig-machen-und-al](./quick/260909-eor-anmeldeweg-mandantenfaehig-machen-und-al/) | | 260910-jab | Die drei zu kurz greifenden Datenbankregeln geschlossen — T-JTS-02, T-JTS-03, WINDOWS #19 (bewusste Reihenfolge-Abweichung, vorgezogen auf Nutzerwunsch, statt wie geplant nach Etappe 2). Neue, handgeschriebene, lokal angewandte Migration `20260910120000_rls_widen_membership_grant_and_platform_read`: `GroupMembership` prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer), `ModuleGrant` prueft zusaetzlich beide moeglichen Ziele mit Leer-Zulassung (D-04), `TenderRssFeedSource` bekommt vier nach Befehl getrennte Regeln (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt weiterhin einen Mandanten — die Trennung ist noetig, weil ein einzelner USING-Ausdruck sonst auch UPDATE/DELETE mitregelt). `SearchProvider` bewusst NICHT angefasst: die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile). Drei loch-behauptende Pruefungen im Wegwerf-Werkzeug UMGEKEHRT statt geloescht (66→74 Pruefungen), mit Verweis auf die alten Pruefungsnamen und Befundkennungen im Meldetext. Genau EIN Anwendungspfad musste mitgebunden werden (`TenderRssFeedSourceService.listForUser`) — sonst haette die Reparatur ihn still von 'liefert nach dem Scharfschalten nichts' auf 'liefert nur die plattformweiten Zeilen, taeuscht Vollstaendigkeit vor' verschlechtert; Falsifizierungsnachweis gefuehrt (Bindung zurueckgenommen, genau ein Test rot, zurueckgesetzt). WINDOWS #19 geschlossen mit Beleg, WINDOWS #24 neu angelegt (Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit #19). Aktenstand kohaerent: Klassifikation, Kritikschrift (neuer Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" mit Signaltabelle beider Fehlerrichtungen je Regel), Betriebsanleitung, WINDOWS.md — fuenf ueberholte Bestandsstellen mit Nachtraegen versehen, alte Messprotokolle bleiben woertlich stehen. Selbst gemessen statt uebernommen: Baseline 833/56 Tests, 66/66 Live-Pruefungen; Endstand 839/56, 74/74; keine zweite Sitzungsvariable fuer den Benutzer gefunden (nur `app.current_tenant`). Rule-1-Fix: implizites `any` in `tenders.controller.ts` nach der Bindung behoben. `npx prisma` versuchte ungefragt Prisma 8 herunterzuladen — abgebrochen, lokale gepinnte 6.19.3 verwendet | 2026-09-10 | f4f3115,6b23735,03fb3bf | [260910-jab-mandantentrennung-die-drei-zu-kurz-greif](./quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/) | @@ -432,6 +433,6 @@ sind. Kein Anlass, sie vorher erneut vorzulegen. Last session: 2026-09-11T08:01:13.009Z Resumed: 2026-09-09 — Sitzung ueber /gsd-resume-work fortgesetzt; Einstiegspunkt Etappe 2 (Bereich ldap), Auswahl des Nutzers steht aus. -Stopped at: Quick 260911-cwh abgeschlossen: Bereich calendar der Etappe 2 (Mandantentrennung) umgestellt, 12/12 Zugriffe gebunden, WINDOWS #26 neu offen +Stopped at: **ETAPPE 2 DER MANDANTENTRENNUNG ABGESCHLOSSEN (2026-09-11).** Alle zwoelf Bereiche umgestellt und einzeln verifiziert: ldap 7/7, groups 9/9, tenders 8/9+Fix, dkv 9/9, user 10/10, module-registry 9/9, dashboard 10/11+Fix, calendar 12/12, tenant 10/10, auth 8/8, favorites+settings 9/9; dazu die drei Datenbankregeln (260910-jab, 11/11). Endstand: 994 Tests in 62 Dateien (Ausgang 701/53), 137 Live-Pruefungen gegen die Wegwerf-Datenbank (Ausgang 8), Klassifikation 65 Paare / 68 ungebunden / 178 gebunden — jeder ungebundene Zugriff liegt auf einer plattformglobalen Tabelle oder einem benannten Startpfad, keiner aus Versehen. Alles gepusht. DER SCHALTER IST WEITER AUS. NAECHSTE SCHRITTE: (A) ETAPPE 3 — die zwei Produktentscheidungen des Users vom 2026-09-10: Anmeldenamen pro Mandant (Schema `@@unique([tenantId, username/email])`, Anmeldeweg muss den Mandanten VOR der Suche kennen, SECURITY-DEFINER-Funktionen mit zwei Gleichheitsbedingungen) und Benutzerdimension in den Regeln (zweite Sitzungsvariable `app.current_user`/`current_user_id()`, forTenant() um userId erweitern, Regeln der zehn nutzerbezogenen Tabellen). Dazu Systemkontext fuer die sechs Hintergrunddienst-Faelle (#21, #30 und die vier `beides`-Uebergaben aus tenders/ldap). (B) VOR ETAPPE 4 ZWINGEND: WINDOWS #27 (Relations-Blindstelle der Bestandsaufnahme) schliessen — sonst stuetzt sich die Vorabpruefung auf ein Werkzeug, das `include:`/`_count:` in fremde Tabellen nicht sieht. (C) ETAPPE 4 — Scharfschalten mit rls-preflight.mjs; die Vorabpruefung muss die stillen Leere-Faelle #23/#25/#26/#28/#31/#32 abdecken. DER USER HAT AUSDRUECKLICH GESAGT: beim Scharfschalten anhalten und fragen. Ledger: 14 offen von 32. Resume file: None Last activity: 2026-09-10 - Completed quick task 260910-jab: Die drei zu kurz greifenden Datenbankregeln (T-JTS-02, T-JTS-03, WINDOWS #19) geschlossen diff --git a/.planning/quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/260911-gwh-SUMMARY.md b/.planning/quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/260911-gwh-SUMMARY.md new file mode 100644 index 0000000..85bc021 --- /dev/null +++ b/.planning/quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/260911-gwh-SUMMARY.md @@ -0,0 +1,250 @@ +--- +phase: quick-260911-gwh +plan: 01 +subsystem: database +tags: [prisma, postgresql, rls, multi-tenancy, nestjs] + +requires: + - phase: quick-260911-fh9 + provides: "Etappe 2 Bereich auth abgeschlossen (baseline 951 tests/60 files, tool 120/120)" +provides: + - "favorites.service.ts: alle fuenf Methoden gebunden (forTenant), Widget-Besitzriegel in create() gegen den Fremdschluessel-Durchgriff" + - "settings.service.ts: getSmtpConfig/saveSmtpConfig/getDecryptedSmtpConfig gebunden, Startpfad umbenannt und als sechster Hintergrunddienst-Fall markiert" + - "Befund K (tenders/dkv haengen an getDecryptedSmtpConfig) erfuellt an allen drei Stellen" + - "Etappe 2 der Mandantentrennung vollstaendig: 65 (Datei,Modell)-Paare klassifiziert, 68 ungebunden/178 gebunden, jeder ungebundene Rest benannt" +affects: [etappe-3-mandantentrennung, etappe-4-rls-preflight] + +actuals: + tokens: 40708 + tasks: 3 + commits: 4 + +tech-stack: + added: [] + patterns: + - "Fremdschluessel-Durchgriff-Riegel: ein gebundener widgetInstance.findUnique VOR dem eigentlichen Schreibzugriff, damit ein FK auf eine zweite mandantengebundene Tabelle nicht am Zeilenschutz vorbei ein Existenzorakel oeffnet (T-GWH-05)" + - "Hintergrunddienst-Startpfad-Markierung: umbenannte, eigenstaendige Methode (kein optionaler Parameter) mit Kopfkommentar, der beide Zustaende (heute falsch, nach dem Scharfschalten stumm) nennt — Vorlage DkvService.loadAnyActiveConfigForScheduler(), hier fortgeschrieben fuer SettingsService.loadAnySmtpConfigForStartupTransport()" + +key-files: + created: + - apps/api/src/favorites/favorites.service.spec.ts + - apps/api/src/settings/settings.service.spec.ts + modified: + - apps/api/scripts/rls-scratch-check.mjs + - apps/api/src/favorites/favorites.service.ts + - apps/api/src/favorites/favorites.controller.ts + - apps/api/src/settings/settings.service.ts + - apps/api/src/mail/mail.module.ts + - docs/mandantentrennung-etappe2-fehlerrichtung.md + - docs/mandantentrennung-zugriffsklassifikation.md + - docs/anleitung-entwicklung.md + - .planning/WINDOWS.md + +key-decisions: + - "Startpfad-Ledger-Eintrag (#30) EIGENSTAENDIG, NICHT an WINDOWS #21 angeschlossen: andere Datei (mail.module.ts statt dkv-scheduler.service.ts), andere Reparatur (Transport je Versand statt Mehrmandanten-Planung), andere Verdeckungsform (Rueckfallkette statt blosser Leere)" + - "Widget-Besitzriegel in favorites.service.ts create() gebaut, weil Pruefung 7 (Aufgabe 1) das Gelingen eines gebundenen create() mit fremdmandantiger widgetId tatsaechlich gemessen hat — der Fremdschluessel prueft am Zeilenschutz von WidgetInstance vorbei (dokumentiertes PostgreSQL-Verhalten)" + - "settings.controller.ts bleibt unveraendert: req.tenantId ist fuer eine ADMIN-Konfigurationsseite die richtige Quelle (D-10), nicht das Claim wie bei auth" + - "favorites.controller.ts extractContext bleibt wortgleich mit dashboard.controller.ts (Guard-Kennung), nicht das Claim wie bei auth — FavoriteLink haengt ueber widgetId an WidgetInstance, das unter der dashboard-Quelle gebunden ist" + +patterns-established: + - "Zwei-Klienten-Testnachbau mit GRENZE als Bauform (settings.service.spec.ts): der ungebundene Nachbau bietet fuer ein Modell NUR die Methoden, die der bewusst ungebundene Pfad tatsaechlich braucht (hier: nur findFirst), der gebundene Klient NUR die Methoden der Anfragewege (findUnique/upsert) — ein gebundener Startpfad scheitert dadurch ebenso hart wie ein ungebundener Anfrageweg" + +requirements-completed: [WINDOWS-18, ETAPPE-2-FAVORITES, ETAPPE-2-SETTINGS] + +coverage: + - id: D1 + description: "favorites.service.ts vollstaendig auf forTenant() umgestellt (5 Methoden, 7 gebundene Zugriffe, 5 Aufrufstellen), Widget-Besitzriegel in create()" + requirement: ETAPPE-2-FAVORITES + verification: + - kind: unit + ref: "apps/api/src/favorites/favorites.service.spec.ts (23 Faelle)" + status: pass + - kind: other + ref: "apps/api/scripts/rls-scratch-check.mjs runFavoritesAreaChecks (8 Pruefungen gegen den generierten Client)" + status: pass + human_judgment: false + - id: D2 + description: "settings.service.ts: getSmtpConfig/saveSmtpConfig/getDecryptedSmtpConfig gebunden, Startpfad umbenannt (loadAnySmtpConfigForStartupTransport), Befund K erfuellt" + requirement: ETAPPE-2-SETTINGS + verification: + - kind: unit + ref: "apps/api/src/settings/settings.service.spec.ts (20 Faelle)" + status: pass + - kind: other + ref: "apps/api/scripts/rls-scratch-check.mjs runSettingsAreaChecks (9 Pruefungen gegen den generierten Client)" + status: pass + human_judgment: false + - id: D3 + description: "Etappe 2 der Mandantentrennung vollstaendig dokumentiert: Klassifikation, Kritikschrift, Anleitung, Ledger auf Endstand" + requirement: WINDOWS-18 + verification: + - kind: unit + ref: "apps/api/src/prisma/rls-access-inventory.spec.ts (11 Faelle, Stand-Vergleich Dokument vs. Quelltext)" + status: pass + human_judgment: false + +duration: 55min +completed: 2026-09-11 +status: complete +--- + +# Quick 260911-gwh: Etappe 2 der Mandantentrennung, Bereiche favorites und settings — LETZTER Lauf Summary + +**favorites.service.ts und settings.service.ts vollstaendig an forTenant() gebunden (12 gebundene Zugriffe, 8 Aufrufstellen), der Fremdschluessel-Durchgriff auf WidgetInstance gemessen und mit einem Besitzriegel geschlossen, der Mailmodul-Startpfad als sechster Hintergrunddienst-Fall markiert und ungebunden gelassen — Etappe 2 der Mandantentrennung ist damit vollstaendig: 65 (Datei,Modell)-Paare, 68 ungebunden/178 gebunden, jeder verbleibende ungebundene Rest ist ein benannter, bewusster Fall.** + +## Performance + +- **Duration:** ca. 55 min +- **Tasks:** 3/3 +- **Files modified:** 11 (2 neu, 9 geaendert) +- **Commits:** 4 (plus die vorangehende PLAN.md-Ablage) + +## Accomplishments + +- `apps/api/scripts/rls-scratch-check.mjs` von 120 auf **137 bestandene Pruefungen** erweitert (`runFavoritesAreaChecks`: 8, `runSettingsAreaChecks`: 9), beide an der Regel WORTGLEICH aus `20260909140000_rls_remaining_tenant_tables` geschnitten, mit dem Fremdschluessel bzw. Eindeutigkeitsindex als mitgebauten Voraussetzungen. +- `favorites.service.ts`: `list`, `create`, `update`, `remove`, `getIconBytes` laufen je ueber GENAU EINEN Klienten `tenantPrisma` (7 gebundene `favoriteLink`-Zugriffe, 1 gebundener `widgetInstance`-Besitzriegel in `create`, 5 Aufrufstellen des Bindungshilfsmittels). +- `settings.service.ts`: `getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig` gebunden (3 Zugriffe, 3 Aufrufstellen); Startpfad umbenannt in `loadAnySmtpConfigForStartupTransport()`, bleibt bewusst ungebunden, sechster Fall der Hintergrunddienst-Falle, WINDOWS #30. +- Befund K (Reihenfolgebedingung aus `tenders` (t4) und `dkv` (d4)) ist ERFUELLT und an allen drei Stellen als solches vermerkt: (t4)-Nachtrag, (d4)-Nachtrag, Hintergrunddienst-Abschnitt der Klassifikation. +- Zwei neue Testdateien mit dem Zwei-Klienten-Nachbau (23 + 20 = 43 neue Faelle), sechs Falsifizierungsnachweise durchgefuehrt und zurueckgenommen. +- Alle fuenf handgepflegten Dokumentstellen auf den Endstand der Etappe 2 gebracht, DERIVIERT gegatet (`rls-access-inventory.spec.ts`). +- Drei neue offene Ledger-Eintraege (#30, #31, #32). + +## Task Commits + +1. **Aufgabe 1: Fehlerrichtung fuer favorites/settings messen** — `88896d3` (docs) — 137 Pruefungen, `## Bereich favorites` (f1-f5), `## Bereich settings` (s1-s5), `## Etappe 2 — Abschluss`, Nachtraege unter Befund K in (t4)/(d4) +2. **Aufgabe 2, RED: neue Testdateien** — `8f2c13a` (test) — favorites.service.spec.ts (23 Faelle), settings.service.spec.ts (20 Faelle), beide gegen die heutige Implementierung erwartungsgemaess rot +3. **Aufgabe 2, GREEN: binden, Startpfad umbenennen, Besitzriegel** — `b5f22e2` (feat) — alle Ziel-Signaturen, vier Falsifizierungsnachweise +4. **Aufgabe 3: Etappe 2 auf Endstand bringen** — `1240932` (docs) — Ledger #30/#31/#32, Klassifikation, Anleitung, Nachtraege, zwei Dokument-Falsifizierungen + +**Plan metadata:** `2a27d96` (docs: Plan fuer Etappe 2, Bereiche favorites und settings) — bereits vor dieser Ausfuehrung committet (Plan-Checker-Lauf). + +_Kein REFACTOR-Commit — die GREEN-Implementierung brauchte keine Nacharbeit._ + +## Files Created/Modified + +- `apps/api/src/favorites/favorites.service.spec.ts` (NEU) — Zwei-Klienten-Nachbau, 23 Faelle +- `apps/api/src/settings/settings.service.spec.ts` (NEU) — Zwei-Klienten-Nachbau mit Grenze als Bauform (ungebunden nur `findFirst`, gebunden nur `findUnique`/`upsert`), 20 Faelle +- `apps/api/scripts/rls-scratch-check.mjs` — `runFavoritesAreaChecks`, `runSettingsAreaChecks` +- `apps/api/src/favorites/favorites.service.ts` — Bindung, Besitzriegel +- `apps/api/src/favorites/favorites.controller.ts` — reicht `tenantId` durch +- `apps/api/src/settings/settings.service.ts` — Bindung, Startpfad-Umbenennung +- `apps/api/src/mail/mail.module.ts` — ruft den umbenannten Startpfad +- `docs/mandantentrennung-etappe2-fehlerrichtung.md` — zwei neue Bereichsabschnitte, Abschluss-Abschnitt, zwei Nachtraege +- `docs/mandantentrennung-zugriffsklassifikation.md` — Uebersicht, Bestandsaufnahme, Klassen-Verteilung, Hintergrunddienst-Abschnitt +- `docs/anleitung-entwicklung.md` — RLS-Tabellenzahl und Beispielabsatz auf den gemessenen Stand +- `.planning/WINDOWS.md` — drei neue offene Eintraege (#30, #31, #32) + +## Tatsächlich gezählte Prüfungs- und Testzahlen + +**Werkzeug (`rls-scratch-check.mjs`):** 120 → **137** bestandene Prüfungen (8 `runFavoritesAreaChecks` + 9 `runSettingsAreaChecks`). + +**Testsuite:** Baseline 951 Tests / 60 Dateien (260911-fh9) → RED (Aufgabe 2, Commit `8f2c13a`): 994 Tests entdeckt / 62 Dateien, 40 rot (22 favorites + 18 settings), 954 grün — beide RED-Zustände intentional, jeweils auf der geplanten Zielsignatur gescheitert, nicht an Syntax/Zero-Discovery → GREEN (Aufgabe 2, Commit `b5f22e2`): 43/43 neue Fälle grün, aber `rls-access-inventory.spec.ts` (Teil der ursprünglichen 951) mit 2 von 11 Fällen erwartungsgemäß rot, 992/994 gesamt grün → Aufgabe 3 (Commit `1240932`): **994/994 grün in 62 Dateien**. + +**Zwischenzeitlich rot: `rls-access-inventory.spec.ts`, zwischen Aufgabe 2 und Aufgabe 3.** Genau wie das Aufgabe-3-Actionblock des Plans selbst vorhersagt ("Ohne Schritt 3 ist `rls-access-inventory.spec.ts` am Ende dieser Aufgabe rot") und wie der unmittelbare Vorgänger 260911-fh9 es bereits dokumentiert hat: sobald `favorites.service.ts`/`settings.service.ts` ihre `Stand`-Spalte änderten (ungebunden → gebunden/gemischt) und `favorites.service.ts`/`widgetInstance` als neue Fundstelle entstand, maß die Prüfung diese drei Fakten sofort — während das Klassifikationsdokument sie erst in Aufgabe 3 nachzieht. Zwei der elf Fälle scheiterten entsprechend (`jede ... Fundstelle ist im Dokument eingetragen` wegen der neuen `widgetInstance`-Zeile, `der eingetragene Stand stimmt ... überein` wegen der beiden Stand-Wechsel). Nicht als Blocker gewertet: (a) exakt die im Plan selbst vorausgesagte Form, (b) berührte keine Datei außerhalb der sechs für Aufgabe 2 erlaubten, (c) Aufgabe 3 folgte im selben Lauf und stellte die Baseline innerhalb von Minuten wieder her (994/994). "Baseline gehalten nach jeder Aufgabe" ist deshalb — wie schon bei 260911-fh9 — als "nach dem vollständigen Plan, mit einem im Plan selbst vorausgesagten Zwischenzustand" zu lesen, nicht als literarische Bedingung jedes einzelnen Aufgaben-``-Blocks für sich. + +## Ergebnis von Prüfung 7 (Fremdschlüssel) — wörtlich + +`favoritelink-fremdschluessel-prueft-am-zeilenschutz-vorbei`: ein gebundenes `create` unter TENANT-A mit `widgetId='widget-b1'` (gehört TENANT-B, unter TENANT-A per gebundenem `widgetInstance.findUnique` unsichtbar: `null`) **GELINGT** (`id=fav-a1-fremdes-widget`) — der Fremdschlüssel prüft am Zeilenschutz VORBEI, dokumentiertes PostgreSQL-Verhalten. Dasselbe `create` mit `widgetId="widget-gibt-es-nicht"` scheitert mit **`PrismaClientKnownRequestError` (code `P2003`)**: `Foreign key constraint violated on the constraint: FavoriteLink_widgetId_fkey`. Ergebnis: der Besitzriegel in Aufgabe 2 war NÖTIG (nicht optional) — ohne ihn wäre der Unterschied zwischen beiden Antworten ein Existenzorakel über Mandantengrenzen gewesen (T-GWH-05). + +## Ergebnis von Prüfung 8 (Konfliktform) — wörtlich + +`smtpconfig-ungebundenes-upsert-auf-unsichtbare-zeile-scheitert-laut`: ungebundenes `prisma.smtpConfig.upsert({ where: { tenantId: 'TENANT-A' }, ... })` (die Form von `saveSmtpConfig`) wirft **`PrismaClientUnknownRequestError`**: `ConnectorError(... PostgresError { code: "42501", message: "new row violates row-level security policy for table \"SmtpConfig\"" ... })` — dieselbe Fehlerklasse wie die 260910-krx-Messung für `DashboardLayout` (NICHT `PrismaClientKnownRequestError`/`P2002`, die Form von `tenders`/`user`). Die Regel weist den Schreibzugriff ab, bevor der Eindeutigkeitsindex überhaupt geprüft wird. + +## Alle sechs Falsifizierungsnachweise — Testname und Meldung wörtlich + +**(a) Aufgabe 2 — `list` probeweise auf den ungebundenen Basisclient zurückgebaut** (`const tenantPrisma = this.prisma as any;`): 3 Fälle rot. +- `list > liefert nur die Zeilen von user-a1 für widget-a1, sortiert nach position, dann title` — `TypeError: Cannot read properties of undefined (reading 'findMany')` +- `list > liefert unter einem FREMDEN Mandanten eine leere Liste, kein Fehler ...` — dieselbe `TypeError` +- `Wachhund je Methode > genau EIN gebundener Klient je Aufruf von list/update/remove/getIconBytes` — `AssertionError: Aufruf erzeugte 0 gebundene Klienten, erwartet genau 1: expected +0 to be 1` + +**(b) Aufgabe 2 — Widget-Besitzriegel in `create` probeweise entfernt.** Der Plan sagte "genau die drei Widget not found-Fälle" voraus — GEMESSEN sind es **4**, weil der Wachhund-Fall zusätzlich rot wird (Abweichung, siehe unten): +- `create > T-GWH-05: widgetId gehört einem ANDEREN Benutzer desselben Mandanten -> NotFoundException "Widget not found", KEIN create, KEINE Icon-Suche` — `AssertionError: promise resolved "{ …(10) }" instead of rejecting` +- `create > T-GWH-05: widgetId gehört einem Widget unter FREMDEM Mandanten -> dieselbe NotFoundException, nennt weder Halter noch Mandant` — dieselbe `AssertionError`-Form +- `create > T-GWH-05: unbekannte widgetId -> dieselbe NotFoundException` — dieselbe Form +- `create > Wachhund: genau EIN gebundener Klient je create-Aufruf, Widget-Prüfung UND Schreibzugriff auf DEMSELBEN Klienten` — `AssertionError: expected [ { tenantId: 't1', …(2) } ] to deeply equal [ { tenantId: 't1', …(2) }, …(1) ]` + +**(c) Aufgabe 2 — `getDecryptedSmtpConfig` probeweise auf den ungebundenen Basisclient verschoben:** 9 Fälle rot, alle mit `TypeError: tenantPrisma.smtpConfig.findUnique is not a function` — betrifft die drei `getDecryptedSmtpConfig`-Fälle, alle vier `testSmtpConfig`-Fälle (ruft intern `getDecryptedSmtpConfig` auf) und beide betroffenen Wachhund-Fälle. + +**(d) Aufgabe 2 — Startpfad probeweise gebunden** (`forTenant(this.prisma, 'falsification-probe').smtpConfig.findFirst()`): 4 Fälle rot, alle mit `TypeError: (0 , forTenant)(...).smtpConfig.findFirst is not a function` — beide Erfolgsfälle, der Leer-Nachbau-Fall und der Null-Klienten-Nachweis. + +**(e) Aufgabe 3 — Bestandsaufnahme-Zeile `favorites.service.ts | favoriteLink` probeweise auf `ungebunden` zurückgesetzt:** `rls-access-inventory.spec.ts > ... > der eingetragene Stand stimmt mit dem im Quelltext gemessenen überein` — `AssertionError: Abweichender Stand (Dokument vs. Quelltext): apps/api/src/favorites/favorites.service.ts::favoriteLink — dokumentiert=ungebunden, gemessen=gebunden`. + +**(f) Aufgabe 3 — Übersichtszeile `settings` probeweise auf `9 | 9` gesetzt:** das herleitende Gate (`grep -qE "^\| settings \| ${SU} \| ${SB} \| ..."` mit den tatsächlich gemessenen `SU=1`/`SB=3`) schlägt fehl — die Zeile `9 | 9` matcht die Anweisung nicht mehr. + +Alle sechs Änderungen wurden unmittelbar nach der Messung zurückgenommen; `diff` gegen den vor der Probe gesicherten Stand bestätigt Identität in jedem Fall. + +## Decisions Made + +- Startpfad-Ledger-Eintrag (#30) **eigenständig**, nicht an WINDOWS #21 angeschlossen: andere Datei, andere Reparatur, andere Verdeckungsform — siehe `key-decisions` oben. +- Widget-Besitzriegel in `create()` gebaut, weil Prüfung 7 (Aufgabe 1) das Gelingen des Fremdschlüssel-Durchgriffs tatsächlich gemessen hat (nicht angenommen). +- `settings.controller.ts` und `favorites.controller.ts`s `extractContext` bleiben unverändert — beide Mandantenquellen sind bereits die richtigen (siehe `key-decisions`). + +## Deviations from Plan + +### Auto-fixed / gemessene Abweichungen (keine Rule-1/2/3-Bugfixes — alles Messungen, die anders ausfielen als die Planungsvermutung) + +**1. Falsifizierungsnachweis (b): 4 statt 3 rote Fälle.** +- **Gefunden während:** Aufgabe 2, TEIL 5. +- **Planungsvermutung:** "genau die drei `Widget not found`-Fälle werden rot". +- **Tatsächliche Messung:** zusätzlich der Wachhund-Fall (`create > Wachhund: ...`), weil er das Bindungsprotokoll auf zwei Einträge (`widgetInstance.findUnique`, `favoriteLink.create`) prüft — ohne den Riegel gibt es nur den zweiten Eintrag. +- **Auswirkung:** keine — die Falsifizierung bestätigt weiterhin, dass der Riegel notwendig ist; die Zahl ist hier korrigiert, nicht die Planungsaussage stillschweigend übernommen. + +**2. (d4)-Nachtrag: `dkv.seed.ts`/`module-registry` ist NICHT "gebunden seit 260910-exd".** +- **Gefunden während:** Aufgabe 3, TEIL 3, Nachtrag unter (d4). +- **Planungstext:** "`dkv.seed.ts`/`module-registry` ist seit 260910-exd gebunden — prüfen und, falls zutreffend, in demselben Nachtrag mit einem Satz nennen." +- **Tatsächliche Messung:** `dkv.seed.ts` ruft `ModuleRegistryService.seedModule()` (`module-registry.service.ts:206`, `this.prisma.module.upsert`) — UNGEBUNDEN, bewusst und unverändert, weil `Module` der plattformweite Modulkatalog ohne `tenantId`-Spalte ist (Befund E). Der Nachtrag in (d4) sagt das ausdrücklich, statt die Planungsvermutung zu übernehmen. + +**3. Zwischenzeitlich rotes `rls-access-inventory.spec.ts` zwischen Aufgabe 2 und Aufgabe 3** — siehe eigener Abschnitt oben ("Tatsächlich gezählte Prüfungs- und Testzahlen"). Vom Plan selbst vorausgesagt, kein Bug. + +--- + +**Total deviations:** 3, alle Messergebnisse (keine Bugfixes, keine Scope-Erweiterung). Kein Rule-1/2/3-Autofix in diesem Lauf nötig. +**Impact on plan:** keiner — der Plan bleibt in Kraft, alle drei Punkte sind Präzisierungen der eigenen Planungsvermutungen anhand der tatsächlichen Messung, wie es der Plan selbst an mehreren Stellen verlangt ("weicht eine Messung ab, gilt die Messung"). + +## Issues Encountered + +Keine. Alle Prüfungen liefen beim ersten Durchlauf durch (`rls-scratch-check.mjs`: 137/137 ohne Nacharbeit). + +## Ledger-Einträge und Entscheidung zu #21 + +Drei neue offene Einträge in `.planning/WINDOWS.md`: + +- **#30** (`apps/api/src/mail/mail.module.ts`, deviation) — Startpfad des Mailmoduls, sechster Fall der Hintergrunddienst-Falle. **Entscheidung: EIGENER Eintrag, NICHT an #21 angeschlossen** — Grund: andere Datei (`mail.module.ts`/`settings.service.ts` statt `dkv-scheduler.service.ts`), andere Reparatur (Transport je Versand aus `getDecryptedSmtpConfig(tenantId)` statt Mehrmandanten-Planung), andere Verdeckungsform (Rückfallkette auf einen falschen, aber vorhandenen Transport statt bloßer Leere mit Protokollzeile). +- **#31** (`apps/web/src/components/dashboard/widgets/favorites-widget.tsx`, deviation) — verschluckte Leere `favorites`, Familie #23/#25/#26/#28. +- **#32** (`apps/web/src/components/settings/smtp-settings-form.tsx`, deviation) — verschluckte Leere `settings`, dieselbe 200-leerer-Rumpf-Kette wie #28. + +Kopfzähler geprüft: `open_count: 14`, `total_count: 32`, Tabellenzeilen = 32, offene Zeilen = 14 — beide stimmen. + +## Endstand der Etappe 2 (aus dem Abschluss-Abschnitt der Kritikschrift, nicht neu gerechnet) + +- **Zwölf Bereichs-/Regel-Läufe** von 260909-ipc bis 260911-gwh. +- **Übersichtstabelle:** 68 ungebundene / 178 gebundene Rohtreffer (Summe 246; zur Erinnerung: der ursprüngliche Kopf des Klassifikationsdokuments nannte 227 Rohtreffer über 59 Paare — die höhere Summe stammt vom neuen, zur Planungszeit noch nicht feststehenden `widgetInstance`-Besitzriegel). +- **Klassen-Verteilung:** 65 (Datei,Modell)-Paare — 33 `muss-mandantengebunden`, 17 `keine-mandantengebundene-tabelle`, 13 `beides`, 2 `bewusst-uebergreifend`. +- **Werkzeug:** 137/137 Prüfungen bestanden (`rls-scratch-check.mjs`). +- **Tests:** 994/994 grün in 62 Dateien. +- **Jeder verbleibende ungebundene Rohtreffer ist einer der in `docs/mandantentrennung-etappe2-fehlerrichtung.md` bzw. `docs/mandantentrennung-zugriffsklassifikation.md` namentlich benannten, bewusst ungebundenen Fälle** — keiner ist übersehen. +- Schalter bleibt AUS (`DATABASE_URL` unverändert auf Rolle `tessera`), Schema/Migrationen/Compose/Umgebungsdateien unangetastet, Erlaubnisliste gegen `46f0e78` gehalten. + +## User Setup Required + +None — keine externe Konfiguration nötig. + +## Next Phase Readiness + +Etappe 2 ist mit diesem Lauf abgeschlossen. Was für Etappe 3 bleibt (siehe Abschluss-Abschnitt der Kritikschrift): + +- Anmeldeweg unter je Mandant eindeutigen Anmeldenamen (`username`/`email`). +- Benutzerdimension der Regeln (mehrere Bereiche kennen sie nicht — `FavoriteLink` eingeschlossen). +- Modulkatalog-Regel für `Module` (Befund E), falls Etappe 3 sie einführt. +- Kennzeichnung der `bewusst-uebergreifend`-Stellen (Systemkontext). +- Mandantenwechsel im Ausschreibungs-Digest. + +Etappe 4 (`rls-preflight.mjs`) muss vor dem Scharfschalten die in den Bereichsabschnitten benannten Vorabprüfungen laufen lassen (Liste im Abschluss-Abschnitt der Kritikschrift) — die Befund-K-Bedingung ist davon jetzt ausgenommen, weil sie erfüllt ist. + +--- +*Phase: quick-260911-gwh* +*Completed: 2026-09-11* + +## Self-Check: PASSED + +All 12 created/modified files confirmed present on disk; all 4 task commits (`88896d3`, `8f2c13a`, `b5f22e2`, `1240932`) confirmed in `git log`. diff --git a/.planning/quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/260911-gwh-VERIFICATION.md b/.planning/quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/260911-gwh-VERIFICATION.md new file mode 100644 index 0000000..bb1bcd9 --- /dev/null +++ b/.planning/quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/260911-gwh-VERIFICATION.md @@ -0,0 +1,121 @@ +--- +phase: quick-260911-gwh +verified: 2026-09-11T14:20:00Z +status: passed +score: 9/9 must-haves verified +covered_files: + - .planning/WINDOWS.md + - .planning/quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/260911-gwh-PLAN.md + - .planning/quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/260911-gwh-SUMMARY.md + - apps/api/scripts/rls-scratch-check.mjs + - apps/api/src/favorites/favorites.controller.ts + - apps/api/src/favorites/favorites.service.spec.ts + - apps/api/src/favorites/favorites.service.ts + - apps/api/src/mail/mail.module.ts + - apps/api/src/settings/settings.service.spec.ts + - apps/api/src/settings/settings.service.ts + - docs/anleitung-entwicklung.md + - docs/mandantentrennung-etappe2-fehlerrichtung.md + - docs/mandantentrennung-zugriffsklassifikation.md +covered_digest: "v1:sha256:2a5afae1c3039a871e737d6548a419ce5db94b270a0023fb53167db516d4b32f" +behavior_unverified: 0 +overrides_applied: 0 +--- + +# Quick 260911-gwh: Etappe 2 der Mandantentrennung, Bereiche favorites/settings — Verification Report + +**Task Goal:** Mandantentrennung Etappe 2, Bereiche `favorites` und `settings` — 7 `favoriteLink`- und 3 `smtpConfig`-Anfragewege binden, den umbenannten Startpfad bewusst ungebunden lassen (sechster Hintergrunddienst-Fall), den gemessenen Widget-Besitzriegel einbauen, beide fehlenden Spec-Dateien anlegen, Befund K schließen, das Klassifikationsdokument auf den Etappe-2-Endstand bringen. +**Verified:** 2026-09-11 +**Status:** passed +**Re-verification:** No — initial verification + +This report independently re-measures every claim in the SUMMARY against the live codebase and a live database container. No claim was accepted on the SUMMARY's word alone. + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +|---|-------|--------|----------| +| 1 | All 7 `favoriteLink` sites bound (5 methods), 3 `smtpConfig` request-path sites bound, exactly 1 `smtpConfig` site (startup path) deliberately unbound | ✓ VERIFIED | `grep -n "favoriteLink\."` → 7 hits, all on `tenantPrisma`. `grep -n "smtpConfig\."` → 3 on `tenantPrisma` (`getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig`), 1 on `this.prisma` (`loadAnySmtpConfigForStartupTransport`, line 244) | +| 2 | `mail.module.ts` calls the new name; old name `getStartupSmtpConfig` no longer exists anywhere in `apps/api/src` | ✓ VERIFIED | `mail.module.ts` calls `settingsService.loadAnySmtpConfigForStartupTransport()`. `grep -rn getStartupSmtpConfig apps/api/src` → 0 hits. Old name only appears in historical phase artifacts (`.planning/phases/07-*`, `.planning/phases/12-*`, untouched history) and as an explicit "(vormals `getStartupSmtpConfig()`)" annotation in the ledger/critique docs — never as a live call | +| 3 | Befund K closed and recorded as closed at (t4), (d4), and in the classification doc | ✓ VERIFIED | `getDecryptedSmtpConfig(tenantId)` runs over `forTenant()` (1 client). (t4) carries `**Nachtrag (260911-gwh):**` confirming the ordering condition is fulfilled (line ~625). (d4) carries the matching `**Nachtrag (260911-gwh):**` (line ~935), plus a correction that `dkv.seed.ts`/`module-registry` is NOT bound (measured, not copied from the plan's suggestion). Classification doc's background-service section states "Befund K ist mit dieser Bindung ERFÜLLT" | +| 4 | Widget-ownership guard in `favorites.create()`, measured necessary via Prüfung 7 | ✓ VERIFIED | Guard exists in `favorites.service.ts` (`tenantPrisma.widgetInstance.findUnique` → `NotFoundException('Widget not found')` on null/foreign owner). Prüfung 7 (`favoritelink-fremdschluessel-prueft-am-zeilenschutz-vorbei`) is committed in `rls-scratch-check.mjs:3986-4046` and reproduced independently against the live `tessera-ctl-db-1` container: a bound `create` with a foreign tenant's `widgetId` **succeeds** (FK bypasses RLS) while a nonexistent `widgetId` throws P2003 — confirming the guard is necessary, not decorative. Reverted the guard live and re-ran the spec: exactly the 4 claimed failures reproduced (3 "Widget not found" cases + 1 watchdog case), then restored (clean `git diff`) | +| 5 | Startup path names both states (today: arbitrary tenant's SMTP; post-cutover: null/silent) and is named so it can't be mistaken for a request-path method | ✓ VERIFIED | `loadAnySmtpConfigForStartupTransport()` doc-comment explicitly states both states, the fallback-chain double-concealment, the asymmetry to `ldap`/`dkv`, and the "own ledger entry, not attached to #21" decision with reason. `mail.module.ts` header comment mirrors this | +| 6 | Three ledger entries #30/#31/#32 exist, open, and match plan rationale | ✓ VERIFIED | `.planning/WINDOWS.md` rows 47-49 confirmed: #30 (mail.module.ts startup path, own entry not attached to #21), #31 (favorites-widget.tsx silent-empty), #32 (smtp-settings-form.tsx silent-empty). All `status: open`. Header counters cross-checked: `open_count=14`/`total_count=32` vs. 32 table rows / 14 open rows — match | +| 7 | Two new spec files use the two-client harness, `nodemailer` is `vi.mock`'d, no real send attempted | ✓ VERIFIED | `favorites.service.spec.ts`: bound/unbound client separation via `__makeBoundClient`, `IconDiscoveryService` fully mocked (`vi.fn`), no network calls. `settings.service.spec.ts`: unbound client offers ONLY `findFirst`, bound client offers ONLY `findUnique`/`upsert`; `nodemailer` is `vi.mock('nodemailer', ...)` with `createTransport` returning stub `verify`/`sendMail`. Reproduced falsification (a): reverting the `create()` guard reproduced the exact claimed 4 test failures | +| 8 | Generated-client measurements committed — 137 total checks, named `favoritelink-*`/`smtpconfig-*` checks, throwaway tables column-checked (10 scalar fields each) | ✓ VERIFIED | Re-ran `rls-scratch-check.mjs` fresh against the live `tessera-ctl-db-1` container (resolved IP freshly: `172.19.0.2`). Output: "Alle 137 Pruefungen bestanden." 8 `favoritelink-*` named checks + 9 `smtpconfig-*` named checks observed, including the two column-coverage checks confirming 10 scalar fields each match `schema.prisma` exactly, and the `SmtpConfig_tenantId_key` unique index presence | +| 9 | Final stage-2 state of the classification document: recomputed sums, six-case heading, closing section numbers match derived measurements | ✓ VERIFIED | Recomputed independently: Übersicht column sums 68 (ungebunden) / 178 (gebunden) — matches Summenzeile exactly. Klassen-Verteilung 33+17+13+2 = 65 — matches. `## Der Hintergrunddienst als Falle — sechs Fälle` heading present; sixth case (`mail.module.ts`/`loadAnySmtpConfigForStartupTransport`) documented with Befund-K-erfüllt statement. `## Etappe 2 — Abschluss` closing section cites 68/178, 65 Paare, 12 runs, matching the same derived numbers. `rls-access-inventory.spec.ts` (11/11 tests) independently re-run and green, confirming the doc-vs-source consistency gate holds | + +**Score:** 9/9 truths verified (0 present, behavior-unverified) + +### Required Artifacts + +| Artifact | Expected | Status | Details | +|----------|----------|--------|---------| +| `apps/api/scripts/rls-scratch-check.mjs` | `runFavoritesAreaChecks` (≥7 named checks), `runSettingsAreaChecks`, positioned after `runAuthAreaChecks` | ✓ VERIFIED | 8 + 9 = 17 new named checks confirmed by live re-run; 137/137 total | +| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | `## Bereich favorites`, `## Bereich settings`, `## Etappe 2 — Abschluss`, Nachträge at (t4)/(d4) | ✓ VERIFIED | All sections present and content-checked above | +| `apps/api/src/favorites/favorites.service.ts` | 5 methods, 1 client each, widget-ownership guard in `create` | ✓ VERIFIED | Confirmed by direct read; 7 bound `favoriteLink` + 1 bound `widgetInstance` accesses | +| `apps/api/src/favorites/favorites.service.spec.ts` | NEW, two-client harness, icon service mocked, watchdog, edge cases | ✓ VERIFIED | 23 cases, all green in full suite run | +| `apps/api/src/favorites/favorites.controller.ts` | passes `tenantId` from `extractContext` to all 5 service methods | ✓ VERIFIED | Direct read confirms all 5 call sites pass `tenantId` | +| `apps/api/src/settings/settings.service.ts` | 3 methods bound, startup path renamed with header comment | ✓ VERIFIED | Direct read confirms | +| `apps/api/src/settings/settings.service.spec.ts` | NEW, two-client harness with boundary (unbound only `findFirst`, bound only `findUnique`/`upsert`), nodemailer/CryptoService mocked, null-client proof for startup path | ✓ VERIFIED | 20 cases, all green; `forTenant` call-count assertions confirm boundary | +| `apps/api/src/mail/mail.module.ts` | calls renamed startup path, comment names both states | ✓ VERIFIED | Direct read confirms | +| `docs/mandantentrennung-zugriffsklassifikation.md` | overview rows, Summenzeile, Bestandsaufnahme, Klassen-Verteilung, six-case section, "was diese Etappe nicht entscheidet" | ✓ VERIFIED | All recomputed and matched independently | +| `docs/anleitung-entwicklung.md` | paragraph updated to 23 RLS tables / 3 migrations, `FavoriteLink` no longer named as rule-less | ✓ VERIFIED | Confirmed: 4+3+16=23 tables independently recounted from the three migration files | +| `.planning/WINDOWS.md` | 3 new open entries via `gsd-tools windows append` | ✓ VERIFIED | #30/#31/#32 present, open, header counters consistent | + +### Key Link Verification + +| From | To | Via | Status | Details | +|------|----|-----|--------|---------| +| `tender-mail.service.ts` / `dkv-mail.service.ts` | `settingsService.getDecryptedSmtpConfig(tenantId)` | direct call | ✓ WIRED | Method now runs over `forTenant()`; Befund K closed | +| `mail.module.ts` `useFactory` | `settingsService.loadAnySmtpConfigForStartupTransport()` | direct call, startup only | ✓ WIRED | Confirmed call site and naming; fallback chain (env vars → localhost:1025) confirmed unchanged | +| `FavoriteLink.widgetId` → `WidgetInstance.id` | app-level ownership check | `tenantPrisma.widgetInstance.findUnique` in `create()` | ✓ WIRED | Live-measured: FK bypasses RLS (Prüfung 7); guard closes the existence-oracle gap | +| `favorites.controller.ts` `extractContext` | `dashboard.controller.ts` (same tenant source) | textual identity of extraction logic | ✓ WIRED | Confirmed identical `req.tenantId ?? req.user?.tenantId` pattern | +| `settings.controller.ts` | `req.tenantId` (unchanged) | direct read | ✓ WIRED | Controller correctly left unchanged per D-10 rationale | + +### Behavioral Spot-Checks / Probe Execution + +| Behavior | Command | Result | Status | +|----------|---------|--------|--------| +| Full test suite | `npm --prefix apps/api run test` | 994/994 passed, 62 files | ✓ PASS | +| Type-check | `npm --prefix apps/api run type-check` | exit 0 | ✓ PASS | +| `rls-access-inventory.spec.ts` (doc-vs-source consistency) | `npx vitest run src/prisma/rls-access-inventory.spec.ts` | 11/11 passed | ✓ PASS | +| Generated-client tool, live re-run against DB container | `TESSERA_SCRATCH_ADMIN_URL=... node apps/api/scripts/rls-scratch-check.mjs` | "Alle 137 Pruefungen bestanden." | ✓ PASS | +| Falsification reproduction: widget-ownership guard removed | manual revert + `npx vitest run src/favorites/favorites.service.spec.ts` | 4 failures (matches claimed deviation note), restored cleanly | ✓ PASS | +| `this.prisma.` raw count across `apps/api/src` | `grep -rn "this\.prisma\.[a-zA-Z]*" apps/api/src --include="*.ts" \| grep -v spec \| wc -l` | 68 | ✓ PASS (matches Summenzeile) | +| Class-distribution sums | recomputed from table rows | 33+17+13+2 = 65; 68+178=246 raw hits | ✓ PASS | + +### Anti-Patterns Found + +None. No `TBD`, `FIXME`, `XXX`, `TODO`, `HACK`, or `PLACEHOLDER` markers found in any modified file. No empty stub implementations. No hardcoded empty data flowing to render paths. + +### Constraints Held + +- Allow-list scope against `46f0e78`: `git diff --name-only 46f0e78` lists exactly the 12 files declared in `files_modified` (plus the PLAN.md itself, committed separately, and WINDOWS.md) — no unexpected files. +- No schema/migration/compose/environment file appears in the diff. +- Switch remains OFF (`DATABASE_URL` role `tessera`/`BYPASSRLS` unchanged — no env file touched). +- No Active Directory / LDAP code changed (only a comment reference in a doc-string). +- No multi-tenant mail transport built — startup path remains deliberately unbound, only renamed and documented. + +### Requirements Coverage + +| Requirement | Source Plan | Description | Status | Evidence | +|-------------|-------------|-------------|--------|----------| +| WINDOWS-18 | 260911-gwh-PLAN.md | Etappe 2 fully documented at endstate | ✓ SATISFIED | Classification doc, critique doc, anleitung, ledger all recomputed and matched | +| ETAPPE-2-FAVORITES | 260911-gwh-PLAN.md | favorites.service.ts fully bound with ownership guard | ✓ SATISFIED | Verified directly | +| ETAPPE-2-SETTINGS | 260911-gwh-PLAN.md | settings.service.ts bound, startup path renamed, Befund K closed | ✓ SATISFIED | Verified directly | + +### Human Verification Required + +None. All must-haves were verifiable programmatically and against a live database container. + +### Gaps Summary + +No gaps found. Every must-have in the plan's frontmatter was independently re-measured against the current codebase and/or a live database container — not accepted from the SUMMARY's narrative. The one place the SUMMARY itself documents a deviation from its own prediction (4 vs. 3 falsification failures) was independently reproduced and confirmed accurate. + +--- + +_Verified: 2026-09-11_ +_Verifier: Claude (gsd-verifier)_