diff --git a/.planning/quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/260910-krx-PLAN.md b/.planning/quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/260910-krx-PLAN.md
new file mode 100644
index 0000000..6e3dc91
--- /dev/null
+++ b/.planning/quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/260910-krx-PLAN.md
@@ -0,0 +1,739 @@
+---
+phase: quick-260910-krx
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+autonomous: true
+requirements: [WINDOWS-18, ETAPPE-2-DASHBOARD]
+
+files_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
+
+estimate:
+ tokens: 185000
+ raw_tokens: 185000
+ tasks: 3
+ confidence: low
+
+must_haves:
+ truths:
+ - "Alle dreizehn Datenbankzugriffe dieses Bereichs sind entschieden: zwoelf laufen gebunden unter dem woertlichen Namen `tenantPrisma`, einer (der plattformweite Modulkatalog) bleibt begruendet ungebunden. Keiner bleibt unentschieden, und keine Methode erzeugt mehr als einen gebundenen Klienten."
+ - "Lesen und Schreiben derselben Tabelle sind nirgends auf gebunden und ungebunden aufgeteilt. `getLayout` und `saveLayout` sind GEMEINSAM gebunden, und die drei Besitzpruefungen (Widget aendern, Widget entfernen, Suchmaschine entfernen) fuehren ihre beiden Abfragen ueber DENSELBEN gebundenen Klienten. Das ist als Testfall festgenagelt, nicht behauptet."
+ - "Die umgekehrte Fehlerrichtung dieses Bereichs ist an den ECHTEN, ausgelieferten Regeln gemessen — und zwar an dem Stand, den die Datenbank NACH der Migration 20260910120000 hat, nicht an dem, den aeltere Migrationen beschreiben. Je umgestelltem Pfad steht das konkrete Signal, an dem man ein zu kleines Ergebnis erkennen wuerde."
+ - "Die beweisvernichtende Schleife dieses Bereichs ist ausdruecklich benannt: ein zu kleines Leseergebnis zeigt kein Fehlerbild, sondern ein LEERES Dashboard beziehungsweise die Vorgabeanordnung. Der Nutzer haelt das fuer einen Fehler des Widget-Systems oder fuer verlorene Einstellungen, baut seine Anordnung neu auf — und die Anwendung schreibt diesen Neuaufbau beim Verlassen des Bearbeitungsmodus von selbst zurueck, womit die einzige Aufzeichnung der urspruenglichen Anordnung ueberschrieben ist. Festgehalten in der Kritikschrift UND als offener Eintrag im Broken-Windows-Register mit der konkreten Vorabpruefung fuer Etappe 4."
+ - "Die widerlegte Praemisse zu `SearchProvider` (WINDOWS #19, geschlossen in 260910-jab) ist in DIESEM Durchlauf eigenstaendig nachgeprueft, nicht aus dem Vorlauf abgeschrieben — mit einer Suche, deren Reichweite benannt ist, damit die Aussage widerlegbar bleibt. Faende sich doch ein Schreibweg fuer eine mandantenlose Zeile, waere die strenge Regel ein offenes Loch und der Befund wuerde umgedreht statt uebergangen."
+ - "Die Entlastung, die dieser Bereich geerbt hat, ist nachgeprueft und NICHT ein zweites Mal repariert: die Modul-Zugriffsaufloesung ist bereits im Dienst gebunden (260910-exd), der Widget-Modulfilter ist damit ohne Zutun dieses Bereichs gebunden, und keine Datei unter `apps/api/src/module-registry` wird angefasst."
+ - "Der plattformweite Modulkatalog bleibt ungebunden, und MESSUNG und BEDINGUNG bleiben getrennt: die Tabelle traegt heute ueberhaupt keinen Zeilenschutz, eine Bindung waere heute WIRKUNGSLOS und nicht katastrophal; katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt. Die Messung dafuer wird beim Namen aus dem bestehenden Werkzeuglauf uebernommen, nicht neu behauptet."
+ - "Die Testlage steht VOR der Umstellung: die vorhandene Testdatei deckt heute ausschliesslich `getWidgets` ab und kennt kein Bindungshilfsmittel. Sie bekommt den Zwei-Klienten-Nachweis und Faelle fuer die Pfade, die heute ueberhaupt keinen Test haben. Ein vergessener Bindungsaufruf wird dadurch rot, statt aus einem anderen Grund zu scheitern."
+ - "Die Regeln dieses Bereichs kennen KEINE Benutzerdimension — zwei Nutzer desselben Mandanten sind fuereinander auf Datenbankebene vollstaendig sichtbar. Das ist gemessen, und die anwendungsseitigen Besitzpruefungen ueber die Benutzerkennung bleiben deshalb unveraendert bestehen; sie werden durch die Bindung ERGAENZT, nie ersetzt."
+ - "Baseline gehalten am Ende JEDER Aufgabe, nicht nur am Ende des Plans: mindestens 839 Tests gruen (mindestens 56 Dateien), Typpruefung sauber, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden."
+ - "Der Schalter bleibt AUS: `DATABASE_URL` zeigt weiterhin auf die Rolle `tessera`, Schema und Migrationen sind unveraendert, keine Compose- oder Umgebungsdatei wird angefasst."
+ artifacts:
+ - "apps/api/scripts/rls-scratch-check.mjs — ein neunter Abschnitt `runDashboardAreaChecks` mit dreizehn namentlich benannten Pruefungen gegen die aus den ausgelieferten Migrationen geschnittenen Regeln fuer `DashboardLayout`, `WidgetInstance` und `SearchProvider`"
+ - "docs/mandantentrennung-etappe2-fehlerrichtung.md — Abschnitt `## Bereich dashboard` mit (w1) Messung, (w2) Signaltabelle, (w3) Leere-als-Abwesenheit samt der beweisvernichtenden Schleife und dem Verhalten der Suchleiste, (w4) bewusst nicht geloest, (w5) bewusst nicht angefasst, plus Nachtrag im Abschnitt `## Bereich module-registry`"
+ - "apps/api/src/dashboard/dashboard.service.ts — die zwoelf mandantengebundenen Zugriffe gebunden, der eine Katalogzugriff begruendet ungebunden"
+ - "apps/api/src/dashboard/dashboard.controller.ts — die fuenf Handler, die den bereits aufgeloesten Mandanten heute verwerfen, reichen ihn durch"
+ - "apps/api/src/dashboard/dashboard.service.spec.ts — Zwei-Klienten-Nachweis ueber `__makeBoundClient`, alle acht bestehenden Faelle erhalten, neue Faelle fuer die heute ungetesteten Pfade"
+ - "docs/mandantentrennung-zugriffsklassifikation.md — vier Bestandsaufnahme-Zeilen, Uebersichtszeile, Summenzeile, Klassen-Verteilung, Abschnitt zur Hintergrunddienst-Falle, Abschnitt `Was diese Etappe NICHT entscheidet`"
+ - ".planning/WINDOWS.md — offener Eintrag zur beweisvernichtenden Schleife, in Tabelle UND JSON-Block"
+ key_links:
+ - "`getLayout` und `saveLayout` greifen ueber einen PLATTFORMWEIT eindeutigen Schluessel (`userId`, ohne Mandantenanteil) auf dieselbe Zeile zu. Wird eine der beiden gebunden und die andere nicht, wird aus einem zu kleinen Leseergebnis ein UEBERSCHREIBEN der tatsaechlichen Anordnung — dieselbe Familie wie WINDOWS #22 im Bereich `user`."
+ - "Der Controller loest den Mandanten bereits in `extractContext` auf, fuenf Handler verwerfen ihn nur. Die Umstellung ist ein Durchreichen, keine neue Vertrauensquelle — die Mandantenkennung kommt weiterhin ausschliesslich aus dem Sitzungsnachweis."
+ - "`dashboard.service.ts` ruft die bereits gebundene Modul-Zugriffsaufloesung auf. Der Widget-Modulfilter ist dadurch gebunden, ohne dass eine Datei dieses Bereichs ihn bindet; eine zweite Bindung hier erzeugte zwei Klienten fuer dieselbe Aufloesung."
+ - "Die drei Regeln dieses Bereichs lauten schlicht `\"tenantId\" = current_tenant_id()` — ohne Benutzerdimension. Die Besitzpruefungen ueber die Benutzerkennung sind und bleiben der einzige Schutz gegen Quer-Lesen und Quer-Loeschen zwischen Nutzern DESSELBEN Mandanten."
+---
+
+
+Etappe 2 der Mandantentrennung, achter Bereich: `dashboard`. Die dreizehn
+Datenbankzugriffe des einzigen Dienstes dieses Bereichs werden dort auf
+`forTenant()` umgestellt, wo sie auf Rechnung genau eines Mandanten laufen, und
+dort begruendet ungebunden gelassen, wo sie den plattformweiten Modulkatalog
+betreffen.
+
+Zweck: dieser Bereich haelt, was ein Nutzer sich selbst eingerichtet hat — die
+Anordnung seiner Widgets und seine eigenen Suchmaschinen. Die umgekehrte
+Fehlerrichtung sieht hier nicht wie ein Fehler aus, sondern wie ein
+Zuruecksetzen: nach dem Scharfschalten meldet eine ungebunden gebliebene
+Abfrage keinen Fehler, sondern liefert ein leeres Dashboard beziehungsweise die
+Vorgabeanordnung. Wer sein sorgfaeltig eingerichtetes Dashboard leer vorfindet,
+vermutet einen Fehler im Widget-System oder verlorene Einstellungen, baut es
+neu auf — und die Anwendung schreibt diesen Neuaufbau beim Verlassen des
+Bearbeitungsmodus von selbst zurueck. Damit ist die einzige Aufzeichnung der
+urspruenglichen Anordnung ueberschrieben, bevor irgendjemand die Ursache
+untersuchen konnte. Das ist die beweisvernichtende Auspraegung der umgekehrten
+Fehlerrichtung, und sie ist der eigentliche Grund, warum dieser Bereich seine
+Kritikschrift VOR der Umstellung braucht.
+
+Ergebnis: zwoelf gebundene Zugriffe, ein begruendet ungebundener Katalogzugriff,
+eine gemessene Kritikschrift, eine Testlage, die einen vergessenen
+Bindungsaufruf rot macht, und zwei Dokumente, die am Ende nachweislich mit dem
+Quelltext uebereinstimmen.
+
+**Der Schalter bleibt AUS.** `DATABASE_URL` zeigt weiterhin auf die Rolle
+`tessera` mit `BYPASSRLS`. Das Scharfschalten ist Etappe 4 und findet hier
+NICHT statt. Schema und Migrationen werden NICHT angefasst — die Regelarbeit
+ist einen Commit zuvor abgeschlossen und verifiziert worden.
+
+
+
+@~/.claude/gsd-core/workflows/execute-plan.md
+@~/.claude/gsd-core/templates/summary.md
+
+
+
+@.planning/STATE.md
+@docs/mandantentrennung-zugriffsklassifikation.md
+@docs/mandantentrennung-etappe2-fehlerrichtung.md
+@apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql
+@apps/api/src/prisma/prisma-tenant.extension.ts
+@apps/api/src/prisma/rls-access-inventory.spec.ts
+@apps/api/scripts/rls-scratch-check.mjs
+@apps/api/src/dashboard/dashboard.service.ts
+@apps/api/src/dashboard/dashboard.service.spec.ts
+@apps/api/src/dashboard/dashboard.controller.ts
+@apps/api/src/dashboard/widget-module-map.ts
+@apps/api/src/module-registry/module-access.service.ts
+@apps/api/src/module-registry/module-access.service.spec.ts
+@apps/api/src/tenders/tender-rss-feed.service.ts
+@.planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-PLAN.md
+
+
+
+
+Alle Zahlen unten sind zur Planungszeit am 2026-09-10 gegen HEAD `f205120`
+GEMESSEN, mit der jeweils angegebenen Anweisung. Sie leiten die Untersuchung,
+sie sind KEINE Bearbeitungsvollmacht — jede Datei wird vor jeder Aenderung
+erneut gelesen, und jede Zahl wird zur Ausfuehrungszeit erneut gemessen.
+
+**Befund A — die Zahl haelt, und der Bereich ist eine einzige Datei.** Gemessen
+mit `grep -rnoE "this\.prisma\.[a-zA-Z]+" apps/api/src/dashboard --include=*.ts | grep -v spec`:
+dreizehn Treffer, alle in `dashboard.service.ts`. Aufteilung nach Modell:
+`dashboardLayout` zwei (Zeilen 69, 85), `widgetInstance` sieben (111, 157, 176,
+192, 203, 213), `module` einer (134), `searchProvider` vier (225, 241, 258,
+268). `dashboard.controller.ts`, `dashboard.module.ts` und
+`widget-module-map.ts` halten null Zugriffe. Nach `dkv` (21), `user` (17) und
+`module-registry` (17) der vierte Bereich in Folge, in dem beim Hineinschauen
+nichts schrumpft. Auch die vier (Datei, Modell)-Paare der Bestandsaufnahme
+stimmen mit dem Quelltext ueberein — anders als im Bereich `user` (eine
+sachlich falsche Zeile) und im Bereich `dkv` (ein ganz fehlendes Paar) ist hier
+keine Korrektur der PAARE noetig. Die BEGRUENDUNGEN von drei der vier Zeilen
+sind trotzdem nachzuziehen, siehe Befunde E und F.
+
+Zaehlkontrolle: 2 + 7 + 1 + 4 = 14, nicht 13 — die Liste oben nennt fuer
+`widgetInstance` sechs Zeilennummern fuer sieben Treffer, weil in einer Zeile
+zwei Vorkommen liegen koennen. Genau deshalb ist die ZAHL zur Ausfuehrungszeit
+neu zu messen und nicht aus dieser Aufzaehlung abzuschreiben. Erwartet wird
+dreizehn; weicht die Messung ab, ist die Abweichung im SUMMARY zu benennen,
+nicht stillschweigend zu uebernehmen.
+
+**Befund B — keine Transaktion, kein Roh-SQL in diesem Bereich.** Gemessen mit
+`grep -rn '\$transaction(\|\$queryRaw\|\$executeRaw' apps/api/src/dashboard --include=*.ts`:
+null Treffer. Der im Kopfkommentar von `prisma-tenant.extension.ts`
+ausdruecklich verlangte erneute Test vor jedem neuen `forTenant()`-Fall mit
+eigener Transaktion ist damit fuer diesen Bereich beantwortet: es faellt kein
+neuer Fall an, `withTenantTransaction()` wird hier nicht gebraucht und nicht
+eingefuehrt.
+
+**Befund C — die Testdatei existiert, deckt aber nur EINEN von neun Pfaden ab,
+und sie hat die `groups`/`tenders`-Form.** `dashboard.service.spec.ts` (215
+Zeilen, acht Faelle) testet ausschliesslich `getWidgets` und den
+Modulfilter aus 15-05. Ihr handgerollter Prisma-Nachbau (`makeFakePrisma`)
+kennt genau zwei Modelle (`widgetInstance`, `module`) und KEINE Attrappe fuer
+das Bindungshilfsmittel. Nach der Umstellung riefe das echte `forTenant()` eine
+`$extends`-Methode auf einem schlichten Objekt auf, das keine hat — die Tests
+wuerden scheitern, aber aus dem falschen Grund, und ein vergessener
+Bindungsaufruf waere von einem vorhandenen nicht zu unterscheiden. Acht der
+neun Dienstmethoden (`getLayout`, `saveLayout`, `addWidget`,
+`updateWidgetConfig`, `removeWidget`, `getSearchProviders`,
+`addSearchProvider`, `removeSearchProvider`) haben heute UEBERHAUPT keinen
+Test. Die Zahl acht ist zur Ausfuehrungszeit nachzuzaehlen.
+
+**Befund D — die Besitzpruefung ist ECHT, anders als im Bereich `ldap`.** Die
+Form ist dieselbe, die in diesem Vorhaben die erste echte Luecke erzeugt hat:
+`findUnique` auf die Kennung, dann `delete`/`update` auf dieselbe Kennung. Aber
+zwischen beiden steht hier tatsaechlich eine Pruefung, und zwar auf der
+BENUTZER-Dimension, die die Mandanten-Dimension einschliesst: `removeWidget`
+und `updateWidgetConfig` pruefen `widget.userId !== userId`,
+`removeSearchProvider` prueft `provider.userId !== userId`, jeweils mit einer
+Benutzerkennung aus dem Sitzungsnachweis, und werfen sonst `NotFoundException`.
+Ein Administrator des Mandanten A kann ein Widget des Mandanten B daher heute
+NICHT entfernen — er muesste es besitzen. Die Vorgabe-Suchmaschinen tragen
+`userId = null` und fallen deshalb ebenfalls in den Nicht-gefunden-Zweig, wie
+es der Kopfkommentar an dieser Methode behauptet. **Diese Entlastung ist zur
+Ausfuehrungszeit erneut zu pruefen, bevor sie im SUMMARY behauptet wird** — sie
+ist der Kern der Sicherheitsaussage dieses Bereichs.
+
+Was daraus NICHT folgt: die beiden Abfragen jeder Besitzpruefung muessen nach
+der Umstellung ueber DENSELBEN gebundenen Klienten laufen. Waere die
+Lesehaelfte gebunden und die Schreibhaelfte nicht, ginge die Pruefung auf einer
+Zeile auf, die der anschliessende Schreibvorgang gar nicht mehr sieht — und
+umgekehrt.
+
+**Befund E — die Entlastung, die dieser Bereich geerbt hat.**
+`apps/api/src/module-registry/module-access.service.ts` bindet
+`getAccessibleModuleIds` seit 260910-exd (`const tenantPrisma = forTenant(this.prisma, tenantId)`),
+und `dashboard.service.ts` ruft genau diese Aufloesung fuer den
+Widget-Modulfilter auf. Der Filter ist damit bereits gebunden, ohne dass eine
+Datei dieses Bereichs angefasst worden waere. Dieser Plan darf ihn NICHT ein
+zweites Mal binden. Zusatzbefund, ebenfalls gemessen:
+`apps/api/src/dashboard/widget-module-map.ts` fuehrt `WIDGET_MODULE_MAP` als
+leeres Objekt, weshalb `getWidgets` heute vor der Aufloesung zurueckkehrt und
+sie im Normalbetrieb gar nicht erreicht — der Pfad ruht, er fehlt nicht.
+
+**Befund F — die widerlegte Praemisse zu `SearchProvider` haelt der eigenen
+Nachpruefung stand.** Die Migration 20260910120000 hat `SearchProvider`
+ausdruecklich UNVERAENDERT gelassen, mit der Begruendung, es gebe keinen
+Codeweg, der eine mandantenlose Zeile erzeugt. Zur Planungszeit eigenstaendig
+nachgeprueft mit einer Suche ueber `apps/`, `packages/` und `prisma/` nach
+`searchProvider`/`SearchProvider` in `*.ts`, `*.mjs`, `*.js`, `*.sql`, `*.json`
+(ohne `node_modules`): der einzige Schreibweg ist `dashboard.service.ts:241`
+(`create`) mit `tenantId: string` als PFLICHTPARAMETER der aufrufenden Methode,
+es existiert keine Seed-Datei (`apps/api/prisma/` enthaelt nur `migrations` und
+`schema.prisma`), kein Skript und kein weiterer Schreibweg. Die drei
+Vorgabe-Suchmaschinen sind eine TypeScript-Konstante im selben Dienst
+(Entscheidung 05-02) mit fest verdrahteten Kennungen `google`/`bing`/`ddg`, die
+in keiner Datenbankzeile vorkommen.
+
+**Grenze dieser Suche, ausdruecklich benannt (Fehler 6 dieses Vorhabens):**
+sie findet keinen Schreibweg, der das Modell ueber einen dynamisch gebildeten
+Namen anspricht, und sie deckt keine manuelle Datenbankaenderung ab. Die
+Aussage lautet deshalb nicht "es kann keine mandantenlose Zeile geben", sondern
+"kein Anwendungspfad erzeugt eine". Aufgabe 1 haelt beide Haelften fest: die
+Codeaussage samt Reichweite UND die Datenbankmessung, dass ein gebundenes
+Einfuegen ohne Mandant von der unveraenderten Regel abgewiesen wird — die
+strenge Regel verteidigt die widerlegte Praemisse also zusaetzlich.
+
+**Befund G — die drei Regeln dieses Bereichs kennen keine Benutzerdimension.**
+Gemessen in `20260909140000_rls_remaining_tenant_tables/migration.sql`: die
+Policies auf `DashboardLayout` (Zeile 67), `SearchProvider` (99) und
+`WidgetInstance` (155) lauten alle drei wortgleich
+`USING ("tenantId" = current_tenant_id())`. Zwei Nutzer DESSELBEN Mandanten
+sind fuereinander auf Datenbankebene vollstaendig sichtbar — wortgleich derselbe
+Fall wie `tendersavedsearch-fremder-nutzer-desselben-mandanten-sichtbar` im
+Bereich `tenders`. Die anwendungsseitigen Besitzpruefungen aus Befund D bleiben
+deshalb der EINZIGE Schutz gegen Quer-Lesen und Quer-Loeschen zwischen Nutzern
+und werden bei der Umstellung ausdruecklich NICHT entfernt.
+
+**Befund H — der Modulkatalog traegt weiterhin ueberhaupt keinen Zeilenschutz.**
+Gemessen mit `grep -n 'ENABLE ROW LEVEL SECURITY' apps/api/prisma/migrations/*/migration.sql`:
+`"Module"` ist nicht darunter, und das Modell traegt im Schema kein `tenantId`.
+Das ist exakt Befund E aus 260910-exd, dort bereits als Werkzeugpruefung
+`module-tabelle-traegt-keinen-zeilenschutz` hinterlegt. Dieser Plan MISST das
+nicht neu — er beruft sich beim Namen auf die bestehende Pruefung und
+uebernimmt die dort etablierte Trennung: eine Bindung waere HEUTE wirkungslos,
+nicht katastrophal; katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle
+eine Regel gibt. Die Bestandsaufnahme-Zeile fuer `dashboard.service.ts`/`module`
+begruendet die Nichtbindung heute mit "Modulkatalog ist plattformweit, kein
+`tenantId`" — richtig, aber unvollstaendig; sie nennt die BEDINGUNG nicht.
+
+**Befund I — die beweisvernichtende Schleife, gemessen im Frontend statt aus
+dem Backend geschlossen.** Drei Stellen greifen ineinander:
+
+1. `dashboard.service.ts`, `getLayout`: `if (!record) return { lg: [], md: [], sm: [], xs: [], xxs: [] };`
+ — kein Datensatz heisst hier nicht "Fehler", sondern "Vorgabeanordnung".
+2. `apps/web/src/lib/stores/dashboard-store.ts`, `loadDashboard`: setzt
+ `layouts` und `widgets` genau auf das, was die beiden Abfragen liefern. Ein
+ zu kleines Ergebnis erzeugt keinen Fehlerzustand — der `catch`-Zweig
+ (`error: 'Failed to load dashboard'`) feuert nur bei einem Netzwerk- oder
+ Statusfehler, nicht bei einer erfolgreichen, leeren Antwort.
+3. Derselbe Store, `setEditMode(false)`: `if (prev && !mode && get().isDirty) { get().saveLayout(); }`
+ — der Neuaufbau wird beim blossen VERLASSEN des Bearbeitungsmodus
+ automatisch zurueckgeschrieben, ohne dass jemand auf "Speichern" klickt.
+
+Die Folge, wenn nach dem Scharfschalten ein Lesepfad zu wenig liefert: leeres
+Dashboard, Nutzer baut neu auf, `addWidget` legt echte neue
+`WidgetInstance`-Zeilen an (es gibt keine Eindeutigkeitsbedingung ueber
+`(userId, widgetType)`, Dubletten haeufen sich also an), und das automatische
+Zurueckschreiben ueberschreibt die `layouts`-Spalte der urspruenglichen Zeile.
+Die urspruengliche Anordnung ist danach nicht mehr rekonstruierbar. **Dieser
+Ablauf ist zur Ausfuehrungszeit an den beiden Web-Dateien erneut zu pruefen,
+bevor er in der Kritikschrift behauptet wird** — dieser Plan aendert am
+Frontend NICHTS, er beschreibt es nur.
+
+**Befund J — was mit der Suchleiste passiert, gemessen.**
+`apps/web/src/components/dashboard/widgets/search-widget.tsx` uebernimmt die
+Antwort nur, wenn sie nicht leer ist (`if (!cancelled && data.length > 0)`),
+und faellt sonst auf drei fest verdrahtete Vorgaben zurueck. Weil
+`getSearchProviders` die drei Vorgaben IMMER voranstellt, ist die Antwort nie
+leer — der Rueckfallzweig feuert nie, und die eigenen Suchmaschinen des
+Nutzers verschwinden schlicht aus der Auswahlliste, waehrend die Leiste
+weiterhin funktioniert. Zusaetzlich waehlt `handleSearch` bei unbekannter
+Auswahl `providers[0]`, also Google: eine Suchanfrage, die fuer eine interne
+Suchmaschine gedacht war, ginge dann an eine externe. Sichtbar bleibt das
+insofern, als die Auswahlliste den ersten Eintrag anzeigt — **wie sichtbar
+genau, ist zur Ausfuehrungszeit an der Datei nachzusehen und praezise zu
+formulieren, statt "still" zu behaupten.**
+
+**Befund K — die plattformweite Eindeutigkeit von `DashboardLayout.userId`.**
+Im Schema traegt `userId` ein `@unique` OHNE Mandantenanteil. Damit ist
+strukturell dieselbe Kette moeglich wie im Bereich `user` (WINDOWS #22):
+unsichtbare Zeile — falsches "frei" — harter Eindeutigkeitsfehler. Gemessen
+wurde zusaetzlich, ob diese Kette ueber einen Anwendungspfad ueberhaupt
+erreichbar ist: `apps/api/src/user/dto/update-user.dto.ts` kennt kein
+`tenantId`, und keine Stelle schreibt `tenantId` auf einen bestehenden
+Benutzer fort — die Mandantenkennung eines Benutzers aendert sich nach der
+Anlage nicht. Die Kette ist damit heute nur ueber eine Aenderung an der
+Datenbank von Hand erreichbar. Der `update`-Zweig von `saveLayout` schreibt
+`tenantId` allerdings NICHT mit, weshalb eine einmal abweichende
+Mandantenkennung dauerhaft stehen bliebe. Aufgabe 1 misst, WAS in diesem Fall
+tatsaechlich passiert; Aufgabe 2 behandelt das Ergebnis. **Ein Schemawechsel
+ist ausdruecklich NICHT Teil dieses Plans** — er waere die ehrliche Reparatur
+und gehoert als Produktentscheidung nach Etappe 3, wie #22 es fuer denselben
+Fall bereits vormerkt.
+
+**Befund L — eine Fremdaussage ueber diesen Bereich, die nicht ganz stimmt.**
+`apps/api/src/groups/groups.service.ts` (Kopfkommentar, um Zeile 24) begruendet
+die dortige Regel "jede Lookup-Query mit einer Gruppen-ID filtert zusaetzlich
+auf tenantId" mit dem Zusatz "nach dem Ownership-Check-Muster aus
+DashboardService.removeWidget". Das trifft in zwei Punkten nicht zu:
+`removeWidget` filtert NICHT zusaetzlich in der Abfrage, sondern vergleicht
+nach dem Laden im JavaScript, und der Vergleich laeuft ueber die BENUTZER-,
+nicht die Mandantenkennung. Die Aussage in
+`apps/api/src/groups/module-grants.service.ts` (um Zeile 41, "betreffen nur
+direktes Eigentum, nicht eine zweite Mandantengrenze ueber eine Relation") ist
+dagegen zutreffend. **Der Bereich `groups` wird von diesem Plan NICHT
+angefasst** — die Ungenauigkeit wird in (w5) festgehalten, nicht behoben. Zur
+Ausfuehrungszeit erneut gegenzulesen, bevor sie im Dokument behauptet wird.
+
+
+
+
+
+
+ Aufgabe 1: Die Fehlerrichtung fuer diesen Bereich MESSEN und aufschreiben — an den Regeln, wie sie nach der Migration 20260910120000 stehen
+ Der lokale Datenbank-Container `tessera-ctl-db-1` laeuft; `docker inspect tessera-ctl-db-1` liefert eine Adresse. Ohne ihn kann das Wegwerf-Werkzeug nichts messen und die Aufgabe ist zu stoppen, nicht zu schaetzen.
+ apps/api/scripts/rls-scratch-check.mjs, docs/mandantentrennung-etappe2-fehlerrichtung.md
+ apps/api/scripts/rls-scratch-check.mjs, apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql, apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql, apps/api/src/dashboard/dashboard.service.ts, apps/api/src/dashboard/dashboard.controller.ts, apps/web/src/lib/stores/dashboard-store.ts, apps/web/src/components/dashboard/widgets/search-widget.tsx, apps/api/prisma/schema.prisma, docs/mandantentrennung-etappe2-fehlerrichtung.md
+
+TEIL 1 — die Messung. Erweitere `apps/api/scripts/rls-scratch-check.mjs` um
+einen neunten Abschnitt `runDashboardAreaChecks(adminUrl, scratchRoleUrl, results)`
+und rufe ihn in `main()` NACH `runModuleRegistryAreaChecks` und VOR
+`runTransactionShapeMeasurement` auf. Der Abschnitt legt die beiden
+Wegwerf-Tabellen `DashboardLayout` und `WidgetInstance` selbst an und benutzt
+die von `runSearchProviderAreaChecks` bereits angelegte Tabelle `SearchProvider`
+WEITER, statt sie ein zweites Mal anzulegen — halte diese Reihenfolgebedingung
+im Kopfkommentar des neuen Abschnitts fest, so wie `runDkvAreaChecks` es fuer
+seine Bedingung vormacht. Die bestehende Pruefung
+`searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar` und
+ihre Testzeile bleiben UNVERAENDERT bestehen; neue Zeilen bekommen eigene
+Kennungen, damit sie nicht kollidieren.
+
+Alle drei Policies werden mit `extractPolicySql()` WORTGLEICH aus
+`20260909140000_rls_remaining_tenant_tables` geschnitten, nicht im Werkzeug
+nachgetippt. Findet die Extraktion eine der drei nicht, meldet der Abschnitt
+eine FEHLGESCHLAGENE Pruefung und bricht ab, statt mit einer geratenen Regel
+weiterzumessen — genau wie die bestehenden Abschnitte. Die Wegwerf-Tabelle
+`DashboardLayout` bildet die Eindeutigkeitsbedingung des Schemas nach
+(`"userId" text UNIQUE`), weil ohne sie die Messung aus Pruefung 5 unten gar
+nicht stattfinden kann.
+
+Dreizehn namentlich benannte Pruefungen, jede mit einer Belegausgabe, die die
+beobachteten Werte nennt:
+
+1. `dashboardlayout-gebunden-nur-eigener-mandant` — ein gebundener Lesezugriff
+ fuer TENANT-A liefert ausschliesslich A-Zeilen.
+2. `dashboardlayout-ungebunden-null-zeilen` — die tragende Belegzeile: der
+ IDENTISCHE Lesezugriff ohne vorheriges Setzen des Mandantenkontexts liefert
+ null Zeilen, nicht etwa alle vorhandenen.
+3. `dashboardlayout-fremder-nutzer-desselben-mandanten-gebunden-sichtbar` — das
+ GELINGEN ist hier das bestandene Ergebnis: die Regel kennt keine
+ Benutzerdimension, ein zweiter Nutzer desselben Mandanten ist gebunden
+ sichtbar. Die Belegausgabe muss ausdruecklich sagen, dass die
+ anwendungsseitige Pruefung ueber die Benutzerkennung deshalb der einzige
+ Schutz gegen Quer-Lesen bleibt und nicht entfallen darf.
+4. `widgetinstance-ungebunden-null-zeilen` und
+ `widgetinstance-gebunden-nur-eigener-mandant` — dasselbe Paar fuer die
+ Widget-Tabelle.
+5. `dashboardlayout-gebundener-konfliktschreibvorgang-auf-unsichtbare-zeile-scheitert-laut`
+ — MISST, was passiert, wenn ein gebundenes Einfuegen mit
+ Konfliktbehandlung (`INSERT ... ON CONFLICT ("userId") DO UPDATE ...`) auf
+ eine physisch vorhandene, unter dem laufenden Mandanten aber unsichtbare
+ Zeile trifft. Bestanden ist diese Pruefung genau dann, wenn ein HARTER,
+ benannter Fehler zurueckkommt; sie ist NICHT bestanden, wenn der Vorgang
+ still gelingt, eine fremde Zeile aendert oder eine Dublette erzeugt. Die
+ Belegausgabe nennt den beobachteten SQLSTATE und die Meldung woertlich,
+ damit Aufgabe 2 daran anknuepfen kann. Rate das Ergebnis NICHT vorweg — es
+ haengt am Zusammenspiel von Eindeutigkeitsindex und Regel und ist genau
+ deshalb eine Messung.
+6. `widgetinstance-gebundenes-einfuegen-fremder-mandant-abgelehnt` — ein
+ gebundenes Einfuegen mit fremder Mandantenkennung wird abgewiesen.
+7. `widgetinstance-gebundenes-loeschen-fremder-zeile-trifft-keine-zeile` — ein
+ gebundenes Loeschen ueber die Kennung einer fremden Zeile entfernt nichts
+ und meldet keinen Fehler. Das ist die Datenbankseite von Befund D.
+8. `searchprovider-ungebunden-null-zeilen` und
+ `searchprovider-gebunden-nur-eigener-mandant` — dasselbe Paar fuer die
+ Suchmaschinentabelle, gegen die UNVERAENDERTE strenge Regel.
+9. `searchprovider-fremder-nutzer-desselben-mandanten-gebunden-sichtbar` —
+ wie Pruefung 3, fuer die Suchmaschinen.
+10. `searchprovider-gebundenes-einfuegen-ohne-mandant-abgelehnt` — ein
+ gebundenes Einfuegen ohne Mandantenkennung wird von der unveraenderten
+ Regel abgewiesen. Das ist die Verteidigung der widerlegten Praemisse aus
+ Befund F: selbst wenn ein kuenftiger Schreibweg es versuchte, kaeme er
+ unter der Anwendungsrolle nicht durch.
+
+Fuelle die Liste auf dreizehn auf, wenn beim Lesen der Migration weitere
+Eigenschaften auffallen, die dieser Bereich braucht; nenne im SUMMARY die
+tatsaechliche Zahl, nicht diese. Ergaenzt wird nur, gestrichen wird nicht.
+
+TEIL 2 — die Codeaussagen, jede mit ihrer Reichweite. Fuehre die drei
+Nachpruefungen aus Befund D, F und I/J tatsaechlich aus und notiere jeweils die
+Anweisung, mit der du gesucht hast, damit die Aussage widerlegbar bleibt:
+die Echtheit der drei Besitzpruefungen, die Vollstaendigkeit der Suche nach
+einem Schreibweg fuer eine mandantenlose Suchmaschinenzeile, und den Ablauf im
+Frontend (Vorgabeanordnung bei fehlendem Datensatz, automatisches
+Zurueckschreiben beim Verlassen des Bearbeitungsmodus, Verhalten der
+Suchleiste). Miss ausserdem, ob dieser Bereich einen Hintergrunddienst enthaelt
+(Anweisung angeben), damit der entsprechende Abschnitt der Klassifikation in
+Aufgabe 3 mit einer Messung statt einer Annahme fortgeschrieben werden kann.
+Faellt eine dieser Nachpruefungen ANDERS aus als in den Planungsbefunden
+beschrieben, gilt die Messung und nicht der Plan — schreibe dann die Messung
+auf und benenne die Abweichung ausdruecklich.
+
+TEIL 3 — die Kritikschrift. Erweitere
+`docs/mandantentrennung-etappe2-fehlerrichtung.md` um einen Abschnitt
+`## Bereich dashboard` mit fuenf Unterabschnitten, in der Form der sechs
+bereits vorhandenen Bereichsabschnitte:
+
+- `### (w1) Die Messung` — die tatsaechlich beobachtete Ausgabe des
+ Werkzeuglaufs woertlich eingeruckt, die tragende Belegzeile benannt, dazu die
+ eigenstaendige Nachpruefung der widerlegten Praemisse aus Befund F samt der
+ Reichweite der Suche. Halte ausdruecklich fest, dass hier an dem Regelstand
+ NACH der Migration 20260910120000 gemessen wurde.
+- `### (w2) Signaltabelle je umgestelltem Pfad` — je Dienstmethode eine Zeile:
+ Verhalten bei zu wenig Ergebnis, und das konkrete Signal, an dem man es
+ saehe. Die Zeile zu `getLayout` muss das Zuruecksetzen auf die
+ Vorgabeanordnung benennen, die Zeile zu `getSearchProviders` das Verschwinden
+ der eigenen Suchmaschinen bei weiterhin funktionierender Leiste.
+- `### (w3) Welcher Code Leere als Abwesenheit deutet` — namentlich, mit
+ Dateiname und Stelle. Trenne dabei die Stellen im Backend von den drei
+ Stellen im Frontend aus Befund I und beschreibe die Schleife als Schleife:
+ leeres Dashboard, Neuaufbau, automatisches Zurueckschreiben, ueberschriebene
+ Aufzeichnung, angehaeufte Widget-Dubletten. Nenne ausdruecklich, dass der
+ Nutzer eine fertige, falsche Erklaerung zur Hand hat ("das Widget-System
+ spinnt", "meine Einstellungen sind weg") und deshalb keinen Fehler meldet.
+ Das Verhalten der Suchleiste aus Befund J gehoert ebenfalls hierhin.
+- `### (w4) Was dieser Durchlauf bewusst nicht loest` — die plattformweite
+ Eindeutigkeit aus Befund K mit dem Verweis auf WINDOWS #22 als
+ gleichgelagerten Fall, und die fehlende Unterscheidbarkeit von "noch keine
+ Anordnung gespeichert" und "Anordnung nicht sichtbar". Halte fest, welche
+ Vorabpruefung fuer Etappe 4 (`rls-preflight.mjs`) das aufdecken wuerde. Eine
+ Laufzeitwarnung ist zu erwaegen und, wenn verworfen, mit derselben
+ Begruendung zu verwerfen wie bei `getAllActiveConfigs` im Bereich `ldap` —
+ begruende die Entscheidung, uebernimm sie nicht.
+- `### (w5) Was dieser Durchlauf bewusst nicht anfasst` — das Frontend, der
+ Bereich `favorites` (dessen Tabelle an der Widget-Tabelle haengt, aber ein
+ eigener Bereich mit eigener Umstellung ist), der Bereich `module-registry`
+ samt der geerbten Entlastung aus Befund E, und die Ungenauigkeit aus Befund L
+ woertlich zitiert und richtiggestellt — ohne die fremde Datei zu aendern.
+
+Setze ausserdem im bereits vorhandenen Abschnitt `## Bereich module-registry`
+einen `**Nachtrag (260910-krx):**`, der festhaelt, dass der dortige Befund zur
+Reihenfolge (der Bereich `dashboard` erbt die Bindung der
+Modul-Zugriffsaufloesung) mit diesem Durchlauf eingeloest und nachgeprueft ist
+— und dass der Widget-Modulfilter NICHT ein zweites Mal gebunden wurde. Der
+urspruengliche Text bleibt unveraendert stehen; ein Nachtrag ergaenzt, er
+ersetzt nicht.
+
+Aendere in dieser Aufgabe KEINE Datei unter `apps/api/src` und KEINE Datei
+unter `apps/api/prisma`.
+
+
+ DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && OUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$OUT" && for K in dashboardlayout-gebunden-nur-eigener-mandant dashboardlayout-ungebunden-null-zeilen dashboardlayout-fremder-nutzer-desselben-mandanten-gebunden-sichtbar dashboardlayout-gebundener-konfliktschreibvorgang-auf-unsichtbare-zeile-scheitert-laut widgetinstance-ungebunden-null-zeilen widgetinstance-gebunden-nur-eigener-mandant widgetinstance-gebundenes-einfuegen-fremder-mandant-abgelehnt widgetinstance-gebundenes-loeschen-fremder-zeile-trifft-keine-zeile searchprovider-ungebunden-null-zeilen searchprovider-gebunden-nur-eigener-mandant searchprovider-fremder-nutzer-desselben-mandanten-gebunden-sichtbar searchprovider-gebundenes-einfuegen-ohne-mandant-abgelehnt searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar module-tabelle-traegt-keinen-zeilenschutz; do echo "$OUT" | grep -q "^$K: bestanden" || { echo "FEHLENDE ODER FEHLGESCHLAGENE PRUEFUNG: $K"; exit 1; }; done && echo "$OUT" | grep -qE '^Alle [0-9]+ Pruefungen bestanden\.$' && N=$(echo "$OUT" | sed -nE 's/^Alle ([0-9]+) Pruefungen bestanden\.$/\1/p') && { test "$N" -ge 86 || { echo "PRUEFUNGSZAHL: $N, erwartet mindestens 86 (74 bisherige plus mindestens 12 neue)"; exit 1; }; } && grep -q '^## Bereich dashboard$' docs/mandantentrennung-etappe2-fehlerrichtung.md && for S in w1 w2 w3 w4 w5; do grep -qE "^### \($S\) " docs/mandantentrennung-etappe2-fehlerrichtung.md || { echo "FEHLENDER UNTERABSCHNITT: ($S)"; exit 1; }; done && grep -q 'Nachtrag (260910-krx' docs/mandantentrennung-etappe2-fehlerrichtung.md && awk '/^## Bereich dashboard$/{f=1; next} /^## /{f=0} f && /dashboard-store|search-widget/{m++} END{ if(m+0 < 2){print "ABSCHNITT (w3): die beiden gemessenen Frontend-Dateien sind nicht namentlich genannt"; exit 1} }' docs/mandantentrennung-etappe2-fehlerrichtung.md && npm --prefix apps/api run test && npm --prefix apps/api run type-check && git rev-parse --verify f205120 >/dev/null && PRISMA_CHANGED=$(git diff --name-only f205120 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — in diesem Plan verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only f205120) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check\.mjs|docs/mandantentrennung-etappe2-fehlerrichtung\.md|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; }
+
+ `apps/api/scripts/rls-scratch-check.mjs` hat einen neunten Abschnitt mit mindestens zwoelf neuen, namentlich benannten Pruefungen gegen die aus den ausgelieferten Migrationen geschnittenen Regeln; alle Pruefungen des Werkzeugs bestehen, einschliesslich der bereits vorhandenen Suchmaschinen-Pruefung und der geliehenen Katalogmessung aus dem Bereich `module-registry`. `docs/mandantentrennung-etappe2-fehlerrichtung.md` hat einen Abschnitt `## Bereich dashboard` mit den fuenf Unterabschnitten (w1) bis (w5), die tatsaechlich beobachtete Werkzeugausgabe woertlich, die beweisvernichtende Schleife namentlich an den beiden gemessenen Frontend-Dateien beschrieben, und einen Nachtrag im Abschnitt `## Bereich module-registry`. Baseline gehalten: Tests gruen, Typpruefung sauber. Unter `apps/api/src`, `apps/api/prisma`, `apps/web` und den Compose-/Umgebungsdateien ist nichts geaendert.
+
+
+
+ Aufgabe 2: Anordnung und Widgets binden — erst die Testlage, dann die Umstellung, Lesen und Schreiben nie getrennt
+ apps/api/src/dashboard/dashboard.service.spec.ts, apps/api/src/dashboard/dashboard.service.ts, apps/api/src/dashboard/dashboard.controller.ts
+ apps/api/src/dashboard/dashboard.service.spec.ts, apps/api/src/dashboard/dashboard.service.ts, apps/api/src/dashboard/dashboard.controller.ts, apps/api/src/module-registry/module-access.service.spec.ts, apps/api/src/prisma/prisma-tenant.extension.ts
+
+Zuerst die Testlage in `dashboard.service.spec.ts`, DANN die Umstellung. Der
+Nachbau bekommt das Muster aus `module-access.service.spec.ts` uebernommen:
+`vi.mock` auf das Bindungshilfsmittel, das auf `prisma.__makeBoundClient(tenantId)`
+umgeleitet wird, und ein Bindungsprotokoll, das je Aufruf Mandantenkennung,
+Modell und Methode festhaelt. Der gebundene Klient veroeffentlicht AUSSCHLIESSLICH
+die mandantengebundenen Modelle — den Modulkatalog bewusst nicht, damit ein
+versehentlich gebundener Katalogzugriff im Test laut scheitert statt still
+durchzugehen.
+
+Alle acht bestehenden Faelle bleiben erhalten und gruen. Neu hinzu, jeder als
+eigener Fall:
+
+- Anordnung lesen: der Lesezugriff laeuft ueber den gebundenen Klienten, mit
+ der uebergebenen Mandantenkennung im Protokoll.
+- Anordnung speichern: der Schreibzugriff laeuft ueber den gebundenen Klienten
+ mit derselben Mandantenkennung. Zusaetzlich ein Fall, der festnagelt, dass
+ Lesen und Speichern GEMEINSAM gebunden sind — er wird rot, sobald eine der
+ beiden Haelften ungebunden liefe.
+- Widgets lesen: der Widget-Lesezugriff laeuft gebunden, der Katalogzugriff
+ NICHT — im Bindungsprotokoll steht kein Katalogeintrag, und der bestehende
+ Fall mit leerer Zuordnungstabelle bleibt unveraendert.
+- Widget anlegen: gebunden, mit der uebergebenen Mandantenkennung.
+- Widget-Konfiguration aendern: BEIDE Abfragen (Besitzpruefung und Aenderung)
+ laufen ueber DENSELBEN gebundenen Klienten und dieselbe Mandantenkennung.
+- Widget entfernen: ebenso, beide Abfragen ueber denselben Klienten.
+- Die Besitzpruefung bleibt wirksam: ein Widget, das einem anderen Benutzer
+ gehoert, fuehrt weiterhin zu `NotFoundException`, sowohl beim Aendern als
+ auch beim Entfernen. Diese beiden Faelle sind der Nachweis, dass die
+ Bindung die Pruefung ueber die Benutzerkennung ERGAENZT und nicht ersetzt —
+ sie muessen rot werden, wenn jemand die Pruefung entfernt.
+- Ein Fall, der festnagelt, dass keine Methode dieses Bereichs mehr als EINEN
+ gebundenen Klienten erzeugt.
+- Kein Widget vorhanden: der Rueckgabewert ist eine leere Liste, kein Fehler —
+ das ist die Deutung von Leere als Abwesenheit, hier ausdruecklich als
+ heutiges Verhalten festgehalten, damit eine spaetere Aenderung sichtbar wird.
+
+Die Zahl der neu hinzugekommenen Faelle wird am Ende ABGEZAEHLT und im SUMMARY
+mit der gezaehlten Zahl genannt, nicht mit der hier aufgelisteten — die Bereiche
+`tenders` und `module-registry` haben genau an dieser Stelle je eine falsche
+Zahl behauptet.
+
+
+Stelle in `dashboard.service.ts` die neun Zugriffe auf `dashboardLayout` und
+`widgetInstance` auf `forTenant()` um. Ein gebundener Klient JE METHODE, unter
+dem woertlichen Namen `tenantPrisma`, wie in jedem bereits umgestellten
+Bereich; gebundene Klienten werden nicht zwischen Methoden weitergereicht. Der
+Katalogzugriff in `getWidgets` bleibt in dieser Aufgabe unveraendert auf
+`this.prisma.module` — seine Begruendung folgt in Aufgabe 3. Die vier Zugriffe
+auf die Suchmaschinen bleiben in dieser Aufgabe ebenfalls unveraendert; sie sind
+Aufgabe 3.
+
+Die bestehenden `where`-Filter und die drei Besitzpruefungen ueber die
+Benutzerkennung bleiben ausnahmslos stehen. Begruende im Quelltext, warum sie
+kein Beiwerk sind: die Regeln dieses Bereichs kennen keine Benutzerdimension
+(gemessen in Aufgabe 1), und bis zum Scharfschalten ist die Anwendungspruefung
+ohnehin der einzige tatsaechlich wirksame Schutz.
+
+Die Modul-Zugriffsaufloesung wird NICHT angefasst — sie bindet bereits in ihrem
+eigenen Dienst. Halte das im Quelltext an der Aufrufstelle fest, damit niemand
+sie hier ein zweites Mal bindet.
+
+Reiche in `dashboard.controller.ts` bei den Handlern, die den bereits
+aufgeloesten Mandanten heute verwerfen, diesen an den Dienst durch. Die
+Mandantenkennung stammt unveraendert aus `extractContext` und damit aus dem
+Sitzungsnachweis — es entsteht KEINE neue Vertrauensquelle, und aus Rumpf oder
+Pfadparametern wird nichts uebernommen. Halte die Parameterreihenfolge des
+Dienstes durchgaengig: die Mandantenkennung steht unmittelbar hinter der
+Benutzerkennung, wie es die bereits vorhandenen Methoden vormachen.
+
+Behandle das in Aufgabe 1 gemessene Ergebnis der Konfliktmessung
+(Pruefung 5) BEDINGT, nicht vorweggenommen: kommt dort ein harter Fehler
+zurueck, uebersetze ihn beim Speichern der Anordnung in eine verstaendliche
+deutsche Meldung statt eines rohen Serverfehlers — dem Muster folgend, das der
+Bereich `tenders` fuer denselben Fall etabliert hat. Kommt etwas anderes
+zurueck, setze KEINE Uebersetzung ein, sondern halte den tatsaechlichen Befund
+im SUMMARY fest und trage ihn in Aufgabe 3 als offenen Punkt nach. Die
+Entscheidung folgt der Messung, nicht diesem Absatz.
+
+Aendere keine Datei ausserhalb der drei genannten. Fuehre am Ende dieser
+Aufgabe einen Falsifizierungsnachweis durch: nimm probeweise EINEN
+Bindungsaufruf zurueck, ueberzeuge dich, dass die Testlage rot wird, notiere
+Testname und Fehlermeldung woertlich, und stelle den Zustand wieder her.
+
+
+ DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs && npm --prefix apps/api run test && npm --prefix apps/api run type-check && npm --prefix apps/api run test -- src/dashboard/dashboard.service.spec.ts && grep -q '__makeBoundClient' apps/api/src/dashboard/dashboard.service.spec.ts && grep -q "vi.mock('../prisma/prisma-tenant.extension'" apps/api/src/dashboard/dashboard.service.spec.ts && grep -q "from '../prisma/prisma-tenant.extension'" apps/api/src/dashboard/dashboard.service.ts && B=$(grep -o "tenantPrisma\.[a-zA-Z]*\." apps/api/src/dashboard/dashboard.service.ts | wc -l | tr -d ' ') && { test "$B" -ge 9 || { echo "BINDUNG: nur $B gebundene Modellzugriffe in dashboard.service.ts, erwartet mindestens 9"; exit 1; }; } && U=$(grep -o "this\.prisma\.[a-zA-Z]*" apps/api/src/dashboard/dashboard.service.ts | wc -l | tr -d ' ') && { test "$U" -eq 5 || { echo "REST: $U ungebundene Rohtreffer in dashboard.service.ts, erwartet genau 5 (Katalog plus die vier Suchmaschinenzugriffe aus Aufgabe 3)"; exit 1; }; } && { if grep -qE 'tenantPrisma\.module\.' apps/api/src/dashboard/dashboard.service.ts; then echo "KATALOG WURDE GEBUNDEN oder die Begruendung nennt die verbotene Zeichenfolge woertlich"; exit 1; fi; } && { if grep -qE 'tenantPrisma\.searchProvider\.' apps/api/src/dashboard/dashboard.service.ts; then echo "SUCHMASCHINEN GEHOEREN IN AUFGABE 3, nicht hierher"; exit 1; fi; } && C=$(grep -o 'forTenant(this\.prisma' apps/api/src/dashboard/dashboard.service.ts | wc -l | tr -d ' ') && { test "$C" -eq 6 || { echo "KLIENTEN: $C forTenant-Aufrufstellen in dashboard.service.ts, erwartet genau 6 (eine je umgestellter Methode, keine zweite in derselben Methode)"; exit 1; }; } && grep -q 'getAccessibleModuleIds' apps/api/src/module-registry/module-access.service.ts && grep -q 'const tenantPrisma = forTenant(this.prisma, tenantId)' apps/api/src/module-registry/module-access.service.ts && test 0 -eq "$(grep -rn '\$transaction(' apps/api/src/dashboard --include=*.ts | grep -v spec | wc -l | tr -d ' ')" && git rev-parse --verify f205120 >/dev/null && PRISMA_CHANGED=$(git diff --name-only f205120 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — in diesem Plan verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only f205120) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(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|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; }
+
+ `dashboard.service.spec.ts` nutzt den Zwei-Klienten-Nachweis, alle acht bisherigen Faelle sind erhalten und gruen, und die in `` genannten Faelle sind abgedeckt — einschliesslich der beiden Faelle, die die Besitzpruefung ueber die Benutzerkennung festnageln, und des Falls, der Lesen und Speichern der Anordnung als gemeinsam gebunden festnagelt. In `dashboard.service.ts` laufen die neun Zugriffe auf Anordnung und Widgets ueber `forTenant()` unter dem Namen `tenantPrisma`, genau ein Klient je Methode; der Katalogzugriff und die vier Suchmaschinenzugriffe sind unveraendert. `dashboard.controller.ts` reicht den bereits aufgeloesten Mandanten durch, ohne eine neue Vertrauensquelle einzufuehren. Die Modul-Zugriffsaufloesung ist nachweislich unangetastet. Der Falsifizierungsnachweis ist durchgefuehrt, zurueckgenommen und mit Testname und Fehlermeldung notiert. Baseline gehalten.
+
+
+
+ Aufgabe 3: Die Suchmaschinen binden, den Katalog begruendet ungebunden lassen und beide Dokumente samt allen Handzaehlungen nachziehen
+ apps/api/src/dashboard/dashboard.service.spec.ts, apps/api/src/dashboard/dashboard.service.ts, apps/api/src/dashboard/dashboard.controller.ts, docs/mandantentrennung-zugriffsklassifikation.md, .planning/WINDOWS.md
+ apps/api/src/dashboard/dashboard.service.ts, apps/api/src/dashboard/dashboard.service.spec.ts, apps/api/src/dashboard/dashboard.controller.ts, apps/api/src/prisma/rls-access-inventory.spec.ts, docs/mandantentrennung-zugriffsklassifikation.md, .planning/WINDOWS.md
+
+Wieder erst die Testlage, dann die Umstellung. Der gebundene Klient des
+Nachbaus veroeffentlicht zusaetzlich das Suchmaschinenmodell; der Modulkatalog
+bleibt bewusst ausgeschlossen. Neu hinzu, jeder als eigener Fall:
+
+- Suchmaschinen lesen: der Lesezugriff laeuft gebunden. Die drei
+ Vorgabe-Suchmaschinen aus der Konstanten werden UNVERAENDERT vorangestellt —
+ sie stammen nicht aus der Datenbank und sind von der Bindung nicht betroffen.
+- Suchmaschine anlegen: gebunden, mit der uebergebenen Mandantenkennung, und
+ die Mandantenkennung wird weiterhin als Pflichtangabe geschrieben. Dieser Fall
+ ist die Testseite der widerlegten Praemisse: er wird rot, sobald jemand einen
+ Schreibweg ohne Mandantenkennung einfuehrt.
+- Suchmaschine entfernen: BEIDE Abfragen ueber DENSELBEN gebundenen Klienten.
+- Die Besitzpruefung bleibt wirksam: die Suchmaschine eines anderen Benutzers
+ fuehrt weiterhin zu `NotFoundException`, und eine der drei
+ Vorgabe-Suchmaschinen laesst sich weiterhin nicht entfernen.
+- Ein Wachhund, der den Katalogzugriff aus dem Bindungsprotokoll heraushaelt:
+ er wird rot, sobald jemand den Modulkatalog bindet.
+
+Auch hier gilt: die Zahl der neu hinzugekommenen Faelle wird ABGEZAEHLT und mit
+der gezaehlten Zahl im SUMMARY genannt.
+
+
+Stelle die vier Zugriffe auf die Suchmaschinen in `dashboard.service.ts` auf
+`forTenant()` um, ein Klient je Methode unter dem Namen `tenantPrisma`, und
+reiche in `dashboard.controller.ts` bei den beiden noch fehlenden Handlern den
+Mandanten durch. Die Besitzpruefungen ueber die Benutzerkennung bleiben stehen.
+
+Lass den EINEN Katalogzugriff ungebunden und schreibe die Begruendung an die
+Stelle, mit MESSUNG und BEDINGUNG getrennt, so wie der Bereich
+`module-registry` es etabliert hat: die Tabelle traegt heute keinen
+Zeilenschutz, eine Bindung waere heute wirkungslos und nicht katastrophal; sie
+wuerde katastrophal, WENN Etappe 3 dieser Tabelle eine Regel gibt. Berufe dich
+dabei auf die bestehende Werkzeugpruefung beim Namen, statt die Messung neu zu
+behaupten.
+
+Ziehe danach `docs/mandantentrennung-zugriffsklassifikation.md` an ALLEN fuenf
+handgepflegten Stellen nach — jede einzeln nachgesehen, keine ueberflogen:
+
+1. Die vier Bestandsaufnahme-Zeilen dieses Bereichs: Spalte `Stand` auf den
+ tatsaechlich gemessenen Wert, und die Begruendungen fortgeschrieben. Die
+ Katalogzeile bekommt die Trennung von Messung und Bedingung; die
+ Suchmaschinenzeile bekommt den Hinweis, dass die widerlegte Praemisse in
+ diesem Durchlauf eigenstaendig nachgeprueft wurde, mit der Reichweite der
+ Suche.
+2. Die Uebersichtszeile dieses Bereichs mit den NEU GEMESSENEN Zahlen, im
+ etablierten Stil mit dem Vermerk des vorherigen Standes.
+3. Die Summenzeile derselben Tabelle.
+4. Die Klassen-Verteilung samt der Zahl in ihrer Ueberschrift. Aendert sich
+ hier nichts, schreibe ausdruecklich hin, dass sich nichts aendert und warum
+ — eine unveraenderte Tabelle ohne Vermerk ist von einer vergessenen nicht zu
+ unterscheiden.
+5. Den Abschnitt zur Hintergrunddienst-Falle: ergaenze einen PLAIN-Absatz (KEINEN
+ Aufzaehlungspunkt in der Form der bestehenden Faelle, sonst waere die Zahl in
+ der Ueberschrift falsch), der festhaelt, dass dieser Bereich keinen
+ Hintergrunddienst enthaelt, mit der Anweisung, mit der das in Aufgabe 1
+ gemessen wurde.
+
+Ergaenze zusaetzlich den Abschnitt `Was diese Etappe NICHT entscheidet` um die
+Entscheidung dieses Bereichs zur offenen Architekturfrage — er bindet
+dienst-intern, wie alle sieben Bereiche vor ihm.
+
+Lege in `.planning/WINDOWS.md` einen neuen OFFENEN Eintrag an, in Tabelle UND
+JSON-Block, fuer die beweisvernichtende Schleife aus (w3): das Zusammenspiel
+aus Vorgabeanordnung bei fehlendem Datensatz, Neuaufbau durch den Nutzer,
+automatischem Zurueckschreiben beim Verlassen des Bearbeitungsmodus und
+angehaeuften Widget-Dubletten, mit der konkreten Vorabpruefung fuer Etappe 4
+und dem Vermerk, dass er an dieselbe Bedingung gebunden ist wie #18. Faellt
+aus Aufgabe 2 ein weiterer offener Punkt an (die bedingte Behandlung des
+Konfliktschreibvorgangs), gehoert er ebenfalls ins Register, nicht nur ins
+SUMMARY.
+
+Fuehre am Ende zwei weitere Falsifizierungsnachweise durch, jeder
+zurueckgenommen und mit Testname beziehungsweise Meldung woertlich notiert:
+den Katalogzugriff probeweise binden (der Wachhund muss rot werden) und eine
+Bestandsaufnahme-Zeile probeweise auf einen falschen `Stand` setzen (die
+maschinelle Bestandspruefung muss rot werden).
+
+
+ DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs && npm --prefix apps/api run test && npm --prefix apps/api run type-check && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && npm --prefix apps/api run test -- src/dashboard/dashboard.service.spec.ts && U=$(grep -o "this\.prisma\.[a-zA-Z]*" apps/api/src/dashboard/dashboard.service.ts | wc -l | tr -d ' ') && { test "$U" -eq 1 || { echo "KATALOG: $U ungebundene Rohtreffer in dashboard.service.ts, erwartet genau 1 (nur der Katalogzugriff)"; exit 1; }; } && { if grep -qE 'tenantPrisma\.module\.' apps/api/src/dashboard/dashboard.service.ts; then echo "KATALOG WURDE GEBUNDEN oder die Begruendung nennt die verbotene Zeichenfolge woertlich"; exit 1; fi; } && C=$(grep -o 'forTenant(this\.prisma' apps/api/src/dashboard/dashboard.service.ts | wc -l | tr -d ' ') && { test "$C" -eq 9 || { echo "KLIENTEN: $C forTenant-Aufrufstellen, erwartet genau 9 (eine je Dienstmethode mit Datenbankzugriff)"; exit 1; }; } && grep -qE '^\| apps/api/src/dashboard/dashboard\.service\.ts \| dashboardLayout \| muss-mandantengebunden \| gebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^\| apps/api/src/dashboard/dashboard\.service\.ts \| widgetInstance \| muss-mandantengebunden \| gebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^\| apps/api/src/dashboard/dashboard\.service\.ts \| searchProvider \| muss-mandantengebunden \| gebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^\| apps/api/src/dashboard/dashboard\.service\.ts \| module \| keine-mandantengebundene-tabelle \| ungebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && DU=$(grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/dashboard | grep -v spec | wc -l | tr -d ' ') && DB=$(grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/dashboard | grep -v spec | wc -l | tr -d ' ') && { test "$DU" -lt 13 || { echo "UEBERSICHTSZEILE: ungebundene Rohtreffer in apps/api/src/dashboard sind $DU, also nicht gesunken — es wurde nichts umgestellt"; exit 1; }; } && { test "$DB" -gt 0 || { echo "UEBERSICHTSZEILE: gebundene Rohtreffer in apps/api/src/dashboard sind $DB"; exit 1; }; } && { grep -qE "^\| dashboard \| ${DU} \| ${DB} \| \*\*war 13/0\*\*" docs/mandantentrennung-zugriffsklassifikation.md || { echo "UEBERSICHTSZEILE dashboard nennt nicht die neu gemessenen Zahlen ${DU}/${DB} im etablierten Stil"; exit 1; }; } && awk -F'|' '$2 ~ /^ *[a-z][a-z-]* *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *[0-9]+ *$/ { su+=$3; sb+=$4; n++ } $2 ~ /^ *\*\*Summe\*\* *$/ && $4 !~ /^ *$/ { g3=$3; g4=$4; gsub(/[^0-9]/,"",g3); gsub(/[^0-9]/,"",g4); ru=g3+0; rb=g4+0; seen=1 } END { if (!seen || n+0 != 12) { print "UEBERSICHTSTABELLE nicht erkannt, Bereichszeilen: " n; exit 1 } if (su+0 != ru || sb+0 != rb) { print "SUMMENZEILE stimmt nicht: Bereichszeilen ergeben " su "/" sb ", Summenzeile nennt " ru "/" rb; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && awk -F'|' '$2 ~ /^ *apps\/api\/src\// { k=$4; gsub(/^ +| +$/,"",k); cls[k]++; pairs++ } $2 ~ /^ *(muss-mandantengebunden|keine-mandantengebundene-tabelle|beides|bewusst-uebergreifend) *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *$/ { k=$2; gsub(/^ +| +$/,"",k); v=$3; gsub(/[^0-9]/,"",v); tab[k]=v+0; tn++ } $2 ~ /^ *\*\*Summe\*\* *$/ && $4 ~ /^ *$/ { v=$3; gsub(/[^0-9]/,"",v); tsum=v+0; tseen=1 } /^## Klassen-Verteilung/ { h=$0; gsub(/[^0-9]/,"",h); hp=h+0; hseen=1 } END { if (tn+0 != 4 || !tseen || !hseen) { print "KLASSEN-VERTEILUNG nicht erkannt: Klassenzeilen " tn ", Summenzeile " tseen ", Ueberschrift " hseen; exit 1 } if (tsum != pairs+0) { print "KLASSEN-SUMME stimmt nicht: Bestandsaufnahme hat " pairs " Paare, Tabellensumme nennt " tsum; exit 1 } if (hp != pairs+0) { print "UEBERSCHRIFT der Klassen-Verteilung nennt " hp " Paare, Bestandsaufnahme hat " pairs; exit 1 } s=0; for (k in tab) { if (tab[k] != cls[k]+0) { print "KLASSE " k ": Tabelle nennt " tab[k] ", Bestandsaufnahme zaehlt " cls[k]+0; exit 1 } s+=tab[k] } if (s != pairs+0) { print "KLASSENZEILEN ergeben " s ", Bestandsaufnahme hat " pairs; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && awk 'BEGIN{split("ein zwei drei vier x sechs sieben acht neun",w," "); w[5]="fünf"} /^## Der Hintergrunddienst als Falle/{seen=1; head=$0; f=1; next} /^## /{f=0} f && /^- \*\*`/{n++} f && /^\*\*Der .* Fall, anderer Bauart/{n++} f && /dashboard/{m=1} END{ if(!seen){print "ABSCHNITT Hintergrunddienst nicht gefunden"; exit 1} want="## Der Hintergrunddienst als Falle — " w[n] " Fälle"; if(head != want){printf "HINTERGRUNDDIENST-UEBERSCHRIFT nennt \"%s\", gezaehlt wurden %d Faelle, erwartet \"%s\"\n", head, n, want; exit 1} if(!m){print "ABSCHNITT Hintergrunddienst nennt diesen Bereich nicht — die Abwesenheit eines sechsten Falls ist nicht belegt"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && awk '/^## Was diese Etappe NICHT entscheidet/{f=1; next} f && /dashboard/{m=1} END{ if(!m){print "ABSCHNITT \"Was diese Etappe NICHT entscheidet\" nennt den Bereich dashboard nicht"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && python3 -c "
+import re,sys
+s=open('.planning/WINDOWS.md',encoding='utf-8').read()
+rows=[l for l in s.splitlines() if re.match(r'^\| \d+ \|', l)]
+ids=re.findall(r'\"id\": (\d+),', s)
+if len(rows)!=len(ids): print('WINDOWS: %d Tabellenzeilen, %d JSON-Eintraege' % (len(rows), len(ids))); sys.exit(1)
+if not any(re.match(r'^\| 25 \|', l) for l in rows): print('WINDOWS: Eintrag 25 fehlt in der Tabelle'); sys.exit(1)
+if '25' not in ids: print('WINDOWS: Eintrag 25 fehlt im JSON-Block'); sys.exit(1)
+if 'dashboard' not in s: print('WINDOWS: kein Eintrag nennt diesen Bereich'); sys.exit(1)
+" && test 0 -eq "$(grep -rn '\$transaction(' apps/api/src/dashboard --include=*.ts | grep -v spec | wc -l | tr -d ' ')" && git rev-parse --verify f205120 >/dev/null && PRISMA_CHANGED=$(git diff --name-only f205120 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — in diesem Plan verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only f205120) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check\.mjs|docs/mandantentrennung-etappe2-fehlerrichtung\.md|docs/mandantentrennung-zugriffsklassifikation\.md|apps/api/src/dashboard/dashboard\.service\.ts|apps/api/src/dashboard/dashboard\.service\.spec\.ts|apps/api/src/dashboard/dashboard\.controller\.ts|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; }
+
+ In `dashboard.service.ts` laufen alle zwoelf mandantengebundenen Zugriffe ueber `forTenant()` unter dem Namen `tenantPrisma`, genau ein Klient je Dienstmethode, und der eine Katalogzugriff bleibt ungebunden mit einer Begruendung, die Messung und Bedingung trennt und sich auf die bestehende Werkzeugpruefung beim Namen beruft. Alle fuenf handgepflegten Stellen von `docs/mandantentrennung-zugriffsklassifikation.md` sind nachgezogen und maschinell gegatet: vier Bestandsaufnahme-Zeilen, Uebersichtszeile mit neu gemessenen Zahlen, Summenzeile, Klassen-Verteilung samt Ueberschrift, Hintergrunddienst-Abschnitt mit dem Messbeleg fuer die Abwesenheit eines sechsten Falls; dazu der Abschnitt `Was diese Etappe NICHT entscheidet`. `.planning/WINDOWS.md` traegt den neuen offenen Eintrag zur beweisvernichtenden Schleife in Tabelle und JSON-Block. Die maschinelle Bestandspruefung ist gruen. Beide weiteren Falsifizierungsnachweise sind durchgefuehrt, zurueckgenommen und woertlich notiert. Baseline gehalten, Schalter unveraendert aus.
+
+
+
+
+
+
+Konfiguriert: ASVS-Stufe 1, blockierend ab `high`.
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Browser/Benutzer → Dashboard-API | Benutzerkennung und Mandantenkennung stammen ausschliesslich aus dem validierten Sitzungsnachweis (`extractContext` liest `request.user` beziehungsweise den vom Wächter gesetzten Mandanten), nie aus Rumpf oder Pfadparametern. Die Widget- und Suchmaschinenkennung im Pfad ist dagegen frei waehlbare Nutzereingabe. |
+| Nutzer A → Anordnung, Widgets und Suchmaschinen von Nutzer B DESSELBEN Mandanten | Die Grenze, die die Datenbank in diesem Bereich NACHWEISLICH nicht zieht — alle drei Regeln kennen nur die Mandantendimension. Gezogen wird sie allein von den drei anwendungsseitigen Besitzpruefungen ueber die Benutzerkennung. |
+| Mandant A → Zeilen des Mandanten B | Die Grenze, um die es in diesem Plan geht. Heute ausschliesslich von der Anwendung gezogen, nach diesem Plan zusaetzlich von der Datenbank — aber erst wirksam nach Etappe 4. |
+| API → PostgreSQL | Die Zeilenschutz-Grenze. Heute wirkungslos, weil die Rolle das Umgehungsrecht traegt (WINDOWS #18) — dieser Plan bereitet sie vor, schaltet sie NICHT scharf. |
+| API → plattformweiter Modulkatalog | Eine bewusst durchlaessige Grenze: der Katalog gehoert keinem Mandanten und traegt heute keinen Zeilenschutz. |
+| Nutzer → externe Suchmaschine | Die Grenze, die das Such-Widget im Browser ueberschreitet. Welche Suchmaschine eine eingegebene Zeichenfolge erhaelt, haengt an der Liste, die dieser Bereich liefert. |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-KRX-01 | Information Disclosure | `dashboard.service.ts`, `getLayout`/`getWidgets`/`getSearchProviders` | high | mitigate | Quer-Lesen der Anordnung, der platzierten Widgets oder der eigenen Suchmaschinen eines fremden Mandanten. Alle drei Lesepfade werden gebunden; die bestehende Filterung ueber die Benutzerkennung bleibt zusaetzlich stehen. Gemessen in Aufgabe 1: `dashboardlayout-gebunden-nur-eigener-mandant`, `widgetinstance-gebunden-nur-eigener-mandant`, `searchprovider-gebunden-nur-eigener-mandant`. |
+| T-KRX-02 | Information Disclosure | Alle drei Regeln dieses Bereichs | high | accept | Quer-Lesen zwischen zwei Nutzern DESSELBEN Mandanten. Die Regeln kennen keine Benutzerdimension — gemessen in Aufgabe 1 (`*-fremder-nutzer-desselben-mandanten-gebunden-sichtbar`, das Gelingen IST das Ergebnis). Bewusst akzeptiert und aufgezeichnet: der Schutz bleibt vollstaendig bei der anwendungsseitigen Filterung ueber die Benutzerkennung, die dieser Plan ausdruecklich NICHT entfernt und als Testfall festnagelt. Eine Benutzerdimension in der Regel waere eine Schemafrage und gehoert nach Etappe 3. |
+| T-KRX-03 | Tampering | `dashboard.service.ts`, `removeWidget`/`updateWidgetConfig`/`removeSearchProvider` | high | mitigate | Quer-Loeschen oder Quer-Aendern ueber die Kennung im Pfad — die Form, die im Bereich `ldap` die erste echte Luecke dieses Vorhabens war. Hier existiert die Besitzpruefung tatsaechlich (Befund D, zur Ausfuehrungszeit erneut nachzupruefen), sie wird durch die Bindung ERGAENZT, und beide Abfragen jedes Paares laufen ueber DENSELBEN gebundenen Klienten, damit die Pruefung nicht auf einer Zeile aufgeht, die der Schreibvorgang nicht mehr sieht. Als zwei Testfaelle je Pfad festgenagelt; datenbankseitig gemessen mit `widgetinstance-gebundenes-loeschen-fremder-zeile-trifft-keine-zeile`. |
+| T-KRX-04 | Denial of Service | `dashboard.service.ts`, `getLayout`, plus `dashboard-store.ts` im Web | high | mitigate | Die umgekehrte Fehlerrichtung mit Beweisvernichtung: ein zu kleines Leseergebnis liefert die Vorgabeanordnung statt eines Fehlers, der Nutzer baut neu auf, und das automatische Zurueckschreiben beim Verlassen des Bearbeitungsmodus ueberschreibt die urspruengliche Zeile, bevor jemand die Ursache untersuchen kann. Vollstaendige, gemeinsame Bindung von Lesen und Speichern; Signaltabelle und namentliche Liste in der Kritikschrift; offener Ledger-Eintrag mit der Vorabpruefung fuer Etappe 4. Das Frontend wird NICHT geaendert — die Kette wird beschrieben, nicht unterbrochen. |
+| T-KRX-05 | Information Disclosure | `search-widget.tsx`, Rueckfall auf den ersten Eintrag | medium | accept | Verschwinden die eigenen Suchmaschinen aus der Liste, faellt die Auswahl auf den ersten Eintrag (eine externe Suchmaschine) zurueck — eine fuer ein internes Werkzeug gedachte Suchanfrage ginge dann nach draussen. Reichweite in Aufgabe 1 nachzumessen (wie sichtbar der Rueckfall in der Auswahlliste ist). Bewusst akzeptiert und aufgezeichnet statt hier behoben: das Frontend ist nicht Gegenstand dieses Plans, und der Fall wird durch die vollstaendige Bindung des Lesepfads gar nicht erst erreicht. Festgehalten in (w3) und in der Signaltabelle. |
+| T-KRX-06 | Tampering | `DashboardLayout.userId`, plattformweit eindeutig | medium | accept | Die Kette aus den Bereichen `tenders` und `user`: unsichtbare Zeile, falsches "frei", harter Eindeutigkeitsfehler — hier strukturell moeglich, weil der Eindeutigkeitsschluessel keinen Mandantenanteil traegt. In Aufgabe 1 GEMESSEN statt angenommen (`dashboardlayout-gebundener-konfliktschreibvorgang-auf-unsichtbare-zeile-scheitert-laut`), in Aufgabe 2 BEDINGT behandelt (verstaendliche Meldung statt rohem Serverfehler). Ueber einen Anwendungspfad heute nicht erreichbar (die Mandantenkennung eines Benutzers aendert sich nach der Anlage nicht, gemessen); die ehrliche Reparatur waere eine Schemaaenderung und gehoert als Produktentscheidung nach Etappe 3, wie WINDOWS #22 sie fuer denselben Fall bereits vormerkt. |
+| T-KRX-07 | Elevation of Privilege | `dashboard.controller.ts`, Durchreichen des Mandanten | high | mitigate | Die Mandantenkennung koennte beim Umbau versehentlich aus Rumpf oder Pfad statt aus dem Sitzungsnachweis genommen werden. Die Handler nehmen sie unveraendert aus `extractContext`, das sie aus dem validierten Sitzungsnachweis liest und ohne Mandant mit `ForbiddenException` abbricht — es entsteht keine neue Vertrauensquelle. Als Negativ-Gate abgesichert: keine Datei ausserhalb der drei genannten wird geaendert. |
+| T-KRX-08 | Information Disclosure | Modulkatalog (`Module`) | medium | mitigate | Der plattformweite Katalog wird versehentlich gebunden und verschwindet, sobald Etappe 3 ihm eine Regel gibt. Negativ-Gate in beiden Verifikationen plus ein Wachhund-Testfall, der jeden Katalogzugriff aus dem Bindungsprotokoll heraushaelt. |
+| T-KRX-09 | Tampering | `SearchProvider`, nullbare Mandantenkennung | medium | mitigate | Eine kuenftige mandantenlose Zeile waere unter der strengen Regel fuer JEDEN Mandanten unsichtbar — die Praemisse von WINDOWS #19, fuer dieses Modell widerlegt. In diesem Durchlauf EIGENSTAENDIG nachgeprueft (Befund F, mit benannter Reichweite) statt aus 260910-jab abgeschrieben, und zusaetzlich datenbankseitig verteidigt: `searchprovider-gebundenes-einfuegen-ohne-mandant-abgelehnt`. Faellt die Nachpruefung anders aus, wird der Befund umgedreht statt uebergangen. |
+| T-KRX-10 | Repudiation | `getLayout`, `getWidgets`, `getSearchProviders` | medium | accept | Es gibt kein Signal, das "noch nichts eingerichtet" von "nicht sichtbar" unterscheidet — beide liefern eine leere Antwort mit Status 200 und keinen Protokolleintrag. Bewusst akzeptiert und AUFGEZEICHNET statt durch eine Laufzeitwarnung ueberdeckt, die auf einer frischen Installation Dauerlaerm waere (dieselbe Begruendung wie bei `getAllActiveConfigs` im Bereich `ldap`). Das Signal gehoert in die Vorabpruefung von Etappe 4, und als Testfall festgenagelt, damit die Aufzeichnung rot wird, sobald jemand ein Signal einbaut. |
+| T-KRX-11 | Spoofing | Sitzungsnachweis | low | accept | Mandanten- oder Benutzerkennung aus Rumpf oder Pfad statt aus dem Sitzungsnachweis. Bereits in Phase 05 und 15 behandelt (T-05-01/T-05-02, T-15-10); dieser Plan aendert daran nichts und schwaecht es nicht. |
+| T-KRX-SC | Tampering | Paketinstallation | low | accept | Dieser Plan installiert kein Paket (npm/pip/cargo) und fuegt keine Abhaengigkeit hinzu. Das Legitimitaets-Gate faellt damit nicht an; ausdruecklich festgehalten statt schweigend ausgelassen. |
+
+
+
+
+
+Nach Abschluss aller drei Aufgaben:
+
+1. `node apps/api/scripts/rls-scratch-check.mjs` meldet alle Pruefungen
+ bestanden (74 bisherige plus die neuen), Rueckgabewert 0 — einschliesslich
+ der bereits vorhandenen Suchmaschinen-Pruefung, die unveraendert bestehen
+ bleibt.
+2. `npm --prefix apps/api run test` meldet mindestens 839 Tests gruen in
+ mindestens 56 Dateien.
+3. `npm --prefix apps/api run type-check` ist sauber.
+4. `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts`
+ ist gruen — die Bestandsaufnahme stimmt mit dem Quelltext ueberein.
+5. Alle fuenf handgepflegten Stellen der Klassifikation sind maschinell gegatet
+ und gruen, und die Zaehlgates LEITEN ihre Werte aus den im Dokument selbst
+ genannten Messanweisungen ab, statt sie fest einzutragen.
+6. Der Umfang ist als ERLAUBNISLISTE gegatet, nicht als Verbotsliste: jede
+ Datei, die sich gegenueber dem Ausgangsstand `f205120` geaendert hat, muss
+ eine der sieben in `files_modified` genannten sein (oder unter
+ `.planning/` liegen). Damit faellt jede Streuaenderung auf — auch eine, an
+ die eine Verbotsliste nicht gedacht haette. Zusaetzlich ausdruecklich:
+ unter `apps/api/prisma` hat sich nichts geaendert. Beide Pruefungen laufen
+ gegen den Ausgangsstand statt gegen `HEAD` und gelten deshalb auch,
+ nachdem eine Aufgabe bereits eingecheckt wurde.
+7. `DATABASE_URL` zeigt unveraendert auf die Rolle `tessera`.
+
+
+
+
+
+- Die dreizehn Zugriffe des Bereichs sind vollstaendig entschieden: zwoelf
+ gebunden, einer begruendet ungebunden, keiner unentschieden.
+- Keine Tabelle dieses Bereichs wird halb gebunden: Lesen und Schreiben
+ derselben Zeile laufen nie auf verschiedenen Seiten der Bindung, und keine
+ Methode erzeugt mehr als einen gebundenen Klienten.
+- Die Begruendung des ungebundenen Zugriffs trennt Messung von Bedingung — sie
+ behauptet nichts, was heute nicht gilt, und verschweigt nicht, was morgen
+ gelten wuerde.
+- Die umgekehrte Fehlerrichtung ist an den echten, ausgelieferten Regeln in
+ ihrem AKTUELLEN Stand gemessen und je Pfad mit ihrem konkreten Signal
+ beschrieben.
+- Die beweisvernichtende Schleife ist benannt, an den tatsaechlichen Dateien
+ belegt und als offener Ledger-Eintrag mit einer konkreten Vorabpruefung
+ festgehalten — nicht als Sorge formuliert.
+- Die widerlegte Praemisse zu den Suchmaschinen ist eigenstaendig nachgeprueft,
+ mit benannter Reichweite der Suche, und datenbankseitig zusaetzlich
+ verteidigt.
+- Die geerbte Entlastung ist nachgeprueft und nicht ein zweites Mal repariert.
+- Alle drei Falsifizierungsnachweise sind durchgefuehrt, zurueckgenommen und im
+ SUMMARY mit Testname und Fehlermeldung festgehalten — nicht als Behauptung.
+- Jede im SUMMARY genannte Zahl (neue Werkzeugpruefungen, neue Testfaelle,
+ umgestellte Zugriffe) ist ABGEZAEHLT, nicht aus diesem Plan abgeschrieben.
+- Baseline gehalten am Ende jeder Aufgabe, nicht nur am Ende des Plans.
+- Der Schalter ist weiterhin AUS.
+
+
+
+