docs(quick-260911-cwh): Bereich calendar Etappe 2 abgeschlossen — restliche Dokumentstellen nachgezogen (Aufgabe 3)

- docs/mandantentrennung-zugriffsklassifikation.md: Uebersichtszeile
  calendar auf 0/12 gezogen (war 12/0, kein ungebundener Rest — erster
  Bereich in Folge ohne begruendeten Rest), Summenzeile auf 83/159
  fortgeschrieben, Klassen-Verteilung unveraendert mit ausdruecklichem
  Stand-260911-cwh-Vermerk, Hintergrunddienst-Abschnitt um den
  Sonderfall refreshCacheInBackground (abgekoppelte Fortsetzung, kein
  sechster Fall) ergaenzt, Abschnitt "Was diese Etappe NICHT entscheidet"
  um calendar ergaenzt
- .planning/WINDOWS.md: neuer offener Eintrag (#26) zur lautlosen
  Auspraegung der umgekehrten Fehlerrichtung im Bereich calendar, ueber
  gsd-tools windows append angelegt
- Beide Falsifizierungsnachweise durchgefuehrt (falscher Stand macht
  rls-access-inventory.spec.ts rot; falsche Uebersichtszahl macht das
  herleitende Gate rot), zurueckgenommen
- Etappe 2, neunter Bereich (calendar) abgeschlossen: alle zwoelf
  Zugriffe gebunden, Baseline gehalten (883 Tests, 57 Dateien, 101
  Werkzeugpruefungen, Typpruefung sauber)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
2026-09-11 09:59:17 +02:00
parent 77cb124f59
commit e0e163ec63
2 changed files with 52 additions and 7 deletions
+16 -3
View File
@@ -1,10 +1,10 @@
---
schema_version: 1
open_count: 7
open_count: 8
waived_count: 1
fixed_count: 17
total_count: 25
last_updated: 2026-09-11T09:01:00.000Z
total_count: 26
last_updated: 2026-09-11T07:57:36.769Z
---
# Broken Windows Ledger
@@ -40,6 +40,7 @@ last_updated: 2026-09-11T09:01:00.000Z
| 23 | quick-260910-exd | deviation | apps/api/src/module-registry/module-access.service.ts | | Es gibt heute KEIN Signal, das 'wirklich keine Freigabe' (USER hat tatsaechlich keinen Grant) von 'die Abfrage hat nichts gefunden' (z. B. eine nach dem Scharfschalten ungebunden gebliebene Abfrage) unterscheidet: dieselbe ForbiddenException-Meldung im Waechter, dieselbe leere Modulliste mit Status 200, kein Protokolleintrag. Nach dem Scharfschalten ist ein zu kleines Ergebnis in diesem Bereich TOTAL und lautlos -- JEDER Benutzer JEDES Mandanten verliert gleichzeitig jedes Modul, waehrend die Aktivierungs- und Freigabetabellen weiterhin Zeilen halten -- und sieht fuer den Betroffenen wie ein absichtlicher Rechteentzug aus, nicht wie ein Fehler (der Betroffene hat eine fertige, falsche Erklaerung zur Hand und meldet deshalb keinen Fehler). Konkrete Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): aktive Aktivierungszeilen vorhanden, aber die Aufloesung liefert fuer einen bekannten Administrator eine leere Menge. Eine Laufzeitwarnung an den betroffenen Stellen wurde erwogen und begruendet verworfen (Dauerlaerm auf einer frischen Installation, dieselbe Begruendung wie bei getAllActiveConfigs im Bereich ldap und den fuenf Stellen im Bereich tenders). In module.guard.spec.ts als Testfall festgenagelt, damit die Aufzeichnung rot wird, sobald jemand ein unterscheidendes Signal einbaut. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich module-registry' (m3). | open | | 2026-09-10T09:28:45.310Z | |
| 24 | quick-260910-jab | deviation | apps/api/src/tenders/tender-rss-feed.service.ts | | Was das Schliessen von WINDOWS #19 NICHT loest: unter der Anwendungsrolle laesst sich eine plattformweite RSS-Quelle (TenderRssFeedSource, tenantId NULL) weder anlegen noch entfernen — in der alten wie in der neuen Regel, weil jede Schreibregel (Einfuegen/Aendern/Entfernen) ausdruecklich einen Mandanten verlangt (tenant_insert_policy/tenant_update_policy/tenant_delete_policy, 20260910120000_rls_widen_membership_grant_and_platform_read). Betroffen sind zwei Pfade in TenderRssFeedSourceService: createPlatform() (setzt tenantId=NULL, ein gebundenes INSERT liefe in die WITH-CHECK-Klausel und wuerde abgewiesen) und remove() (deckt fuer Administratoren auch das Entfernen einer plattformweiten Zeile ab; ein gebundenes DELETE traefe sie nie). Beide bleiben deshalb bewusst ungebunden — das ist KEINE Folge dieser Reparatur, sondern bestand bereits vor 260910-jab identisch, weil die vom Ledger vorgegebene #19-Semantik Schreibzugriffe ausdruecklich an einen Mandanten bindet. Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): ein Verwaltungsweg fuer plattformweite Zeilen (Anlegen/Entfernen unter der Anwendungsrolle) muss gebaut werden, BEVOR die Rolle umgeschaltet wird — sonst kann kein Administrator nach dem Scharfschalten mehr eine plattformweite Quelle pflegen. Eigener Eintrag, damit dieser Rest nicht mit #19 verschwindet. | open | | 2026-09-10T12:35:40.000Z | |
| 25 | quick-260910-krx | deviation | apps/web/src/lib/stores/dashboard-store.ts | | Die beweisvernichtende Auspraegung der umgekehrten Fehlerrichtung im Bereich dashboard: ein nach dem Scharfschalten (WINDOWS #18) zu klein gebliebenes Leseergebnis auf getLayout sieht nicht wie ein Fehler aus, sondern wie eine leere Vorgabeanordnung. Drei Stellen greifen ineinander (docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich dashboard', (w3)): (1) DashboardService.getLayout liefert bei fehlendem Datensatz {lg:[],md:[],sm:[],xs:[],xxs:[]} statt eines Fehlers; (2) apps/web/src/lib/stores/dashboard-store.ts, loadDashboard setzt layouts/widgets ungeprueft auf das Ergebnis, der catch-Zweig feuert nur bei Netzwerk-/Statusfehlern, nicht bei einer erfolgreichen leeren Antwort; (3) dieselbe Datei, setEditMode(false) schreibt bei isDirty automatisch zurueck, sobald der Bearbeitungsmodus verlassen wird — ohne dass der Nutzer auf Speichern klickt. Die Folge: der Nutzer haelt ein leeres Dashboard fuer einen Fehler des Widget-Systems oder fuer verlorene Einstellungen ('das Widget-System spinnt', 'meine Einstellungen sind weg'), baut seine Anordnung neu auf (addWidget legt echte neue WidgetInstance-Zeilen an, keine Eindeutigkeitsbedingung ueber (userId, widgetType), Dubletten haeufen sich bei wiederholtem Neuaufbau an), und das automatische Zurueckschreiben ueberschreibt die layouts-Spalte der urspruenglichen Zeile — die einzige Aufzeichnung der urspruenglichen Anordnung ist verloren, bevor irgendjemand die Ursache untersuchen konnte. Zusaetzlich, kleiner: apps/web/src/components/dashboard/widgets/search-widget.tsx laesst bei einem zu kleinen custom-Ergebnis die eigenen Suchmaschinen des Nutzers aus der Auswahlliste verschwinden (der Rueckfallzweig auf DEFAULT_PROVIDERS feuert nie, weil getSearchProviders die drei Vorgaben immer voranstellt), und handleSearch faellt bei unbekannter Auswahl auf providers[0] (Google) zurueck — eine fuer ein internes Werkzeug gedachte Suchanfrage ginge dann an eine externe Suchmaschine. Konkrete Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): eine physisch vorhandene DashboardLayout-Zeile fuer einen bekannten Benutzer, aber der gebundene Lesezugriff fuer dessen Mandanten liefert null — das unterscheidet den echten Erstbenutzer-Fall vom Trennungsfehler. An dieselbe Bedingung gebunden wie #18 — beobachtbar erst nach dem Scharfschalten. Das Frontend wird von 260910-krx NICHT geaendert, dieser Eintrag beschreibt es nur. Die verwandte, strukturelle Eindeutigkeitsfrage von DashboardLayout.userId (plattformweit @unique ohne Mandantenanteil) ist derselbe Fall wie WINDOWS #22 im Bereich user — dort mitgefuehrt, kein zweiter Eintrag hier. | open | | 2026-09-11T09:01:00.000Z | |
| 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 | |
````json
[
@@ -342,6 +343,18 @@ last_updated: 2026-09-11T09:01:00.000Z
"reason": "",
"recorded_at": "2026-09-11T09:01:00.000Z",
"resolved_at": null
},
{
"id": 26,
"kind": "deviation",
"phase": "quick-260911-cwh",
"file": "apps/web/src/components/dashboard/widgets/calendar-widget.tsx",
"line": null,
"description": "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.",
"status": "open",
"reason": "",
"recorded_at": "2026-09-11T07:57:36.769Z",
"resolved_at": null
}
]
````
@@ -124,11 +124,11 @@ autoritative Quelle.
| module-registry | 7 | 10 | **war 17/0** — Aufgabe 2/3 (260910-exd) haben `module-access.service.ts` (`getAccessibleModuleIds`: Kurzschlusszweig, Direktweg, Gruppenweg, Schnittmenge; `getCatalogFlags`: eigener Aktivierungs-Lesezugriff) und `module-registry.service.ts` (`findActiveForTenant`, `activateForTenant`, `deactivateForTenant`, `isModuleActive`) auf `forTenant()` umgestellt. Die 7 verbleibenden ungebundenen Rohtreffer sind bewusst: der eine Katalogzugriff in `module-access.service.ts` (`findAccessibleModules`) und die sechs Katalogzugriffe in `module-registry.service.ts` (`findAll`, `findBySlug`, die beiden Katalog-Existenzpruefungen in `activateForTenant`/`deactivateForTenant`, die Katalogsuche in `isModuleActive`, `seedModule`) — der Modulkatalog (`Module`) traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal; katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E) |
| dashboard | 1 | 12 | **war 13/0** — Aufgabe 2/3 (260910-krx) haben `dashboard.service.ts` vollständig umgestellt: `getLayout`/`saveLayout` (gemeinsam gebunden), `getWidgets`/`addWidget`/`updateWidgetConfig`/`removeWidget` sowie `getSearchProviders`/`addSearchProvider`/`removeSearchProvider` laufen über `forTenant()`, je Methode ein Klient. Der eine verbleibende ungebundene Rohtreffer ist bewusst: der Modulkatalog (`Module`) trägt heute keinen Zeilenschutz, eine Bindung wäre heute wirkungslos, nicht katastrophal — katastrophal würde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E aus `module-registry`, hier übernommen) |
| auth | 8 | 5 | unverändert gegenüber dem in Etappe 1 (260909-eor) gemessenen Stand |
| calendar | 12 | 0 | unverändert |
| calendar | 0 | 12 | **war 12/0** — Aufgabe 2 (260911-cwh) hat `calendar.service.ts` vollständig auf `forTenant()` umgestellt: `getSources`, `addSource`, beide Abfragen von `updateSource`/`deleteSource`, alle drei Abfragen von `testConnection`, Laden plus beide Synchronstatus-Rückschreibungen von `fetchAndCacheEvents` — je Methode ein Klient. Anders als bei den sieben Bereichen davor bleibt KEIN ungebundener Rest übrig: `CalendarSource` trägt eine Pflicht-Mandantenkennung, und kein Pfad dieses Bereichs liest über Mandanten hinweg |
| tenant | 8 | 0 | unverändert |
| favorites | 7 | 0 | unverändert |
| settings | 4 | 0 | unverändert |
| **Summe** | **95** | **147** | Ungebunden: war 118 nach 260910-das, dann 108 nach 260910-exd (module-registry 17→7), dann 107 nach 260910-jab (`tenders` 36→35, `listForUser` gebunden), jetzt 95 nach 260910-krx (`dashboard` 13→1). Gebunden: war 124, dann 134 nach 260910-exd (zusätzlich 10 in `module-registry`), dann 135 nach 260910-jab (zusätzlich 1 in `tenders`), jetzt 147 nach 260910-krx (zusätzlich 12 in `dashboard`). Diese Übersicht ist eine Buchführungshilfe; **autoritativ ist die Fundstellentabelle unten**, die `rls-access-inventory.spec.ts` bei jedem Lauf gegen den Quelltext prüft |
| **Summe** | **83** | **159** | Ungebunden: war 118 nach 260910-das, dann 108 nach 260910-exd (module-registry 17→7), dann 107 nach 260910-jab (`tenders` 36→35, `listForUser` gebunden), dann 95 nach 260910-krx (`dashboard` 13→1), jetzt 83 nach 260911-cwh (`calendar` 12→0). Gebunden: war 124, dann 134 nach 260910-exd (zusätzlich 10 in `module-registry`), dann 135 nach 260910-jab (zusätzlich 1 in `tenders`), dann 147 nach 260910-krx (zusätzlich 12 in `dashboard`), jetzt 159 nach 260911-cwh (zusätzlich 12 in `calendar`). Diese Übersicht ist eine Buchführungshilfe; **autoritativ ist die Fundstellentabelle unten**, die `rls-access-inventory.spec.ts` bei jedem Lauf gegen den Quelltext prüft |
## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 63 Paare)
@@ -178,6 +178,16 @@ ohne diesen Vermerk waere von einer vergessenen Nachziehung nicht zu
unterscheiden — deshalb steht die Abwesenheit einer Aenderung hier
ausdruecklich, statt stillschweigend uebersprungen zu werden.
**Stand 260911-cwh (Aufgabe 3): unveraendert, ausdruecklich festgehalten
statt uebersprungen.** Weiterhin 63 Paare, keine Klasse verschoben sich. Das
eine Paar des Bereichs `calendar` (`calendar.service.ts`/`calendarSource`)
war bereits vor diesem Durchlauf korrekt klassifiziert (`muss-mandantengebunden`)
— Aufgabe 2 (260911-cwh) aendert nur seine `Stand`-Spalte (`ungebunden` auf
`gebunden`), nicht seine Klasse. Eine unveraenderte Tabelle ohne diesen
Vermerk waere von einer vergessenen Nachziehung nicht zu unterscheiden —
deshalb steht die Abwesenheit einer Aenderung hier ausdruecklich, statt
stillschweigend uebersprungen zu werden.
| Klasse | Anzahl Paare |
|---|---|
| muss-mandantengebunden | 31 |
@@ -304,6 +314,25 @@ bekannten Nutzers heraus aufgerufen — die Bauform dieses Abschnitts
(übergreifend LESEN über alle Mandanten, dann je Mandant BINDEN) kommt in
diesem Bereich an keiner Stelle vor.
**Stand 260911-cwh — auch der Bereich `calendar` fügt diesem Abschnitt
keinen sechsten Fall hinzu, gemessen statt angenommen (260911-cwh, Aufgabe 1,
Befund B).** `grep -rn "@Cron\|onModuleInit\|onApplicationBootstrap\|setInterval\|setTimeout" apps/api/src/calendar --include=*.ts`
liefert außerhalb von Testdateien genau einen Treffer,
`providers/ics.provider.ts:100` — ein `setTimeout` für den Abbruch eines
HTTP-Abrufs nach acht Sekunden, kein Planer. Der einzige Dienst dieses
Bereichs, `calendar.service.ts`, enthält keinen Hintergrunddienst, keinen
Planer und keinen Start-Hook — die Bauform dieses Abschnitts (übergreifend
LESEN über alle Mandanten, dann je Mandant BINDEN) kommt an keiner Stelle
vor. Es gibt aber einen benannten Sonderfall, anderer Bauart als die fünf
Fälle oben: `refreshCacheInBackground` (private Methode) ist eine
ABGEKOPPELTE FORTSETZUNG einer Anfrage — `aggregateEvents` stößt sie an,
wartet nicht auf sie, und sie ruft `fetchAndCacheEvents` mit den Parametern
der ursprünglichen Anfrage auf. Sie iteriert NICHT über mehrere Mandanten
und hat deshalb keine übergreifende Hälfte zu binden — sie trägt die
Mandantenkennung der Anfrage, die sie ausgelöst hat, und kann strukturell
keine andere haben. Kein Kandidat für diese Liste; dieser Absatz hält die
Abwesenheit fest, damit sie nicht wie ein Übersehen aussieht.
## Bestandsaufnahme
Maschinell ermittelt, `rls-access-inventory.spec.ts` hält Vollständigkeit
@@ -392,8 +421,11 @@ verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit
werden nicht zwischen Methoden weitergereicht. Der Bereich `dashboard`
(260910-krx) hat sich für denselben dienst-internen Weg entschieden — jede
der neun umgestellten Methoden in `dashboard.service.ts` erzeugt ihren
eigenen `forTenant()`-Aufruf, wie alle sieben Bereiche vor ihm. Die Frage
bleibt für alle übrigen Bereiche der Etappe 2 offen.
eigenen `forTenant()`-Aufruf, wie alle sieben Bereiche vor ihm. Der Bereich
`calendar` (260911-cwh) hat sich für denselben dienst-internen Weg
entschieden — jede der sechs umgestellten Methoden in `calendar.service.ts`
erzeugt ihren eigenen `forTenant()`-Aufruf, wie alle acht Bereiche vor ihm.
Die Frage bleibt für alle übrigen Bereiche der Etappe 2 offen.
- ~~Wie die WINDOWS-#19-Policy für `SearchProvider`/`TenderRssFeedSource`
am Ende genau lautet — nur, dass sie vor dem Scharfschalten gelöst sein
muss.~~ Aufgelöst (260910-jab): `TenderRssFeedSource` bekommt vier nach