docs(quick-260914-ebg): Ledger — WINDOWS #29 fixed, Nebenbefunde Biome-Konfiguration und stilles 403 im Frontend eingetragen
Tessera CI/CD / Lint & Type Check (push) Successful in 46s
Tessera CI/CD / Tests (push) Successful in 53s
Tessera CI/CD / Build & Publish Images (push) Successful in 30s

- #29 fixed: Zielrollen-Riegel schliesst die Rechteausweitung ADMIN gegen SUPER_ADMIN in UserController.update()/remove()
- #35 (deviation, biome.json): Biome im Bestand nicht lauffaehig — unbekannter Schluessel organizeImports, fehlender Parser-Schalter, CI-Lint-Schritt ein Leerlauf
- #36 (deviation, apps/web/.../admin/users/page.tsx): 403 wird im Frontend still verschluckt — seit #29 fuer einen ADMIN im Alltag erreichbar (SUPER_ADMIN-Zeile in der eigenen Liste)
- Frontmatter: open_count 16, waived_count 1, fixed_count 19, total_count 36

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
2026-09-14 10:38:29 +02:00
parent 63f9df0afb
commit 70d007bb47
+35 -7
View File
@@ -1,10 +1,10 @@
---
schema_version: 1
open_count: 15
open_count: 16
waived_count: 1
fixed_count: 18
total_count: 34
last_updated: 2026-09-11T15:46:08.295Z
fixed_count: 19
total_count: 36
last_updated: 2026-09-14T08:38:12.619Z
---
# Broken Windows Ledger
@@ -43,12 +43,14 @@ last_updated: 2026-09-11T15:46:08.295Z
| 26 | quick-260911-cwh | deviation | apps/web/src/components/dashboard/widgets/calendar-widget.tsx | | Bereich calendar: zu kleines Leseergebnis auf getSources/fetchAndCacheEvents sieht aus wie 'keine Quelle eingerichtet' bzw. 'keine Termine' (calendar-widget.tsx, calendar-settings-panel.tsx); das Frontend verschluckt zusaetzlich LAUTE Fehler derselben Pfade in denselben leeren Zustand (calendar-widget.tsx catch->setEvents([]), calendar-settings-panel.tsx .catch(()=>{}) auf fetchSources); der Nutzer legt seine Quelle neu an und tippt Exchange-/CalDAV-Zugangsdaten ein zweites Mal in ein scheinbar defektes System ein, die urspruengliche Zeile bleibt unsichtbar liegen und wird nach Behebung zur Dublette; Vorabpruefung fuer Etappe 4: physisch vorhandene CalendarSource-Zeilen je Mandant ueber die Wartungsrolle zaehlen und mit der gebundenen Zaehlung vergleichen (docs/mandantentrennung-etappe2-fehlerrichtung.md (k4)(e)); an dieselbe Bedingung gebunden wie WINDOWS #18; Familie mit #23 (module-registry) und #25 (dashboard); das Frontend wird von 260911-cwh NICHT geaendert. | open | | 2026-09-11T07:57:36.769Z | |
| 27 | 2 | unmet-truth | apps/api/src/prisma/rls-access-inventory.spec.ts | | Die maschinelle Bestandsaufnahme (rls-access-inventory.spec.ts) ist fuer Relationszugriffe strukturell blind. Sie erkennt nur direkte Zugriffe der Form this.prisma.<Modell> bzw. <gebundener Client>.<Modell>. Ein Zugriff, der ueber include:/_count:/select: in eine ZWEITE Tabelle hineinreicht, ist fuer sie unsichtbar — obwohl Prisma daraus eine Unterabfrage auf diese zweite Tabelle macht, die unter DEREN Regel laeuft. Nachgewiesen in 260911-e2s: drei Zugriffe in tenant.controller.ts zaehlten ueber include: { _count: { select: { users } } } in die geschuetzte Tabelle User hinein (Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)); nach dem Scharfschalten haette die Mandantenliste des Plattform-Administrators fuer jeden Mandanten 0 Benutzer gezeigt und der Loeschriegel T-02-09 waere vakuum geworden. Diese drei Stellen sind behoben (Fan-out je Mandant ueber gebundenen Client). Zur Planungszeit wurden alle 19 include:-Stellen und alle _count-Stellen in apps/api/src einzeln beurteilt, vom Orchestrator und vom Verifizierer unabhaengig gegengeprueft: nur diese drei waren gefaehrlich (tenders zaehlt auf dem plattformglobalen Katalog ohne Zeilenschutz, groups zaehlt ueber einen bereits gebundenen Client in eine Tabelle desselben Mandanten). OFFEN bleibt der MECHANISMUS: jede kuenftige include:/_count:-Stelle in eine fremd geschuetzte Tabelle bleibt fuer die Pruefung unsichtbar. Zu schliessen, indem der Detektor include:/select:/_count:-Bloecke auf Modellnamen durchsucht und die Zieltabelle als eigene Fundstelle fuehrt — oder durch eine Pruefung, die jede include:-Stelle einer expliziten Freigabeliste unterwirft. Gehoert vor das Scharfschalten (Etappe 4), weil die Vorabpruefung sich sonst auf eine Bestandsaufnahme stuetzt, die diese Form nicht sieht. | fixed | | 2026-09-11T09:08:00.435Z | 2026-09-11T14:48:15.447Z |
| 28 | quick-260911-fh9 | deviation | apps/web/src/components/layout/header.tsx | | Bereich auth: getMe liefert nach dem Scharfschalten null, der Controller antwortet 200 mit leerem Rumpf, fetchCurrentUser (auth-actions.ts) macht daraus null, header.tsx und account-settings-form.tsx tun bei null nichts — die Portalhuelle rendert ohne angemeldeten Benutzer; changePassword liest sich als networkError (nicht als falsches Kennwort); adminResetPassword als 'User not found' ohne UI-Aufrufer. 'nicht angemeldet' und 'Zeile unsichtbar' sind fuer das Frontend derselbe Wert — an dieselbe Bedingung gebunden wie WINDOWS #18; Familie #23/#25/#26; Etappe-4-Vorabpruefung: bekannten Benutzer ueber die Wartungsrolle lesen und den gebundenen findUnique unter seinem Claim-Mandanten daneben halten. Das Frontend wird von 260911-fh9 NICHT geaendert. | open | | 2026-09-11T10:00:29.558Z | |
| 29 | quick-260911-fh9 | unmet-truth | apps/api/src/user/user.controller.ts | | Bereich auth: adminResetPassword schliesst die Rechteausweitung ADMIN -> SUPER_ADMIN im eigenen Handler (T-FH9-04), der Schwesterweg PATCH /users/:id tut das nicht. UserController.update (T-02-08) prueft nur, ob die Rolle SUPER_ADMIN NEU ZUGEWIESEN wird (dto.role === Role.SUPER_ADMIN), nicht ob das ZIEL diese Rolle bereits HAT — password/isActive gehen fuer ein bestehendes SUPER_ADMIN-Ziel ungeprueft durch; remove prueft ueberhaupt keine Rolle des Ziels, nur Selbstloeschung und Mandantengrenze. Kein Mandantenproblem, sondern Rechteausweitung INNERHALB des Mandanten. Reparatur in einem Satz: die Rolle des ZIELS pruefen (ist user.role === Role.SUPER_ADMIN und der Aufrufer nicht SUPER_ADMIN, ForbiddenException) — die Vorlage steht seit 260911-fh9 in AuthService.adminResetPassword. Ausserhalb der Erlaubnisliste dieses Plans, deshalb Ledger statt Reparatur; vor dem ersten Mandanten mit einem zweiten Administrator zu schliessen. | open | | 2026-09-11T10:00:38.418Z | |
| 29 | quick-260911-fh9 | unmet-truth | apps/api/src/user/user.controller.ts | | Bereich auth: adminResetPassword schliesst die Rechteausweitung ADMIN -> SUPER_ADMIN im eigenen Handler (T-FH9-04), der Schwesterweg PATCH /users/:id tut das nicht. UserController.update (T-02-08) prueft nur, ob die Rolle SUPER_ADMIN NEU ZUGEWIESEN wird (dto.role === Role.SUPER_ADMIN), nicht ob das ZIEL diese Rolle bereits HAT — password/isActive gehen fuer ein bestehendes SUPER_ADMIN-Ziel ungeprueft durch; remove prueft ueberhaupt keine Rolle des Ziels, nur Selbstloeschung und Mandantengrenze. Kein Mandantenproblem, sondern Rechteausweitung INNERHALB des Mandanten. Reparatur in einem Satz: die Rolle des ZIELS pruefen (ist user.role === Role.SUPER_ADMIN und der Aufrufer nicht SUPER_ADMIN, ForbiddenException) — die Vorlage steht seit 260911-fh9 in AuthService.adminResetPassword. Ausserhalb der Erlaubnisliste dieses Plans, deshalb Ledger statt Reparatur; vor dem ersten Mandanten mit einem zweiten Administrator zu schliessen. | fixed | | 2026-09-11T10:00:38.418Z | 2026-09-14T08:37:53.307Z |
| 30 | quick-260911-gwh | deviation | apps/api/src/mail/mail.module.ts | | Startpfad des Mailmoduls (SettingsService.loadAnySmtpConfigForStartupTransport(), vormals getStartupSmtpConfig()) als SECHSTER Fall der Hintergrunddienst-Falle bleibt bewusst UNGEBUNDEN. HEUTE bereits falsch: findFirst() ohne Bedingung zieht bei mehreren Mandanten den SMTP-Server und Absender EINES beliebigen Mandanten fuer ALLE Kennwort-Zuruecksetzungs- und Willkommensmails ALLER Mandanten (T-GWH-03, Nutzung fremder Zugangsdaten). NACH DEM SCHARFSCHALTEN (#18) liefert dieselbe Abfrage null, mail.module.ts faellt auf MAIL_*/TESSERA_SMTP_*/localhost:1025 zurueck, MailService faengt den Transportfehler (T-02-12) -- KEINE Protokollzeile, das Verstummen ist doppelt verdeckt (Unsymmetrie zu ldap.getAllActiveConfigs [heute korrekt] UND zu dkv WINDOWS #21 [verstummt mit Protokollzeile]). Drei erwogene Formen: an einen aufgeloesten Mandanten binden (unmoeglich, kein Kontext beim Start); Mehrmandanten-Versand (abgelehnt als Funktion -- Vorlage steht in DkvMailService/TenderMailService, Transport je Versand aus getDecryptedSmtpConfig(tenantId)); als benannte Altlast weiterfuehren mit Markierung (GEWAEHLT). Eigener Eintrag statt Anschluss an #21: andere Datei, andere Reparatur, andere Verdeckungsform. Signal fuer rls-preflight.mjs gehoert in Etappe 4. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s4)(a). | open | | 2026-09-11T11:57:36.656Z | |
| 31 | quick-260911-gwh | deviation | apps/web/src/components/dashboard/widgets/favorites-widget.tsx | | Bereich favorites: ein nach dem Scharfschalten (#18) zu klein gebliebenes Leseergebnis auf list() sieht aus wie 'Noch keine Favoriten.' (favorites-widget.tsx Zeile um 212, de.json favorites.empty) -- fetchFavorites (favorites-api.ts) reicht die leere Liste durch. 'Nie einen gespeichert' und 'Zeile unsichtbar' sind fuer das Frontend derselbe Wert []. Etappe-4-Vorabpruefung (f4)(d): fuer einen bekannten Nutzer/Widget die Favoritenzahl ueber die Wartungsrolle und ueber den gebundenen findMany daneben halten. Familie #23/#25/#26/#28. Das Frontend wird von 260911-gwh NICHT geaendert. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich favorites' (f3)/(f4). | open | | 2026-09-11T11:57:50.276Z | |
| 32 | quick-260911-gwh | deviation | apps/web/src/components/settings/smtp-settings-form.tsx | | Bereich settings: getSmtpConfig liefert nach dem Scharfschalten (#18) null, der Controller antwortet 200 mit leerem Rumpf, fetchSmtp (settings-api.ts) laeuft mit res.json() auf den leeren Rumpf und wirft, smtp-settings-form.tsx verschluckt das in .catch(() => {}) -- leeres Formular 'nicht eingerichtet', waehrend die Zugangsdaten physisch da sind. Ein erneutes Speichern unter der ungebundenen Form scheitert am Eindeutigkeitsindex SmtpConfig_tenantId_key (PrismaClientUnknownRequestError, gemessen in Aufgabe 1 Pruefung 8) -- nach diesem Lauf ist saveSmtpConfig gebunden und trifft die eigene Zeile, dieser Rest bestand nur unter der ungebundenen Form vor dieser Aenderung. Dieselbe 200-leerer-Rumpf-Kette wie #28. Etappe-4-Vorabpruefung (s4)(e). Das Frontend wird von 260911-gwh NICHT geaendert. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s3)/(s4). | open | | 2026-09-11T11:57:50.484Z | |
| 33 | quick-260911-mkj | unmet-truth | apps/api/src/tenders/tenders.seed.ts | | Modellaufrufe auf Empfaengern, die weder this.prisma noch eine const X = forTenant(-Zuweisung noch ein Transaktionsparameter sind, sind fuer ALLE vier Erkennungsformen der Bestandsaufnahme unsichtbar. Gemessen 260911-mkj: tenders/tenders.seed.ts (Funktionsparameter prisma: PrismaService, tenderRssFeedSource.findFirst/create, kein Eintrag in der Bestandsaufnahme) und tenders/backfill-tender-source.ts (eigenstaendiges Skript mit new PrismaClient(), tender.findMany/update, durch RELATION_SPEC_EXCEPTIONS laut gehalten). Beide beruehren nur den plattformglobalen Katalog bzw. die plattformweite RSS-Verwaltung (WINDOWS #24), heute ungefaehrlich; OFFEN ist der Mechanismus (ein kuenftiger Dienst mit Parameter-Empfaenger auf einer geschuetzten Tabelle bliebe unsichtbar). Zu schliessen vor Etappe 4 durch eine Zaehlung ALLER <Kennung>.<Modell>.<Operation>(-Anker gegen die bekannte Empfaengermenge, Ueberschuss laut. | open | | 2026-09-11T14:48:09.723Z | |
| 34 | quick-260911-nke | deviation | apps/api/src/prisma/prisma-tenant.extension.ts | | Etappe 3b: ein Nutzer-CRUD-Aufrufer, der den Benutzer an forTenant() vergisst, sieht den ganzen Mandanten (IS-NULL-Form) — gleicher Stand wie vor 20260911120000, keine Verschlechterung, aber kein Netz. Die Bestandsaufnahme unterscheidet nur mandanten-gebunden/ungebunden, nicht benutzer-gebunden; ein Waechter, der jede Methode mit userId-Parameter auf das dritte Argument prueft, ist NICHT gebaut. Bis dahin sind die dreistelligen Spec-Zusicherungen je Dienst das einzige Netz. Vor dem Scharfschalten (Etappe 4, rls-preflight.mjs) zu entscheiden: Waechter bauen oder Rest benennen. | open | | 2026-09-11T15:46:08.295Z | |
| 35 | quick-260914-ebg | deviation | biome.json | | Biome ist im Bestand nicht lauffaehig: biome.json traegt den in Biome 2.5.0 unbekannten Schluessel organizeImports (gehoert unter assist), Biome bricht bei jedem Aufruf mit Konfigurationsfehler ab; zusaetzlich fehlt javascript.parser.unsafeParameterDecoratorsEnabled, ohne den jeder NestJS-Parameter-Dekorator ein Parse-Fehler ist (17 allein in user.controller.ts). Der CI-Schritt Lint ruft pnpm lint = turbo lint, keine App hat ein lint-Skript - der Schritt ist ein Leerlauf, der gruen meldet. CLAUDE.md und docs/anleitung-entwicklung.md beschreiben Biome als aktives Werkzeug. Gemessen 260914-ebg; das dortige Gate lief mit einer Ersatzkonfiguration im Scratchpad, relativ zur Baseline (0 Fehler, Warnungen je Datei 22/25/20, alle noExplicitAny-Familie; biome format ebenfalls unsauber, Anfuehrungszeichen-Stil). Zu entscheiden: biome.json reparieren (organizeImports nach assist, Parser-Schalter, quoteStyle single) und ein lint-Skript je App anlegen, dann die Warnungen in einem eigenen Durchlauf abbauen oder als Regelabschaltung begruenden. | open | | 2026-09-14T08:38:04.079Z | |
| 36 | quick-260914-ebg | deviation | apps/web/src/app/(portal)/admin/users/page.tsx | | handleSubmit und handleDelete pruefen nur res.ok ohne else-Zweig und fangen mit leerem catch - ein 403 der API fuehrt zu keiner sichtbaren Reaktion (Formular bleibt offen, Loeschdialog bleibt stehen, keine Meldung). Bestehendes Verhalten fuer alle 403-Wege (fremder Mandant, Selbstloeschung); seit 260914-ebg (WINDOWS #29) ist der Fall fuer einen ADMIN im Alltag erreichbar, weil die SUPER_ADMIN-Zeile in der eigenen Benutzerliste steht und Aendern/Loeschen darauf jetzt 403 liefert. Familie der still verschluckten Antworten (#28, #32). Frontend von 260914-ebg NICHT geaendert (ausserhalb der Erlaubnisliste). Zu schliessen: Fehlermeldung aus dem Antwortrumpf anzeigen und die Aktionsknoepfe fuer SUPER_ADMIN-Zeilen einem ADMIN gar nicht erst anbieten. | open | | 2026-09-14T08:38:12.619Z | |
````json
[
@@ -395,10 +397,10 @@ last_updated: 2026-09-11T15:46:08.295Z
"file": "apps/api/src/user/user.controller.ts",
"line": null,
"description": "Bereich auth: adminResetPassword schliesst die Rechteausweitung ADMIN -> SUPER_ADMIN im eigenen Handler (T-FH9-04), der Schwesterweg PATCH /users/:id tut das nicht. UserController.update (T-02-08) prueft nur, ob die Rolle SUPER_ADMIN NEU ZUGEWIESEN wird (dto.role === Role.SUPER_ADMIN), nicht ob das ZIEL diese Rolle bereits HAT — password/isActive gehen fuer ein bestehendes SUPER_ADMIN-Ziel ungeprueft durch; remove prueft ueberhaupt keine Rolle des Ziels, nur Selbstloeschung und Mandantengrenze. Kein Mandantenproblem, sondern Rechteausweitung INNERHALB des Mandanten. Reparatur in einem Satz: die Rolle des ZIELS pruefen (ist user.role === Role.SUPER_ADMIN und der Aufrufer nicht SUPER_ADMIN, ForbiddenException) — die Vorlage steht seit 260911-fh9 in AuthService.adminResetPassword. Ausserhalb der Erlaubnisliste dieses Plans, deshalb Ledger statt Reparatur; vor dem ersten Mandanten mit einem zweiten Administrator zu schliessen.",
"status": "open",
"status": "fixed",
"reason": "",
"recorded_at": "2026-09-11T10:00:38.418Z",
"resolved_at": null
"resolved_at": "2026-09-14T08:37:53.307Z"
},
{
"id": 30,
@@ -459,6 +461,32 @@ last_updated: 2026-09-11T15:46:08.295Z
"reason": "",
"recorded_at": "2026-09-11T15:46:08.295Z",
"resolved_at": null
},
{
"id": 35,
"kind": "deviation",
"phase": "quick-260914-ebg",
"file": "biome.json",
"line": null,
"description": "Biome ist im Bestand nicht lauffaehig: biome.json traegt den in Biome 2.5.0 unbekannten Schluessel organizeImports (gehoert unter assist), Biome bricht bei jedem Aufruf mit Konfigurationsfehler ab; zusaetzlich fehlt javascript.parser.unsafeParameterDecoratorsEnabled, ohne den jeder NestJS-Parameter-Dekorator ein Parse-Fehler ist (17 allein in user.controller.ts). Der CI-Schritt Lint ruft pnpm lint = turbo lint, keine App hat ein lint-Skript - der Schritt ist ein Leerlauf, der gruen meldet. CLAUDE.md und docs/anleitung-entwicklung.md beschreiben Biome als aktives Werkzeug. Gemessen 260914-ebg; das dortige Gate lief mit einer Ersatzkonfiguration im Scratchpad, relativ zur Baseline (0 Fehler, Warnungen je Datei 22/25/20, alle noExplicitAny-Familie; biome format ebenfalls unsauber, Anfuehrungszeichen-Stil). Zu entscheiden: biome.json reparieren (organizeImports nach assist, Parser-Schalter, quoteStyle single) und ein lint-Skript je App anlegen, dann die Warnungen in einem eigenen Durchlauf abbauen oder als Regelabschaltung begruenden.",
"status": "open",
"reason": "",
"recorded_at": "2026-09-14T08:38:04.079Z",
"resolved_at": null,
"milestone": "v1.2"
},
{
"id": 36,
"kind": "deviation",
"phase": "quick-260914-ebg",
"file": "apps/web/src/app/(portal)/admin/users/page.tsx",
"line": null,
"description": "handleSubmit und handleDelete pruefen nur res.ok ohne else-Zweig und fangen mit leerem catch - ein 403 der API fuehrt zu keiner sichtbaren Reaktion (Formular bleibt offen, Loeschdialog bleibt stehen, keine Meldung). Bestehendes Verhalten fuer alle 403-Wege (fremder Mandant, Selbstloeschung); seit 260914-ebg (WINDOWS #29) ist der Fall fuer einen ADMIN im Alltag erreichbar, weil die SUPER_ADMIN-Zeile in der eigenen Benutzerliste steht und Aendern/Loeschen darauf jetzt 403 liefert. Familie der still verschluckten Antworten (#28, #32). Frontend von 260914-ebg NICHT geaendert (ausserhalb der Erlaubnisliste). Zu schliessen: Fehlermeldung aus dem Antwortrumpf anzeigen und die Aktionsknoepfe fuer SUPER_ADMIN-Zeilen einem ADMIN gar nicht erst anbieten.",
"status": "open",
"reason": "",
"recorded_at": "2026-09-14T08:38:12.619Z",
"resolved_at": null,
"milestone": "v1.2"
}
]
````