Files
tessera-ctl/.planning/WINDOWS.md
T
schalli 222f453747 feat(quick-260909-mir): dkv-Testlage herstellen, Konfigurationspfade binden, Planer-Pfad benennen
- apps/api/src/dkv/dkv.service.spec.ts (neu): Zwei-Klienten-Nachweis nach
  dem Muster aus groups.service.spec.ts/tender-triage.service.spec.ts —
  dieser Bereich hatte vorher KEINE Testdatei (Befund J). 7 Testfaelle
  decken getConfigForApi, saveConfig (Zugangsdaten-Erhaltung), testConnection,
  die Verarbeitungsstrecke und den bewusst ungebundenen Planer-Startpfad ab
- dkv.service.ts: loadConfig(tenantId?) in zwei Methoden geteilt —
  loadConfig(tenantId) [Pflicht-Mandant, gebunden] und die neue, eigene
  Methode loadAnyActiveConfigForScheduler() [bewusst UNGEBUNDEN, eigener
  Kopfkommentar mit beiden Zustaenden]. getConfigForApi/saveConfig/
  testConnection/_runPipeline binden je EINEN Klienten pro Methode
  vollstaendig ueber forTenant()
- dkv-scheduler.service.ts: Kopfkommentar fortgeschrieben (beide Zustaende,
  Praezedenzfall, Unsymmetrie), Aufruf auf loadAnyActiveConfigForScheduler()
  umgestellt — an der Ablauflogik des Planers nichts geaendert
- .planning/WINDOWS.md: Eintrag #21 (deviation) fuer die benannte Altlast
  des Planer-Startpfads angelegt
- docs/mandantentrennung-zugriffsklassifikation.md: dkvModuleConfig-Zeile
  auf den jetzt gemessenen Stand "gemischt" nachgezogen (Rule 3 — noetig,
  damit rls-access-inventory.spec.ts nach der Aufteilung von loadConfig()
  gruen bleibt; die uebrigen zwei dkv-Zeilen und die Uebersichtstabelle
  bleiben Aufgabe 3 vorbehalten)
- Falsifizierungsnachweis erbracht: getConfigForApi's erster gebundener
  Client probeweise durch this.prisma ersetzt, genau Test 1 wurde rot
  (6 andere blieben gruen), Rueckbau zurueckgenommen, Dateien identisch
  zum Ausgangsstand bestaetigt

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 16:46:09 +02:00

46 KiB

schema_version, open_count, waived_count, fixed_count, total_count, last_updated
schema_version open_count waived_count fixed_count total_count last_updated
1 4 1 16 21 2026-09-09T14:41:07.256Z

Broken Windows Ledger

Cross-phase defect register. With workflow.windows_enforce enabled, /gsd-ship blocks while open_count > 0. Waive with gsd-tools windows waive <id> "<reason>" (reason required). Mark fixed with gsd-tools windows fixed <id>.

