d0393ac4af
Empirisch reproduziert, nicht hergeleitet: set_config landete auf Backend-PID 254999, die eigentliche Abfrage auf 255000, dort war app.current_tenant NULL. Die Erweiterung setzt den Kontext auf tx, dispatcht die Abfrage aber ueber den aeusseren Client. Folge: die Mandantentrennung hat nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzen. Heute unsichtbar, weil die Rolle ohnehin BYPASSRLS hat (#18). Nach dem Scharfschalten kehrt es sich um — die Abfragen liefern dann null Zeilen, und der LDAP-Loeschzweig deutet das als "Gruppe im Verzeichnis verschwunden" und loescht sie samt Mitgliedschaften und Modulfreigaben. Zusatz: von 36 vermeintlichen forTenant-Vorkommen sind die meisten Kommentare, die dessen Fehlen erklaeren — echte Aufrufstellen sind 6. Und das von tenant.middleware.ts:44 und tenant.guard.ts:41 gesetzte req.tenantPrisma liest niemand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
39 KiB
39 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 | 3 | 1 | 16 | 20 | 2026-09-09T08:44:18.496Z |
Broken Windows Ledger
Cross-phase defect register. With
workflow.windows_enforceenabled,/gsd-shipblocks whileopen_count > 0. Waive withgsd-tools windows waive <id> "<reason>"(reason required). Mark fixed withgsd-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. | 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. | 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. | open | 2026-09-09T08:44:18.496Z |
[
{
"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.",
"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.",
"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.",
"status": "open",
"reason": "",
"recorded_at": "2026-09-09T08:44:18.496Z",
"resolved_at": null
}
]