id phase kind file line description status reason recorded_at resolved_at
1 15 unrun-verify .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-06-PLAN.md 15-06 : manueller Browser-Durchklick (Anlegen/Umbenennen/Standardmarkierung/AD-Bindung/Mitglieder/Loeschdialog + Fehlerpfade bei abgeschalteter API) nicht ausgefuehrt — kein Browser-Tool in dieser Session verfuegbar fixed 2026-08-04T17:05:42.026Z 2026-09-07T08:03:50.832Z
2 15 unrun-verify .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-07-PLAN.md Manueller Browser-Durchklick aus dem Plan-Verification-Block (Matrix-Freigabe setzen/entziehen, Aktivierungsdialog beide Wege, Direkt-Grant neben Gruppen-Grant, Fehlerfall bei gestoppter API, lange Namen) nicht ausgefuehrt -- kein Browser-Tool in dieser Session (15-07-SUMMARY.md D4) fixed 2026-08-04T17:27:42.267Z 2026-09-07T08:03:51.067Z
3 15 unrun-verify .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-08-PLAN.md Manueller Browser-Durchklick aus dem Plan-Verification-Block (USER ohne Freigabe: Sidebar/403-Seite/Marketplace-Badge+Toast/API-403 identisch; ADMIN: alle vier Ebenen zugaenglich) nicht ausgefuehrt - kein Browser-Tool in dieser Session verfuegbar. fixed 2026-08-04T17:50:13.320Z 2026-09-07T08:03:51.288Z
4 16 unrun-verify .planning/phases/16-ad-gruppen-synchronisation/16-03-PLAN.md 16-03 Task 2 human-check: RESEARCH.md Annahmen A1 (objectGUID uebersteht AD-Umbenennung) und A2 (byteweise Hex-Filter-Syntax) sind gegen kein echtes Active Directory geprueft — kein erreichbares AD in dieser Sandbox. Negatives Ergebnis bei A1 oder A2 ist Stopp-Grund fuer die Loeschsemantik (D-05). fixed 2026-08-06T14:21:59.987Z 2026-09-07T13:10:51.060Z
5 16 unrun-verify .planning/phases/16-ad-gruppen-synchronisation/16-04-PLAN.md Manueller Browser-Durchklick aus dem Plan-Verification-Block (drei GroupFormModal-Zustaende: Anlegen mit Hinweis-Link, lokale Gruppe umbenennen, importierte Gruppe mit gesperrtem Namen + internem Namen speichern; Namensanzeige mit Tooltip in der Liste nach dem Speichern) nicht ausgefuehrt — kein Browser-Tool in dieser Session verfuegbar. fixed 2026-08-06T14:31:14.336Z 2026-09-07T08:03:51.517Z
6 16 unrun-verify .planning/phases/16-ad-gruppen-synchronisation/16-05-PLAN.md 16-05 Manuell nachzuholen (Browser): Sync ausloesen und pruefen, dass alle drei Zahlenzeilen erscheinen; Lauf mit verschobener Standardmarkierung provozieren und Amber-Zeile pruefen; Freigabe-Matrix-Spaltensuche unter beiden Namen probieren - nicht ausgefuehrt, kein Browser-Tool in dieser Session verfuegbar. fixed 2026-08-06T14:39:03.905Z 2026-09-07T13:10:51.272Z
7 17 unrun-verify .planning/phases/17-eigene-ausschreibungs-quellen-je-nutzer/17-01-PLAN.md 17-01 Task 2 human-check: Browser-Gegenprobe (normaler Nutzer oeffnet /modules/tender-radar/my-sources, speichert Postfach, zweites Konto desselben Mandanten sieht leeres Formular) nicht ausgefuehrt — kein Browser-Tool in dieser Session verfuegbar. Automatisierte Pruefungen (prisma validate/migrate status, Index-Liste, beide Typpruefungen, 335/335 src/tenders-Tests) sind gelaufen und gruen. fixed 2026-08-12T09:24:35.289Z 2026-09-07T07:50:56.468Z
8 17 unrun-verify apps/web/src/app/(portal)/modules/tender-radar/my-sources/page.tsx Browser-Gegenprobe Plan 17-03 Task 1 (Tracer-Feedback-Gate): Meine Quellen mit drei Abschnitten, neu angelegter eigener Feed sofort sichtbar, plattformweiter Feed ohne Entfernen-Knopf, Digest-Intervall speichert nach Reload — kein Browser-Tool in dieser Sitzung verfuegbar fixed 2026-08-12T10:02:49.122Z 2026-09-07T07:50:56.693Z
9 17 unrun-verify apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx Browser-Gegenprobe Plan 17-03 Task 2: USER-Konto sieht keine Bedienelemente auf /settings (nur Hinweis+Verweis), ADMIN-Konto sieht Abrufintervall+plattformweite Feeds ohne Postfach/Benachrichtigung, Zahnrad fuehrt in beiden Faellen nach Meine Quellen — kein Browser-Tool in dieser Sitzung verfuegbar fixed 2026-08-12T10:02:49.291Z 2026-09-07T07:50:56.910Z
10 15 unmet-truth apps/web/src/app/(portal)/modules/tender-radar/page.tsx PERM-04 greift nicht auf modul-eigenen Routen: der Zugriffs-Guard sitzt nur in modules/[category]/[moduleSlug]/page.tsx. Die vier fest verdrahteten Modulrouten (tender-radar, dkv-fleet, cert-manager, domaincheck) samt Unterseiten (z.B. tender-radar/my-sources) laufen daran vorbei und rendern fuer einen USER OHNE Modulfreigabe vollstaendig, inklusive bedienbarer Knoepfe. Im Browser gemessen am 2026-09-07 mit nutzer2 (keine Freigabe): /modules/procurement/tender-radar zeigt korrekt die 403-Seite, /modules/tender-radar und /modules/tender-radar/my-sources und /modules/dkv-fleet zeigen die volle Seite. Keine Datenpreisgabe - die API antwortet auf allen Endpunkten mit 403 - aber der Nutzer sieht Bedienelemente, die er nicht benutzen darf, und bekommt rohe englische Techniktexte statt einer verstaendlichen Meldung. Beleg: .planning/phases/17-eigene-ausschreibungs-quellen-je-nutzer/uat-2026-09-07/befund-modul-eigene-route-ohne-freigabe.png fixed 2026-09-07T08:03:45.978Z 2026-09-07T08:40:04.951Z
11 13 unmet-truth apps/api/src/tenders/tender-fingerprint.ts Cross-Source-Dedup kollabiert die Mehrzahl echter Duplikate nicht: tenderFingerprint() haengt [buyerName, title, cpvDivisionKey, deadlineKey, valueBucket] zu einem String und hasht ihn. Die Scraper-Adapter liefern cpvDivisions immer leer, DOE dagegen meist gefuellt - dieselbe Ausschreibung ergibt aus beiden Quellen verschiedene Fingerabdruecke, die Fingerprint-Stufe greift also gerade dort nicht, wo sie greifen soll. Steht als offener Rest in 13-VERIFICATION.md (status gaps_found, 3/4 Wahrheiten), war aber bisher nicht im Ledger und damit unsichtbar. Die frueher vermutete Ursache (Normalizer-Defaults) ist mit 9881005 geschlossen und nicht gemeint. fixed 2026-09-07T08:09:04.188Z 2026-09-07T08:42:34.794Z
12 14 unrun-verify .planning/phases/14-rss-email-alert-ingestion-module-rollout/14-03-PLAN.md 14-03 Task 4 (checkpoint human-verify, gate blocking): echtes Portal-Alert-Postfach ueber den Exchange/EWS-Weg anbinden und eine echte Alarm-Mail ingestieren. Zu belegen sind (a) die Mail erzeugt eine mandantenprivate Ausschreibung, (b) ein anderer Mandant sieht sie nicht, (c) der EWS-Body-Abruf liefert nicht-leeren, nicht-verstuemmelten Inhalt gegen einen echten Exchange-Server. Der handgeschriebene NTLM/SOAP-Weg ist nur gegen gemocktes httpntlm.post geprueft, nie gegen einen echten Server. Braucht ein echtes Postfach - vom User am 2026-07-24 bewusst zurueckgestellt. Stand bisher nur in 14-VERIFICATION.md (status human_needed), nicht im Ledger. waived Zurueckgestellt auf Entscheidung des Users vom 2026-09-09: es gibt intern derzeit KEIN Postfach, in das Ausschreibungs-Alarme hereinkommen — die Voraussetzung des Pruefpunkts existiert nicht. Ein frueheres Missverstaendnis hatte ein solches Postfach angenommen. Nachzuholen, sobald ein Alarm-Postfach eingerichtet ist: dann sind (a) mandantenprivate Ausschreibung aus der Mail, (b) Unsichtbarkeit fuer andere Mandanten und (c) nicht-leerer, nicht-verstuemmelter Nachrichtentext zu belegen. Teilentlastung vom 2026-09-09: der handgeschriebene NTLM/EWS-Weg wurde beim Test von #16 erstmals gegen den echten Exchange owa.ctl.de gemessen — Anmeldung, FindFolder und Ordneraufloesung funktionieren, belegt durch die echte ServerVersionInfo-Antwort (Exchange 15.2) im API-Protokoll. Der frueher als fragil markierte Verbindungsaufbau ist damit nicht mehr ungeprueft; offen bleibt allein das Einlesen einer echten Alarm-Mail. 2026-09-07T08:09:04.406Z 2026-09-09T05:20:34.736Z
13 1 unmet-truth apps/web/src/app/globals.css Tailwind-4-Dunkelvariante war nie an die .dark-Klasse gebunden: globals.css definierte die Farbtokens unter .dark, deklarierte aber kein @custom-variant dark. In Tailwind 4 haengt die dark:-Utility-Variante per Vorgabe an prefers-color-scheme, nicht an einer Klasse. Die App schaltet den Modus jedoch ueber next-themes mit attribute=class. Folge: JEDE dark:-Utility im Web-Quellcode (ueber 100 Vorkommen in 10+ Dateien - Status-, Warn- und Fehlerfarben, Hinweisboxen, Badges in der Administration) blieb wirkungslos, sobald der Nutzer im Portal auf dunkel stellte, waehrend sein Betriebssystem hell stand. Am laufenden System gemessen: html.dark gesetzt, prefers-color-scheme hell, dark:bg-gray-800 ergab transparent und dark:text-green-400 blieb ohne Wirkung. Unentdeckt geblieben, weil Hintergrund und Textfarbe ueber CSS-Variablen unter .dark laufen und deshalb korrekt umschalten. Aufgedeckt beim Logo-Einbau 260907-fgv, dessen Plattenkontur dark:stroke-white/25 aus demselben Grund nicht griff. fixed 2026-09-07T09:32:24.960Z 2026-09-07T09:35:49.019Z
14 15 unmet-truth apps/web/src/app/(portal)/admin/modules/grants/page.tsx Die Suche in der Freigaben-Matrix macht die Matrix unbenutzbar, sobald sie etwas findet: derselbe Suchbegriff filtert BEIDE Achsen unabhaengig voneinander (page.tsx:137 filtert Module ueber m.name, page.tsx:145 filtert Gruppen ueber internalName/name). Ein Begriff, der nur eine Achse trifft, leert die andere vollstaendig — es bleibt nie ein Kaestchen zum Klicken uebrig. Am 2026-09-07 im Browser auf alpha gemessen: Suche 'Claude_VT' bzw. 'Vertrieb' laesst die Gruppenspalte stehen, entfernt aber jede Modulzeile; Suche 'Cert' laesst die Modulzeile stehen, entfernt aber jede Gruppenspalte. Damit scheitert genau der Zweck der Suche — in einer grossen Matrix die Kreuzung Modul x Gruppe finden. Belege: .planning/phases/16-ad-gruppen-synchronisation/uat-2026-09-07/befund-matrix-suche-gruppenname.png und befund-matrix-suche-modulname.png fixed 2026-09-07T12:46:56.172Z 2026-09-09T06:25:17.928Z
15 16 unmet-truth apps/api/src/ldap/ldap.service.ts Der Sync reicht rohe Techniktexte an den Administrator durch und laesst echte AD-Konten still liegen. Am 2026-09-07 auf alpha gegen das echte AD gemessen (Lauf 14:42): zehn Fehlerzeilen unter den drei Zahlenzeilen, davon zwei Sorten. (1) Vier Konten scheitern mit der woertlichen Prisma-Meldung 'Invalid prisma.user.create() invocation: Unique constraint failed on the fields: (email)' — CN=uvertrieb_ro, uvertrieb_rw, uvertrieb_ro_ss, usoftware_rw aus OU=CTL_PWS_Gruppen teilen sich offenbar eine E-Mail-Adresse. Sie werden dadurch NIE importiert, ohne dass der Administrator erfaehrt warum oder was er tun soll. (2) Sechs Eintraege melden englisch 'no username mapped (check sAMAccountName mapping)' — korrekt uebersprungene Kontakte/Ressourcen ohne sAMAccountName, aber die Meldung liest sich wie ein Fehler und ist nicht uebersetzt. Beides braucht eine verstaendliche deutsche Meldung; die E-Mail-Kollision zusaetzlich eine Entscheidung, ob solche Konten ohne E-Mail angelegt oder bewusst uebersprungen werden. Beleg: .planning/phases/16-ad-gruppen-synchronisation/uat-2026-09-07/windows6a-sync-zahlenzeilen.png fixed 2026-09-07T12:47:32.058Z 2026-09-09T06:25:18.145Z
16 14 unmet-truth apps/api/src/tenders/tenders.controller.ts Das Postfach im Ausschreibungs-Radar hat keinen Verbindungstest, obwohl die Faehigkeit fertig vorliegt. Beide Inbox-Provider bringen testConnection() mit (imap.provider.ts:343, exchange-inbox.provider.ts:294), und das DKV-Modul nutzt sie ueber POST /dkv/test-connection samt Knopf 'Verbindung testen' im InboxConfigForm. Beim Ausschreibungs-Radar fehlt beides: der Controller kennt zu email-config nur GET (:343) und PUT (:357), kein Test-Endpunkt, und das Formular unter Meine Quellen hat keinen Knopf. Historie geprueft: der Knopf war nie vorhanden (git log -S testConnection im tender-radar-Frontend ist leer), es ist also eine Luecke, keine Regression. Folge fuer den Betrieb: ein Tippfehler in der EWS-Endpunkt-URL oder falsche Zugangsdaten fallen erst auf, wenn dauerhaft nichts ankommt — und dann ist nicht unterscheidbar, ob die Verbindung scheitert oder schlicht keine Alarm-Mail da war. Das trifft besonders WINDOWS #12, dessen ganzer Zweck der Beleg des handgeschriebenen NTLM/SOAP-Wegs ist. Aufgefallen am 2026-09-07 beim Einrichten des Postfachs. fixed 2026-09-07T13:22:33.916Z 2026-09-09T05:20:09.851Z
17 6 unmet-truth docker-compose.yml Hochgeladene Dateien ueberleben kein Neuerstellen der Container. Der Code legt sie unter user-files/ ab (user.controller.ts:40 und :266 fuer Profilbilder, dazu die DKV-Exporte), aber KEINE der Compose-Dateien mountet dieses Verzeichnis — weder im Repository (docker-compose.yml, .prod.yml, .dev.yml haben nur das Volume pgdata) noch in der abweichenden Datei auf dem Server /opt/tessera/docker-compose.yml. Am 2026-09-09 gemessen: 'docker inspect' auf tessera-api-1 meldet ueberhaupt keinen Mount, /app/user-files liegt damit nur in der beschreibbaren Container-Schicht und ist bei jedem 'up -d --force-recreate' weg. Aufgefallen beim Schreiben des Betriebshandbuchs. KEIN Schaden entstanden: aktuell hat kein Nutzer ein Profilbild hinterlegt (avatarPath ueberall NULL), und die bisherigen Neuerstellungen trafen einen leeren Ordner. Die Luecke schlaegt zu, sobald der erste Nutzer ein Bild hochlaedt oder ein DKV-Export aufgehoben werden soll. Behebung: ein benanntes Volume oder Bind-Mount fuer user-files in beiden Compose-Dateien; die Server-Datei muss zusaetzlich von Hand ergaenzt werden, weil sie vom Repository abweicht. fixed 2026-09-09T06:42:22.801Z 2026-09-09T08:22:51.235Z
18 2 unmet-truth docker-compose.yml Die Mandantentrennung auf Datenbankebene ist wirkungslos, weil die Anwendungsrolle sie umgeht. Die API verbindet laut docker-compose.yml:33 als Rolle 'tessera'; diese Rolle hat auf alpha rolsuper=t UND rolbypassrls=t. PostgreSQL wendet Row-Level-Security auf solche Rollen grundsaetzlich nicht an — auch FORCE ROW LEVEL SECURITY aendert daran nichts, das erzwingt nur die Anwendung auf den Tabelleneigentuemer, nicht auf BYPASSRLS-Rollen. Am 2026-09-09 praktisch gemessen: ohne gesetztes app.current_tenant liefert 'SELECT count(*) FROM "Group"' zwei Zeilen, waehrend die Policy USING ("tenantId" = current_tenant_id()) bei NULL-Kontext null Zeilen liefern muesste. Damit sind alle sieben bisher mit RLS ausgestatteten Tabellen (User, Group, GroupMembership, LdapConfig, LdapFieldMapping, ModuleGrant, PasswordResetToken) faktisch ungeschuetzt; die Trennung haengt allein am manuellen 'where tenantId' im Anwendungscode. Die Migration 20260804130918 nennt RLS ausdruecklich 'ein zweites Sicherheitsnetz' — dieses Netz existiert derzeit nicht. Reihenfolge der Behebung: ZUERST eine eigene Anwendungsrolle ohne Superuser- und BYPASSRLS-Recht einrichten und die Anwendung darauf umstellen, DANN greifen die vorhandenen Policies, und ERST DANN lohnt es, fehlende Tabellen zu ergaenzen. Vorher gebaute Policies waeren wirkungslos und wuerden eine Sicherheit vortaeuschen. Kein akutes Risiko, solange Tessera nur intern und einmandantig laeuft (ein einziger Mandant 'default'), aber vor jedem Kundeneinsatz zwingend. NACHTRAG (260909-eor, Aufgabe 3): dieser Plan hat den fuer die Umstellung noetigen Anwendungscode klassifiziert (docs/mandantentrennung-zugriffsklassifikation.md, 227 Fundstellen / 59 Datei-Modell-Paare) und zusaetzlich einen weiteren, beim Anlegen dieses Eintrags noch nicht bekannten Defekt gefunden und behoben: forTenant() setzte den Mandantenkontext auf einer anderen Datenbankverbindung als die eigentliche Abfrage lief (siehe #20). #20 bleibt trotz nachgewiesener Reparatur bewusst OPEN, an dieselbe Bedingung gebunden wie dieser Eintrag — die Wirkung unter der echten Rolle ist erst nach dem Scharfschalten beobachtbar. open 2026-09-09T07:42:13.878Z
19 2 unmet-truth apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql Zwei der neuen Policies wuerden plattformweite Zeilen unsichtbar machen, sobald die Mandantentrennung scharf geschaltet wird. SearchProvider und TenderRssFeedSource haben ein nullable tenantId: Zeilen mit tenantId = NULL gelten fuer alle Mandanten (die von der Administration gepflegten Feeds und Suchanbieter). Die einfache Policy 'tenantId = current_tenant_id()' vergleicht NULL niemals gleich, diese Zeilen waeren nach der Aktivierung fuer JEDEN Mandanten weg — nicht nur fuer fremde. Heute ohne Wirkung, weil die Anwendung weiter als BYPASSRLS-Rolle verbindet (#18, Schalter bewusst aus). Beim Scharfschalten zwingend mitzuloesen, zusammen mit den 182 unskalierten Zugriffen: die Policy muss die plattformweiten Zeilen ausdruecklich einschliessen, etwa ueber 'tenantId IS NULL OR tenantId = current_tenant_id()' fuer den Lesezugriff, waehrend Schreibzugriffe weiterhin einen Mandanten verlangen. Beim Schreiben der Migration am 2026-09-09 aufgefallen und bewusst nicht eigenmaechtig anders geloest, weil die richtige Semantik eine Produktentscheidung ist. NACHTRAG (260909-eor, Aufgabe 3): docs/mandantentrennung-zugriffsklassifikation.md haelt diesen Befund im Abschnitt 'Zwei belegte Befunde' fest und benennt ihn als Blocker fuer Etappe 3. Die '182 unskalierten Zugriffe' sind ueberholt — die aktuelle, maschinell geprüfte Zahl ist 227 Fundstellen (59 Datei-Modell-Paare, siehe Klassifikationsdokument). open 2026-09-09T08:08:19.293Z
20 2 unmet-truth apps/api/src/prisma/prisma-tenant.extension.ts forTenant() setzt den Mandantenkontext auf einer anderen Verbindung als die Abfrage laeuft — die Mandantentrennung hat damit nie funktioniert, auch nicht dort, wo sie scheinbar benutzt wird. Die Erweiterung oeffnet prisma.$transaction, setzt app.current_tenant per set_config(..., true) auf tx, ruft dann aber query(args) auf, das ueber den AEUSSEREN Client dispatcht. set_config mit local=true gilt nur in der Transaktion und nur auf deren Verbindung. Am 2026-09-09 gegen die lokale Datenbank reproduziert: set_config landete auf Backend-PID 254999, die eigentliche Abfrage auf 255000, und dort war current_setting('app.current_tenant') NULL. Heute ohne sichtbare Folge, weil die Anwendungsrolle BYPASSRLS hat (#18) und deshalb ohnehin alles sieht. NACH dem Scharfschalten kehrt sich das um: die betroffenen Abfragen liefern dann NULL ZEILEN statt zu vieler. Besonders gefaehrlich in ldap.service.ts (Loeschzweig um Zeile 1559): der Sync deutet die Leere als 'Gruppe im Verzeichnis verschwunden' und loescht sie samt Mitgliedschaften und Modulfreigaben — aus einem stillen Trennungsfehler wuerde stiller Datenverlust. Zusatzbefund: von den 36 vermeintlichen forTenant-Vorkommen sind die meisten Kommentare, die erklaeren, warum forTenant FEHLT; echte Aufrufstellen sind 6, echte mandantengebundene Abfragen 9, alle in ldap.service.ts. Ausserdem setzen tenant.middleware.ts:44 und tenant.guard.ts:41 ein req.tenantPrisma, das in apps/api/src von NIEMANDEM gelesen wird. Muss vor jedem weiteren Umbau repariert werden, sonst baut alles Weitere auf einem Helfer auf, der nicht traegt. NACHTRAG (260909-eor, Aufgabe 1/4): der beschriebene Verbindungsfehler ist behoben (Array-Form von $transaction, prisma-tenant.extension.ts) und gegen eine Wegwerf-Datenbank mit einer Rolle ohne BYPASSRLS live nachgewiesen (rls-scratch-check.mjs, 8/8 Pruefungen bestanden). Bleibt dennoch bewusst OPEN, nicht fixed: die Wirkung unter der echten Anwendungsrolle tessera_app ist erst nach dem Scharfschalten (#18) beobachtbar — bis dahin bleibt #20 an dieselbe Bedingung gebunden wie #18 und #19. open 2026-09-09T08:44:18.496Z
21 2 deviation apps/api/src/dkv/dkv-scheduler.service.ts DKV-Planer-Startpfad (DkvSchedulerService.onModuleInit -> DkvService.loadAnyActiveConfigForScheduler, vormals loadConfig() ohne Mandant) bleibt bewusst UNGEBUNDEN, als benannte Altlast aus 07-04 (260909-mir, Befund D). Zwei Zustaende, beide gehoeren genannt: HEUTE bereits falsch -- findFirst() ohne jede Bedingung zieht bei mehreren Mandanten EINEN beliebigen und bedient die uebrigen NIE (isActive-Pruefung kann den Planer sogar ganz leer laufen lassen, wenn ausgerechnet die gezogene Zeile inaktiv ist, obwohl ein zweiter Mandant aktiv waere). NACH DEM SCHARFSCHALTEN (#18) verstummt sie zusaetzlich -- dieselbe Abfrage liefert dann null, der Planer protokolliert 'no active config found' und richtet fuer JEDEN Mandanten nichts ein, ohne Alarm. Drei erwogene Formen prufen: (a) an einen konkret aufgeloesten Mandanten binden -- nicht moeglich, onModuleInit() hat beim Boot strukturell keinen Mandantenkontext. (b) Umbau auf einmal-abfragen-viele-bedienen -- abgelehnt, das ist die in 07-04 zurueckgestellte Mehrmandanten-Planung (neue Auftragsverwaltung je Mandant statt des heutigen setInterval() mit GENAU EINEM Auftrag) und damit eine Funktionsaenderung, kein Bindungsumbau. (c) Als benannte Altlast weiterfuehren, mit Markierung -- GEWAEHLT, Praezedenzfall LdapConfigService.getAllActiveConfigs() (260909-ipc, Befund B). Die Unsymmetrie zu diesem Praezedenzfall: getAllActiveConfigs ist HEUTE korrekt und verstummt erst spaeter: der DKV-Planer ist HEUTE bereits falsch UND verstummt zusaetzlich spaeter. Markierung dreifach: eigene benannte Methode loadAnyActiveConfigForScheduler() mit Kopfkommentar (dkv.service.ts), fortgeschriebener Kopfkommentar in dkv-scheduler.service.ts, Abschnitt (d4) in docs/mandantentrennung-etappe2-fehlerrichtung.md. Signal fuer das Verstummen gehoert in die Vorabpruefung von Etappe 4 (rls-preflight.mjs), NICHT in diesen Durchlauf. open 2026-09-09T14:41:07.256Z
[
  {
    "id": 1,
    "kind": "unrun-verify",
    "phase": "15",
    "file": ".planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-06-PLAN.md",
    "line": null,
    "description": "15-06 <verification>: manueller Browser-Durchklick (Anlegen/Umbenennen/Standardmarkierung/AD-Bindung/Mitglieder/Loeschdialog + Fehlerpfade bei abgeschalteter API) nicht ausgefuehrt — kein Browser-Tool in dieser Session verfuegbar",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-08-04T17:05:42.026Z",
    "resolved_at": "2026-09-07T08:03:50.832Z"
  },
  {
    "id": 2,
    "kind": "unrun-verify",
    "phase": "15",
    "file": ".planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-07-PLAN.md",
    "line": null,
    "description": "Manueller Browser-Durchklick aus dem Plan-Verification-Block (Matrix-Freigabe setzen/entziehen, Aktivierungsdialog beide Wege, Direkt-Grant neben Gruppen-Grant, Fehlerfall bei gestoppter API, lange Namen) nicht ausgefuehrt -- kein Browser-Tool in dieser Session (15-07-SUMMARY.md D4)",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-08-04T17:27:42.267Z",
    "resolved_at": "2026-09-07T08:03:51.067Z"
  },
  {
    "id": 3,
    "kind": "unrun-verify",
    "phase": "15",
    "file": ".planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-08-PLAN.md",
    "line": null,
    "description": "Manueller Browser-Durchklick aus dem Plan-Verification-Block (USER ohne Freigabe: Sidebar/403-Seite/Marketplace-Badge+Toast/API-403 identisch; ADMIN: alle vier Ebenen zugaenglich) nicht ausgefuehrt - kein Browser-Tool in dieser Session verfuegbar.",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-08-04T17:50:13.320Z",
    "resolved_at": "2026-09-07T08:03:51.288Z"
  },
  {
    "id": 4,
    "kind": "unrun-verify",
    "phase": "16",
    "file": ".planning/phases/16-ad-gruppen-synchronisation/16-03-PLAN.md",
    "line": null,
    "description": "16-03 Task 2 human-check: RESEARCH.md Annahmen A1 (objectGUID uebersteht AD-Umbenennung) und A2 (byteweise Hex-Filter-Syntax) sind gegen kein echtes Active Directory geprueft — kein erreichbares AD in dieser Sandbox. Negatives Ergebnis bei A1 oder A2 ist Stopp-Grund fuer die Loeschsemantik (D-05).",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-08-06T14:21:59.987Z",
    "resolved_at": "2026-09-07T13:10:51.060Z"
  },
  {
    "id": 5,
    "kind": "unrun-verify",
    "phase": "16",
    "file": ".planning/phases/16-ad-gruppen-synchronisation/16-04-PLAN.md",
    "line": null,
    "description": "Manueller Browser-Durchklick aus dem Plan-Verification-Block (drei GroupFormModal-Zustaende: Anlegen mit Hinweis-Link, lokale Gruppe umbenennen, importierte Gruppe mit gesperrtem Namen + internem Namen speichern; Namensanzeige mit Tooltip in der Liste nach dem Speichern) nicht ausgefuehrt — kein Browser-Tool in dieser Session verfuegbar.",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-08-06T14:31:14.336Z",
    "resolved_at": "2026-09-07T08:03:51.517Z"
  },
  {
    "id": 6,
    "kind": "unrun-verify",
    "phase": "16",
    "file": ".planning/phases/16-ad-gruppen-synchronisation/16-05-PLAN.md",
    "line": null,
    "description": "16-05 <verification> Manuell nachzuholen (Browser): Sync ausloesen und pruefen, dass alle drei Zahlenzeilen erscheinen; Lauf mit verschobener Standardmarkierung provozieren und Amber-Zeile pruefen; Freigabe-Matrix-Spaltensuche unter beiden Namen probieren - nicht ausgefuehrt, kein Browser-Tool in dieser Session verfuegbar.",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-08-06T14:39:03.905Z",
    "resolved_at": "2026-09-07T13:10:51.272Z"
  },
  {
    "id": 7,
    "kind": "unrun-verify",
    "phase": "17",
    "file": ".planning/phases/17-eigene-ausschreibungs-quellen-je-nutzer/17-01-PLAN.md",
    "line": null,
    "description": "17-01 Task 2 human-check: Browser-Gegenprobe (normaler Nutzer oeffnet /modules/tender-radar/my-sources, speichert Postfach, zweites Konto desselben Mandanten sieht leeres Formular) nicht ausgefuehrt — kein Browser-Tool in dieser Session verfuegbar. Automatisierte Pruefungen (prisma validate/migrate status, Index-Liste, beide Typpruefungen, 335/335 src/tenders-Tests) sind gelaufen und gruen.",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-08-12T09:24:35.289Z",
    "resolved_at": "2026-09-07T07:50:56.468Z"
  },
  {
    "id": 8,
    "kind": "unrun-verify",
    "phase": "17",
    "file": "apps/web/src/app/(portal)/modules/tender-radar/my-sources/page.tsx",
    "line": null,
    "description": "Browser-Gegenprobe Plan 17-03 Task 1 (Tracer-Feedback-Gate): Meine Quellen mit drei Abschnitten, neu angelegter eigener Feed sofort sichtbar, plattformweiter Feed ohne Entfernen-Knopf, Digest-Intervall speichert nach Reload — kein Browser-Tool in dieser Sitzung verfuegbar",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-08-12T10:02:49.122Z",
    "resolved_at": "2026-09-07T07:50:56.693Z"
  },
  {
    "id": 9,
    "kind": "unrun-verify",
    "phase": "17",
    "file": "apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx",
    "line": null,
    "description": "Browser-Gegenprobe Plan 17-03 Task 2: USER-Konto sieht keine Bedienelemente auf /settings (nur Hinweis+Verweis), ADMIN-Konto sieht Abrufintervall+plattformweite Feeds ohne Postfach/Benachrichtigung, Zahnrad fuehrt in beiden Faellen nach Meine Quellen — kein Browser-Tool in dieser Sitzung verfuegbar",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-08-12T10:02:49.291Z",
    "resolved_at": "2026-09-07T07:50:56.910Z"
  },
  {
    "id": 10,
    "kind": "unmet-truth",
    "phase": "15",
    "file": "apps/web/src/app/(portal)/modules/tender-radar/page.tsx",
    "line": null,
    "description": "PERM-04 greift nicht auf modul-eigenen Routen: der Zugriffs-Guard sitzt nur in modules/[category]/[moduleSlug]/page.tsx. Die vier fest verdrahteten Modulrouten (tender-radar, dkv-fleet, cert-manager, domaincheck) samt Unterseiten (z.B. tender-radar/my-sources) laufen daran vorbei und rendern fuer einen USER OHNE Modulfreigabe vollstaendig, inklusive bedienbarer Knoepfe. Im Browser gemessen am 2026-09-07 mit nutzer2 (keine Freigabe): /modules/procurement/tender-radar zeigt korrekt die 403-Seite, /modules/tender-radar und /modules/tender-radar/my-sources und /modules/dkv-fleet zeigen die volle Seite. Keine Datenpreisgabe - die API antwortet auf allen Endpunkten mit 403 - aber der Nutzer sieht Bedienelemente, die er nicht benutzen darf, und bekommt rohe englische Techniktexte statt einer verstaendlichen Meldung. Beleg: .planning/phases/17-eigene-ausschreibungs-quellen-je-nutzer/uat-2026-09-07/befund-modul-eigene-route-ohne-freigabe.png",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-09-07T08:03:45.978Z",
    "resolved_at": "2026-09-07T08:40:04.951Z"
  },
  {
    "id": 11,
    "kind": "unmet-truth",
    "phase": "13",
    "file": "apps/api/src/tenders/tender-fingerprint.ts",
    "line": null,
    "description": "Cross-Source-Dedup kollabiert die Mehrzahl echter Duplikate nicht: tenderFingerprint() haengt [buyerName, title, cpvDivisionKey, deadlineKey, valueBucket] zu einem String und hasht ihn. Die Scraper-Adapter liefern cpvDivisions immer leer, DOE dagegen meist gefuellt - dieselbe Ausschreibung ergibt aus beiden Quellen verschiedene Fingerabdruecke, die Fingerprint-Stufe greift also gerade dort nicht, wo sie greifen soll. Steht als offener Rest in 13-VERIFICATION.md (status gaps_found, 3/4 Wahrheiten), war aber bisher nicht im Ledger und damit unsichtbar. Die frueher vermutete Ursache (Normalizer-Defaults) ist mit 9881005 geschlossen und nicht gemeint.",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-09-07T08:09:04.188Z",
    "resolved_at": "2026-09-07T08:42:34.794Z"
  },
  {
    "id": 12,
    "kind": "unrun-verify",
    "phase": "14",
    "file": ".planning/phases/14-rss-email-alert-ingestion-module-rollout/14-03-PLAN.md",
    "line": null,
    "description": "14-03 Task 4 (checkpoint human-verify, gate blocking): echtes Portal-Alert-Postfach ueber den Exchange/EWS-Weg anbinden und eine echte Alarm-Mail ingestieren. Zu belegen sind (a) die Mail erzeugt eine mandantenprivate Ausschreibung, (b) ein anderer Mandant sieht sie nicht, (c) der EWS-Body-Abruf liefert nicht-leeren, nicht-verstuemmelten Inhalt gegen einen echten Exchange-Server. Der handgeschriebene NTLM/SOAP-Weg ist nur gegen gemocktes httpntlm.post geprueft, nie gegen einen echten Server. Braucht ein echtes Postfach - vom User am 2026-07-24 bewusst zurueckgestellt. Stand bisher nur in 14-VERIFICATION.md (status human_needed), nicht im Ledger.",
    "status": "waived",
    "reason": "Zurueckgestellt auf Entscheidung des Users vom 2026-09-09: es gibt intern derzeit KEIN Postfach, in das Ausschreibungs-Alarme hereinkommen — die Voraussetzung des Pruefpunkts existiert nicht. Ein frueheres Missverstaendnis hatte ein solches Postfach angenommen. Nachzuholen, sobald ein Alarm-Postfach eingerichtet ist: dann sind (a) mandantenprivate Ausschreibung aus der Mail, (b) Unsichtbarkeit fuer andere Mandanten und (c) nicht-leerer, nicht-verstuemmelter Nachrichtentext zu belegen. Teilentlastung vom 2026-09-09: der handgeschriebene NTLM/EWS-Weg wurde beim Test von #16 erstmals gegen den echten Exchange owa.ctl.de gemessen — Anmeldung, FindFolder und Ordneraufloesung funktionieren, belegt durch die echte ServerVersionInfo-Antwort (Exchange 15.2) im API-Protokoll. Der frueher als fragil markierte Verbindungsaufbau ist damit nicht mehr ungeprueft; offen bleibt allein das Einlesen einer echten Alarm-Mail.",
    "recorded_at": "2026-09-07T08:09:04.406Z",
    "resolved_at": "2026-09-09T05:20:34.736Z"
  },
  {
    "id": 13,
    "kind": "unmet-truth",
    "phase": "1",
    "file": "apps/web/src/app/globals.css",
    "line": null,
    "description": "Tailwind-4-Dunkelvariante war nie an die .dark-Klasse gebunden: globals.css definierte die Farbtokens unter .dark, deklarierte aber kein @custom-variant dark. In Tailwind 4 haengt die dark:-Utility-Variante per Vorgabe an prefers-color-scheme, nicht an einer Klasse. Die App schaltet den Modus jedoch ueber next-themes mit attribute=class. Folge: JEDE dark:-Utility im Web-Quellcode (ueber 100 Vorkommen in 10+ Dateien - Status-, Warn- und Fehlerfarben, Hinweisboxen, Badges in der Administration) blieb wirkungslos, sobald der Nutzer im Portal auf dunkel stellte, waehrend sein Betriebssystem hell stand. Am laufenden System gemessen: html.dark gesetzt, prefers-color-scheme hell, dark:bg-gray-800 ergab transparent und dark:text-green-400 blieb ohne Wirkung. Unentdeckt geblieben, weil Hintergrund und Textfarbe ueber CSS-Variablen unter .dark laufen und deshalb korrekt umschalten. Aufgedeckt beim Logo-Einbau 260907-fgv, dessen Plattenkontur dark:stroke-white/25 aus demselben Grund nicht griff.",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-09-07T09:32:24.960Z",
    "resolved_at": "2026-09-07T09:35:49.019Z"
  },
  {
    "id": 14,
    "kind": "unmet-truth",
    "phase": "15",
    "file": "apps/web/src/app/(portal)/admin/modules/grants/page.tsx",
    "line": null,
    "description": "Die Suche in der Freigaben-Matrix macht die Matrix unbenutzbar, sobald sie etwas findet: derselbe Suchbegriff filtert BEIDE Achsen unabhaengig voneinander (page.tsx:137 filtert Module ueber m.name, page.tsx:145 filtert Gruppen ueber internalName/name). Ein Begriff, der nur eine Achse trifft, leert die andere vollstaendig — es bleibt nie ein Kaestchen zum Klicken uebrig. Am 2026-09-07 im Browser auf alpha gemessen: Suche 'Claude_VT' bzw. 'Vertrieb' laesst die Gruppenspalte stehen, entfernt aber jede Modulzeile; Suche 'Cert' laesst die Modulzeile stehen, entfernt aber jede Gruppenspalte. Damit scheitert genau der Zweck der Suche — in einer grossen Matrix die Kreuzung Modul x Gruppe finden. Belege: .planning/phases/16-ad-gruppen-synchronisation/uat-2026-09-07/befund-matrix-suche-gruppenname.png und befund-matrix-suche-modulname.png",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-09-07T12:46:56.172Z",
    "resolved_at": "2026-09-09T06:25:17.928Z"
  },
  {
    "id": 15,
    "kind": "unmet-truth",
    "phase": "16",
    "file": "apps/api/src/ldap/ldap.service.ts",
    "line": null,
    "description": "Der Sync reicht rohe Techniktexte an den Administrator durch und laesst echte AD-Konten still liegen. Am 2026-09-07 auf alpha gegen das echte AD gemessen (Lauf 14:42): zehn Fehlerzeilen unter den drei Zahlenzeilen, davon zwei Sorten. (1) Vier Konten scheitern mit der woertlichen Prisma-Meldung 'Invalid prisma.user.create() invocation: Unique constraint failed on the fields: (email)' — CN=uvertrieb_ro, uvertrieb_rw, uvertrieb_ro_ss, usoftware_rw aus OU=CTL_PWS_Gruppen teilen sich offenbar eine E-Mail-Adresse. Sie werden dadurch NIE importiert, ohne dass der Administrator erfaehrt warum oder was er tun soll. (2) Sechs Eintraege melden englisch 'no username mapped (check sAMAccountName mapping)' — korrekt uebersprungene Kontakte/Ressourcen ohne sAMAccountName, aber die Meldung liest sich wie ein Fehler und ist nicht uebersetzt. Beides braucht eine verstaendliche deutsche Meldung; die E-Mail-Kollision zusaetzlich eine Entscheidung, ob solche Konten ohne E-Mail angelegt oder bewusst uebersprungen werden. Beleg: .planning/phases/16-ad-gruppen-synchronisation/uat-2026-09-07/windows6a-sync-zahlenzeilen.png",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-09-07T12:47:32.058Z",
    "resolved_at": "2026-09-09T06:25:18.145Z"
  },
  {
    "id": 16,
    "kind": "unmet-truth",
    "phase": "14",
    "file": "apps/api/src/tenders/tenders.controller.ts",
    "line": null,
    "description": "Das Postfach im Ausschreibungs-Radar hat keinen Verbindungstest, obwohl die Faehigkeit fertig vorliegt. Beide Inbox-Provider bringen testConnection() mit (imap.provider.ts:343, exchange-inbox.provider.ts:294), und das DKV-Modul nutzt sie ueber POST /dkv/test-connection samt Knopf 'Verbindung testen' im InboxConfigForm. Beim Ausschreibungs-Radar fehlt beides: der Controller kennt zu email-config nur GET (:343) und PUT (:357), kein Test-Endpunkt, und das Formular unter Meine Quellen hat keinen Knopf. Historie geprueft: der Knopf war nie vorhanden (git log -S testConnection im tender-radar-Frontend ist leer), es ist also eine Luecke, keine Regression. Folge fuer den Betrieb: ein Tippfehler in der EWS-Endpunkt-URL oder falsche Zugangsdaten fallen erst auf, wenn dauerhaft nichts ankommt — und dann ist nicht unterscheidbar, ob die Verbindung scheitert oder schlicht keine Alarm-Mail da war. Das trifft besonders WINDOWS #12, dessen ganzer Zweck der Beleg des handgeschriebenen NTLM/SOAP-Wegs ist. Aufgefallen am 2026-09-07 beim Einrichten des Postfachs.",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-09-07T13:22:33.916Z",
    "resolved_at": "2026-09-09T05:20:09.851Z"
  },
  {
    "id": 17,
    "kind": "unmet-truth",
    "phase": "6",
    "file": "docker-compose.yml",
    "line": null,
    "description": "Hochgeladene Dateien ueberleben kein Neuerstellen der Container. Der Code legt sie unter user-files/ ab (user.controller.ts:40 und :266 fuer Profilbilder, dazu die DKV-Exporte), aber KEINE der Compose-Dateien mountet dieses Verzeichnis — weder im Repository (docker-compose.yml, .prod.yml, .dev.yml haben nur das Volume pgdata) noch in der abweichenden Datei auf dem Server /opt/tessera/docker-compose.yml. Am 2026-09-09 gemessen: 'docker inspect' auf tessera-api-1 meldet ueberhaupt keinen Mount, /app/user-files liegt damit nur in der beschreibbaren Container-Schicht und ist bei jedem 'up -d --force-recreate' weg. Aufgefallen beim Schreiben des Betriebshandbuchs. KEIN Schaden entstanden: aktuell hat kein Nutzer ein Profilbild hinterlegt (avatarPath ueberall NULL), und die bisherigen Neuerstellungen trafen einen leeren Ordner. Die Luecke schlaegt zu, sobald der erste Nutzer ein Bild hochlaedt oder ein DKV-Export aufgehoben werden soll. Behebung: ein benanntes Volume oder Bind-Mount fuer user-files in beiden Compose-Dateien; die Server-Datei muss zusaetzlich von Hand ergaenzt werden, weil sie vom Repository abweicht.",
    "status": "fixed",
    "reason": "",
    "recorded_at": "2026-09-09T06:42:22.801Z",
    "resolved_at": "2026-09-09T08:22:51.235Z"
  },
  {
    "id": 18,
    "kind": "unmet-truth",
    "phase": "2",
    "file": "docker-compose.yml",
    "line": null,
    "description": "Die Mandantentrennung auf Datenbankebene ist wirkungslos, weil die Anwendungsrolle sie umgeht. Die API verbindet laut docker-compose.yml:33 als Rolle 'tessera'; diese Rolle hat auf alpha rolsuper=t UND rolbypassrls=t. PostgreSQL wendet Row-Level-Security auf solche Rollen grundsaetzlich nicht an — auch FORCE ROW LEVEL SECURITY aendert daran nichts, das erzwingt nur die Anwendung auf den Tabelleneigentuemer, nicht auf BYPASSRLS-Rollen. Am 2026-09-09 praktisch gemessen: ohne gesetztes app.current_tenant liefert 'SELECT count(*) FROM \"Group\"' zwei Zeilen, waehrend die Policy USING (\"tenantId\" = current_tenant_id()) bei NULL-Kontext null Zeilen liefern muesste. Damit sind alle sieben bisher mit RLS ausgestatteten Tabellen (User, Group, GroupMembership, LdapConfig, LdapFieldMapping, ModuleGrant, PasswordResetToken) faktisch ungeschuetzt; die Trennung haengt allein am manuellen 'where tenantId' im Anwendungscode. Die Migration 20260804130918 nennt RLS ausdruecklich 'ein zweites Sicherheitsnetz' — dieses Netz existiert derzeit nicht. Reihenfolge der Behebung: ZUERST eine eigene Anwendungsrolle ohne Superuser- und BYPASSRLS-Recht einrichten und die Anwendung darauf umstellen, DANN greifen die vorhandenen Policies, und ERST DANN lohnt es, fehlende Tabellen zu ergaenzen. Vorher gebaute Policies waeren wirkungslos und wuerden eine Sicherheit vortaeuschen. Kein akutes Risiko, solange Tessera nur intern und einmandantig laeuft (ein einziger Mandant 'default'), aber vor jedem Kundeneinsatz zwingend. NACHTRAG (260909-eor, Aufgabe 3): dieser Plan hat den fuer die Umstellung noetigen Anwendungscode klassifiziert (docs/mandantentrennung-zugriffsklassifikation.md, 227 Fundstellen / 59 Datei-Modell-Paare) und zusaetzlich einen weiteren, beim Anlegen dieses Eintrags noch nicht bekannten Defekt gefunden und behoben: forTenant() setzte den Mandantenkontext auf einer anderen Datenbankverbindung als die eigentliche Abfrage lief (siehe #20). #20 bleibt trotz nachgewiesener Reparatur bewusst OPEN, an dieselbe Bedingung gebunden wie dieser Eintrag — die Wirkung unter der echten Rolle ist erst nach dem Scharfschalten beobachtbar.",
    "status": "open",
    "reason": "",
    "recorded_at": "2026-09-09T07:42:13.878Z",
    "resolved_at": null
  },
  {
    "id": 19,
    "kind": "unmet-truth",
    "phase": "2",
    "file": "apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql",
    "line": null,
    "description": "Zwei der neuen Policies wuerden plattformweite Zeilen unsichtbar machen, sobald die Mandantentrennung scharf geschaltet wird. SearchProvider und TenderRssFeedSource haben ein nullable tenantId: Zeilen mit tenantId = NULL gelten fuer alle Mandanten (die von der Administration gepflegten Feeds und Suchanbieter). Die einfache Policy 'tenantId = current_tenant_id()' vergleicht NULL niemals gleich, diese Zeilen waeren nach der Aktivierung fuer JEDEN Mandanten weg — nicht nur fuer fremde. Heute ohne Wirkung, weil die Anwendung weiter als BYPASSRLS-Rolle verbindet (#18, Schalter bewusst aus). Beim Scharfschalten zwingend mitzuloesen, zusammen mit den 182 unskalierten Zugriffen: die Policy muss die plattformweiten Zeilen ausdruecklich einschliessen, etwa ueber 'tenantId IS NULL OR tenantId = current_tenant_id()' fuer den Lesezugriff, waehrend Schreibzugriffe weiterhin einen Mandanten verlangen. Beim Schreiben der Migration am 2026-09-09 aufgefallen und bewusst nicht eigenmaechtig anders geloest, weil die richtige Semantik eine Produktentscheidung ist. NACHTRAG (260909-eor, Aufgabe 3): docs/mandantentrennung-zugriffsklassifikation.md haelt diesen Befund im Abschnitt 'Zwei belegte Befunde' fest und benennt ihn als Blocker fuer Etappe 3. Die '182 unskalierten Zugriffe' sind ueberholt — die aktuelle, maschinell geprüfte Zahl ist 227 Fundstellen (59 Datei-Modell-Paare, siehe Klassifikationsdokument).",
    "status": "open",
    "reason": "",
    "recorded_at": "2026-09-09T08:08:19.293Z",
    "resolved_at": null
  },
  {
    "id": 20,
    "kind": "unmet-truth",
    "phase": "2",
    "file": "apps/api/src/prisma/prisma-tenant.extension.ts",
    "line": null,
    "description": "forTenant() setzt den Mandantenkontext auf einer anderen Verbindung als die Abfrage laeuft — die Mandantentrennung hat damit nie funktioniert, auch nicht dort, wo sie scheinbar benutzt wird. Die Erweiterung oeffnet prisma.$transaction, setzt app.current_tenant per set_config(..., true) auf tx, ruft dann aber query(args) auf, das ueber den AEUSSEREN Client dispatcht. set_config mit local=true gilt nur in der Transaktion und nur auf deren Verbindung. Am 2026-09-09 gegen die lokale Datenbank reproduziert: set_config landete auf Backend-PID 254999, die eigentliche Abfrage auf 255000, und dort war current_setting('app.current_tenant') NULL. Heute ohne sichtbare Folge, weil die Anwendungsrolle BYPASSRLS hat (#18) und deshalb ohnehin alles sieht. NACH dem Scharfschalten kehrt sich das um: die betroffenen Abfragen liefern dann NULL ZEILEN statt zu vieler. Besonders gefaehrlich in ldap.service.ts (Loeschzweig um Zeile 1559): der Sync deutet die Leere als 'Gruppe im Verzeichnis verschwunden' und loescht sie samt Mitgliedschaften und Modulfreigaben — aus einem stillen Trennungsfehler wuerde stiller Datenverlust. Zusatzbefund: von den 36 vermeintlichen forTenant-Vorkommen sind die meisten Kommentare, die erklaeren, warum forTenant FEHLT; echte Aufrufstellen sind 6, echte mandantengebundene Abfragen 9, alle in ldap.service.ts. Ausserdem setzen tenant.middleware.ts:44 und tenant.guard.ts:41 ein req.tenantPrisma, das in apps/api/src von NIEMANDEM gelesen wird. Muss vor jedem weiteren Umbau repariert werden, sonst baut alles Weitere auf einem Helfer auf, der nicht traegt. NACHTRAG (260909-eor, Aufgabe 1/4): der beschriebene Verbindungsfehler ist behoben (Array-Form von $transaction, prisma-tenant.extension.ts) und gegen eine Wegwerf-Datenbank mit einer Rolle ohne BYPASSRLS live nachgewiesen (rls-scratch-check.mjs, 8/8 Pruefungen bestanden). Bleibt dennoch bewusst OPEN, nicht fixed: die Wirkung unter der echten Anwendungsrolle tessera_app ist erst nach dem Scharfschalten (#18) beobachtbar — bis dahin bleibt #20 an dieselbe Bedingung gebunden wie #18 und #19.",
    "status": "open",
    "reason": "",
    "recorded_at": "2026-09-09T08:44:18.496Z",
    "resolved_at": null
  },
  {
    "id": 21,
    "kind": "deviation",
    "phase": "2",
    "file": "apps/api/src/dkv/dkv-scheduler.service.ts",
    "line": null,
    "description": "DKV-Planer-Startpfad (DkvSchedulerService.onModuleInit -> DkvService.loadAnyActiveConfigForScheduler, vormals loadConfig() ohne Mandant) bleibt bewusst UNGEBUNDEN, als benannte Altlast aus 07-04 (260909-mir, Befund D). Zwei Zustaende, beide gehoeren genannt: HEUTE bereits falsch -- findFirst() ohne jede Bedingung zieht bei mehreren Mandanten EINEN beliebigen und bedient die uebrigen NIE (isActive-Pruefung kann den Planer sogar ganz leer laufen lassen, wenn ausgerechnet die gezogene Zeile inaktiv ist, obwohl ein zweiter Mandant aktiv waere). NACH DEM SCHARFSCHALTEN (#18) verstummt sie zusaetzlich -- dieselbe Abfrage liefert dann null, der Planer protokolliert 'no active config found' und richtet fuer JEDEN Mandanten nichts ein, ohne Alarm. Drei erwogene Formen prufen: (a) an einen konkret aufgeloesten Mandanten binden -- nicht moeglich, onModuleInit() hat beim Boot strukturell keinen Mandantenkontext. (b) Umbau auf einmal-abfragen-viele-bedienen -- abgelehnt, das ist die in 07-04 zurueckgestellte Mehrmandanten-Planung (neue Auftragsverwaltung je Mandant statt des heutigen setInterval() mit GENAU EINEM Auftrag) und damit eine Funktionsaenderung, kein Bindungsumbau. (c) Als benannte Altlast weiterfuehren, mit Markierung -- GEWAEHLT, Praezedenzfall LdapConfigService.getAllActiveConfigs() (260909-ipc, Befund B). Die Unsymmetrie zu diesem Praezedenzfall: getAllActiveConfigs ist HEUTE korrekt und verstummt erst spaeter: der DKV-Planer ist HEUTE bereits falsch UND verstummt zusaetzlich spaeter. Markierung dreifach: eigene benannte Methode loadAnyActiveConfigForScheduler() mit Kopfkommentar (dkv.service.ts), fortgeschriebener Kopfkommentar in dkv-scheduler.service.ts, Abschnitt (d4) in docs/mandantentrennung-etappe2-fehlerrichtung.md. Signal fuer das Verstummen gehoert in die Vorabpruefung von Etappe 4 (rls-preflight.mjs), NICHT in diesen Durchlauf.",
    "status": "open",
    "reason": "",
    "recorded_at": "2026-09-09T14:41:07.256Z",
    "resolved_at": null
  }
]