From 0094a60d15824eadc391ddd428e746077495016f Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 23 Sep 2026 08:29:50 +0200 Subject: [PATCH] docs(quick-260923-ad9): Akte - Rundgang mit zwoelf Punkten bestanden Bestand bleibt erhalten (5 Kacheln im ersten Reiter), Kacheln je Reiter getrennt, Ziehen ordnet um, nach dem Neuladen kommt der erste Reiter, letzter Reiter ohne Loeschknopf, Raster unveraendert. Offener Kleinbefund notiert: die Knopf-Beschriftungen nennen den betroffenen Reiter nicht, nur das Bestaetigungsfenster tut es. Co-Authored-By: Claude Opus 5 (1M context) --- .planning/STATE.md | 7 +- .../260923-ad9-SUMMARY.md | 168 +++++++++++++++++ .../260923-ad9-VERIFICATION.md | 170 ++++++++++++++++++ 3 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 .planning/quick/260923-ad9-dashboard-reiter-mehrere-dashboards-je-b/260923-ad9-SUMMARY.md create mode 100644 .planning/quick/260923-ad9-dashboard-reiter-mehrere-dashboards-je-b/260923-ad9-VERIFICATION.md diff --git a/.planning/STATE.md b/.planning/STATE.md index 5f6a0ba..b4d8005 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,9 +5,9 @@ current_phase: 18 current_phase_name: desktop-client-fertigstellen status: verified stopped_at: "22.09.2026: 1.3.0 freigegeben; danach quick-260922-hk4 — Bilderrahmen-Bilder liegen jetzt im Dateibereich (user-files) statt in der Datenbank, Umzug laeuft automatisch beim Start, Selbstheilung aus der alten data-Spalte eingebaut; im Browser nachgewiesen. NAECHSTER SCHRITT, vom Nutzer noch nicht bestaetigt: (1) einmaliges Aufraeumen, damit ein Modul seine Dashboard-Kachel selbst mitbringt (heute sieben Hartkodierungen je Kachel; Katalog zeigt auch Kacheln gesperrter Module; gesperrte Kachel bleibt leer statt zu erklaeren) — das Geruest WIDGET_MODULE_MAP existiert und ist leer; (2) danach das Proxmox-Modul (PVE/PBS/PMG) und seine Kachel. Offen beim Nutzer: Live-Server auf 1.3.0 ziehen, neuen Client per Browser installieren." -last_updated: "2026-09-22T21:10:00.000Z" -last_activity: 2026-09-22 -last_activity_desc: Quick 260922-vdk — Dashboard-Raster misst seine Breite auch aus dem Leerzustand; im echten Linux-Client gegengemessen (469 -> 389 px, Ziehen erreicht den rechten Rand) +last_updated: "2026-09-23T08:40:00.000Z" +last_activity: 2026-09-23 +last_activity_desc: Quick 260923-ad9 — Dashboard-Reiter (mehrere Dashboards je Benutzer, Ziehen sortiert, erster ist Standard); davor Freigabe 1.3.1 state_head: 4d485432c003a6caf68f6d85aff7de0bd27794e2 progress: total_phases: 18 @@ -464,6 +464,7 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests. | 260922-hk4 | **Bilderrahmen-Bilder liegen jetzt im Dateibereich statt in der Datenbank.** Frage des Nutzers nach der Freigabe 1.3.0, ob `bytea` auf Dauer sinnvoll ist. Befund: Geschwindigkeit ist NICHT das Argument (ein Bild wird je Browser einmal taeglich geladen), die SICHERUNG ist es — gesichert wird von Hand per `pg_dump`, und 30 Bilder à 5 MiB je Benutzer waeren im Extremfall 150 MB pro Benutzer in jedem Abzug (alpha-DB heute 18 MB). Dazu Einheitlichkeit: Profilbilder (`user-files/avatars`, `User.avatarPath`) und DKV-Exporte liegen laengst im Volume. Umsetzung: Spalte `storagePath`, Ablage `user-files/dashboard-images//.` — Dateiname IMMER vom Server (UUID + Endung aus dem erkannten Mime-Typ), `originalName` nie im Pfad; ein eigener Ordner je Benutzer ist ausdruecklich KEIN Schutz, es entscheidet weiterhin die Besitzpruefung im Dienst. Umzug laeuft automatisch beim Start (`onApplicationBootstrap` ueber `forSystem()`), idempotent; die Spalte `data` bleibt bewusst vorerst stehen (Todo fuer den DROP, erst wenn alpha und live einmal gelaufen sind). **Befund im Rundgang, eigener Commit:** eine Zeile zeigte auf eine fehlende Datei (lokal Host vs. Container-Volume; im Betrieb: alter `pg_dump` + leeres Volume) — `getBytes` stellt die Datei jetzt aus der noch vorhandenen Spalte `data` wieder her, statt 404 zu melden. **Zahlen:** api 1175 → 1188, web 640, type-check 4/4, lint 5/5, RLS-Waechter 78/78. | 2026-09-22 | 9039cea,8cbfb8b,82472ee | [260922-hk4-bilderrahmen-bilder-auf-die-festplatte](./quick/260922-hk4-bilderrahmen-bilder-auf-die-festplatte/) | | 260922-m1h | **Ein Modul bringt seine Dashboard-Kachel jetzt selbst mit (Vorarbeit fuer Proxmox).** Bestandsaufnahme (lesend) hatte ergeben: ein neuer Widget-Typ war an SIEBEN Stellen hartkodiert (Union-Typ, Constraints, Registry, eigene `wireXWidget()` je Typ, Aufruf in page.tsx, zweite Liste im Katalogfenster, `@IsIn` im API-DTO); die Verbindung Kachel↔Modul existierte als `WIDGET_MODULE_MAP` in `dashboard.service.ts` (filtert fail-closed), war aber nie befuellt; der Katalog zeigte jedem alle Kacheln, auch die gesperrter Module. Umbau: `WIDGET_TYPES`/`WidgetType`/`WIDGET_MODULE_SLUGS` in `packages/shared` als EINE Quelle (API validiert per `@IsIn` gegen genau sie), ein generisches `registerWidget()` statt neun Funktionen, Katalog leitet seine Liste aus der Registry ab und filtert ueber `/modules/active` (fail-closed bei Fehler, reine Funktion `visibleWidgetTypes`), nicht verfuegbare Kachel zeigt `widgets.unavailable` statt leer zu bleiben. Deckungsgleichheits-Test faengt kuenftig jede vergessene Stelle. **Befund des Executors, geprueft statt vermutet:** `apps/web` hatte KEINE Abhaengigkeit auf `@tessera/shared` (frueher bewusst) — vor der Umsetzung nachgemessen, dass Bau und Produktions-Abbild das tragen (node:24-alpine strippt die Typen nativ); Folgeregel „nur loeschbare Syntax in shared“ steht als Warnung in der Datei. Verhalten der neun Kacheln unveraendert, im Browser bestaetigt (Reihenfolge, Anlegen, Entfernen, keine rohen Schluessel). Bewusst offen: der Einstellungs-Zweig je Typ in `widget-settings-panel.tsx` und die Live-Aktualisierung des Katalogs. **Zahlen:** api 1188 → 1202, web 640 → 659, type-check 4/4, lint 5/5 (74/53 wie Basis). | 2026-09-22 | 56c07c3,8be0725 | [260922-m1h-dashboard-widgets-ein-modul-bringt-seine](./quick/260922-m1h-dashboard-widgets-ein-modul-bringt-seine/) | | 260922-vdk | **Dashboard-Raster misst seine Breite auch aus dem Leerzustand heraus.** Meldung des Nutzers aus dem **Linux-Client**: rechts neben dem Kalender freie Flaeche, in die sich keine Kachel schieben laesst — „als ob es keinen Anker gibt“. Aus dem Bildschirmfoto zurueckgerechnet (Spaltenbreite 51,5 px, Platzhalter auf Spalte 13 = letzte moegliche, Rasterende bei x=1459 bei ~1660 px Inhaltsbreite): das Raster rechnete mit **1200 px** statt mit der echten Breite, rechts blieben ~460 px totes Feld. Ursache: die Breitenmessung hing in `useEffect(..., [])` mit `if (!containerRef.current) return` — haengt `DashboardGrid` mit NULL Kacheln ein, rendert der fruehe Ruecksprung in den Leerzustand den gemessenen `
` gar nicht, der Effekt bricht ab und laeuft nie wieder, auch nicht wenn spaeter die erste Kachel entsteht. `width` blieb die ganze Sitzung auf dem Startwert 1200; react-grid-layout vergleicht strikt (`width > breakpoint`), 1200 ist damit `md` (20 Spalten, 51,6 px) statt `lg`. Fix: Ref-Rueckruf `measureRef` statt Einmal-Effekt — folgt dem Knoten ueber den Wechsel Leerzustand ↔ gefuellt, misst synchron in der Commit-Phase, haengt den ResizeObserver dort an; Fenster-Horcher als zusaetzliches Netz; `applyWidth` verwirft 0 und nicht endliche Werte. **Verhalten sonst unveraendert** — belegte Plaetze bleiben gesperrt, nichts weicht aus (Ansage des Nutzers). **Geprueft im echten Client**, nicht im Browser: `Tessera-1.3.0.AppImage` auf `DISPLAY=:10` ueber den WebKit-Remote-Inspektor gesteuert. Gleicher Fehlerfall vorher/nachher: Kachel 469 px → **389 px** bei 1000 px Bereich, Ziehen endet jetzt bei 603 px = `1000 − 8 − 389`, exakt der rechte Rand. **Messfalle notiert:** im Client gegen `style.width`/`style.transform` messen, nie gegen `getBoundingClientRect()` — bei Fenster im Hintergrund friert WebKitGTK die Animationsuhr ein und der `width`-Uebergang bleibt auf dem alten Wert stehen. **Zahlen:** web 659 → 661 Tests, type-check 4/4, lint 5/5, Biome web 53 Warnungen unveraendert. | 2026-09-22 | d9f2af3,cf67c8a | [260922-vdk-dashboard-raster-misst-seine-breite-nich](./quick/260922-vdk-dashboard-raster-misst-seine-breite-nich/) | +| 260923-ad9 | **Dashboard-Reiter: mehrere Dashboards je Benutzer.** Wunsch des Nutzers (23.09.): mehrere Dashboards als Reiter, per Ziehen sortierbar, der erste ist der Standard und wird beim Oeffnen geladen; „als Favorit festlegen“ = nach vorn ziehen, kein zusaetzliches Kennzeichen. Umsetzung in 5 Schritten: neues Modell `Dashboard` (userId, tenantId, name, position) mit RLS wie die Nachbartabellen; `WidgetInstance.dashboardId` und `DashboardLayout.dashboardId @unique` — Kacheln und Anordnung haengen jetzt am Reiter statt am Benutzer. Handgeschriebene Migration `20260923120000_dashboard_tabs` haengt den Bestand um: Bestandsuebernahme VOR `NOT NULL`/Fremdschluessel, danach 0 verwaiste Kacheln, 0 verwaiste Anordnungen, je Benutzer genau ein Reiter auf Position 0. Fuenf Endpunkte unter `/dashboard/tabs`; `assertOwnedDashboard` laeuft als erstes in JEDEM Lese- und Schreibweg und antwortet fuer „gibt es nicht“, „Kollege“ und „fremder Mandant“ identisch (kein Orakel) — acht eigene Tests dafuer. Umsortieren und Loeschen je EINE Transaktion nach dem Muster `FavoritesService.reorder`. Riegel: 20 Reiter, 40 Zeichen, 20 Kennungen je Anfrage. Ziehen per Pointer-Ereignissen ohne neue Abhaengigkeit (Muster xframe-Ausschnitt), ausserhalb des Bearbeitungsmodus moeglich, weil „nach vorn ziehen“ das Festlegen des Standards IST; Umbenennen und Loeschen bleiben im Bearbeitungsmodus, Loeschen mit `alertdialog`-Rueckfrage. **Raster unangetastet** (`FREE_PLACEMENT_COMPACTOR`/`preventCollision` und die Breitenmessung aus 260922-vdk) — vom Verifizierer per `git diff` nachgewiesen. **Rundgang mit zwoelf Punkten bestanden** (Bestand 5 Kacheln erhalten, Reiter leer angelegt, Kacheln je Reiter getrennt, Ziehen ordnet um, nach Neuladen kommt der erste Reiter, Umbenennen, Loeschen mit Rueckfrage, letzter Reiter ohne Loeschknopf, Kachelbreite 531 px bei 1625 px Bereich). **Kleiner Befund, offen:** die Knopf-Beschriftungen nennen den betroffenen Reiter nicht (nur das Bestaetigungsfenster tut es). **Zahlen:** api 1202 → 1240 Tests, web 661 → 693, type-check 4/4, lint 5/5 mit 53 Warnungen unveraendert, `migrate diff` ohne Unterschied. | 2026-09-23 | 9c51823,df7a5e7,d34f682,05feaa3,58ce88e | [260923-ad9-dashboard-reiter-mehrere-dashboards-je-b](./quick/260923-ad9-dashboard-reiter-mehrere-dashboards-je-b/) | ## Deferred Items diff --git a/.planning/quick/260923-ad9-dashboard-reiter-mehrere-dashboards-je-b/260923-ad9-SUMMARY.md b/.planning/quick/260923-ad9-dashboard-reiter-mehrere-dashboards-je-b/260923-ad9-SUMMARY.md new file mode 100644 index 0000000..6d0b7c4 --- /dev/null +++ b/.planning/quick/260923-ad9-dashboard-reiter-mehrere-dashboards-je-b/260923-ad9-SUMMARY.md @@ -0,0 +1,168 @@ +--- +phase: quick-260923-ad9 +plan: 01 +subsystem: dashboard +tags: [dashboard, reiter, rls, migration, frontend, drag-and-drop] +dependency-graph: + requires: [] + provides: [dashboard-tabs, multi-dashboard-model] + affects: [apps/api/src/dashboard, apps/web/src/components/dashboard, apps/web/src/lib/stores/dashboard-store.ts] +tech-stack: + added: [] + patterns: [forTenant-per-method, withTenantTransaction-multi-step, pointer-drag-with-threshold] +key-files: + created: + - apps/api/prisma/migrations/20260923120000_dashboard_tabs/migration.sql + - apps/api/src/dashboard/dto/rename-dashboard.dto.ts + - apps/api/src/dashboard/dto/reorder-dashboards.dto.ts + - apps/api/src/dashboard/dashboard.controller.spec.ts + - apps/web/src/components/dashboard/dashboard-tabs.tsx + - apps/web/src/components/dashboard/dashboard-tabs.test.tsx + modified: + - apps/api/prisma/schema.prisma + - apps/api/src/dashboard/dashboard.service.ts + - apps/api/src/dashboard/dashboard.service.spec.ts + - apps/api/src/dashboard/dashboard.controller.ts + - apps/api/src/dashboard/dto/save-layout.dto.ts + - apps/api/src/dashboard/dto/create-widget.dto.ts + - apps/api/src/dashboard/widget-module-map.spec.ts + - apps/web/src/lib/dashboard-api.ts + - apps/web/src/lib/stores/dashboard-store.ts + - apps/web/src/lib/stores/dashboard-store.test.ts + - apps/web/src/app/(portal)/page.tsx + - apps/web/src/app/(portal)/page.test.tsx + - apps/web/src/app/(portal)/settings/dashboard/page.tsx + - apps/web/src/messages/de.json + - apps/web/src/messages/en.json + - docs/mandantentrennung-zugriffsklassifikation.md + - docs/anleitung-anwender.md + - CHANGELOG.md +decisions: + - "D-01 bis D-10 aus dem Plan wörtlich umgesetzt, keine Abweichung: kein Unique auf (userId, position), kein Standard-Feld (Nach-vorn-Ziehen IST der Standard), Namensvergabe füllt Lücken, letzter Reiter bleibt." + - "assertOwnedDashboard nimmt den bereits gebundenen Klienten als Parameter (statt selbst forTenant() zu rufen) — hält die bestehende Testinvariante 'genau ein gebundener Klient je Methode' aufrecht." + - "Reiterwechsel: ungespeicherte Anordnung wird über get().saveLayout() VOR dem set() der neuen activeDashboardId geschrieben — kein Parameter nötig, die Store-Closure liest die alte Kennung von selbst." + - "Ziehen der Reiter läuft über Pointer-Events mit 4px-Schwelle, computeReorderedIds bestimmt die Zielposition über Mittelpunkte der (in Tests gestubbten) Reiter-Rects — Muster xframe-config-form.tsx, keine neue Abhängigkeit (D-05)." +metrics: + duration: "~2.5h" + completed: 2026-09-23 +actuals: + tokens: 45514 + tasks: 5 + commits: 5 +plan_head_before: 84fe73e +status: complete +--- + +# Phase quick-260923-ad9 Plan 01: Dashboard-Reiter — mehrere Dashboards je Benutzer Summary + +Jeder Benutzer hat jetzt mehrere Dashboards ("Reiter"), oben nebeneinander in einer Leiste, jeder mit eigenen Kacheln und eigener Anordnung, per Ziehen umsortierbar, der erste geladen beim Öffnen — bestehende Kacheln landeten unverändert auf einem einzigen Reiter "Dashboard". + +## Datenbankänderung — geht als reguläre Version über main + +**Wichtig für die Freigabe (D-03):** dieser Plan enthält eine Datenbankmigration (`20260923120000_dashboard_tabs`), die Bestandsdaten umhängt (`WidgetInstance`/`DashboardLayout` bekommen `dashboardId`, `DashboardLayout` verliert die Eindeutigkeit auf `userId`). Das geht als reguläre Version über `main`, **nicht als Hotfix**. + +## Gemessene Torzahlen + +| Tor | Vorher (gemessen 23.09. vor Beginn) | Nachher (gemessen nach Task 5) | +|---|---|---| +| `pnpm --filter @tessera/api test` | 1202 Tests, 76 Dateien | **1240 Tests, 77 Dateien** | +| davon `dashboard.service.spec.ts` | 31 Tests | **61 Tests** | +| davon `dashboard.controller.spec.ts` | (Datei existierte nicht) | **8 Tests** (neu) | +| `rls-coverage.spec.ts` / `rls-access-inventory.spec.ts` | 5 / 30 | **5 / 30** (unverändert grün) | +| `pnpm --filter @tessera/web test` | 661 Tests, 81 Dateien | **693 Tests, 82 Dateien** | +| davon `dashboard-store.test.ts` | 6 Tests | **17 Tests** | +| davon `dashboard-tabs.test.tsx` | (Datei existierte nicht) | **21 Tests** (neu) | +| `dashboard-grid.test.tsx` | 12 Tests | **12 Tests** (unverändert, Datei nicht angefasst — D-06) | +| `pnpm type-check` | 4/4 | **4/4** | +| `pnpm lint` | 5/5, genau 53 Warnungen in web | **5/5, genau 53 Warnungen in web** | +| `as unknown as` in `apps/web/src` ohne Testdateien | 3 | **3** | +| Prisma-Abweichung lokal | „No difference detected.“ | **„No difference detected.“** | + +## Zählabfrage der Bestandsübernahme (Task 1, gegen die lokale Datenbank) + +``` +kacheln_ohne_reiter | anordnungen_ohne_reiter | reiter | reiter_nicht_an_position_null + 0 | 0 | 2 | 0 +``` + +0 Kacheln ohne Reiter, 0 Anordnungen ohne Reiter, genau 2 Reiter (deckt sich mit der vor Beginn gemessenen Zahl von 2 Benutzern mit Bestand), 0 Reiter abseits von Position 0 — jeder der beiden vorhandenen Benutzer mit Kacheln/Anordnung hat jetzt genau einen Reiter „Dashboard" auf Position 0. + +## Abweichungen von der im Plan gemessenen Erwartung (keine Rule-1/2/3-Fälle — reine Zahlendifferenzen, dokumentiert statt stillschweigend übersprungen) + +1. **`apps/web` Dateizahl 82 statt der für Task 4 erwarteten 83.** Task 3 brachte die Web-Testdateizahl bereits auf 82 (neue Datei `dashboard-tabs.test.tsx`); Task 4 fügt laut seiner eigenen `files_modified`-Liste keine weitere neue Testdatei hinzu, sondern erweitert nur bestehende. Die Plan-Erwartung „≥83" für Task 4 war auf eine damals noch nicht vorhersehbare zusätzliche Datei ausgelegt, die nie gebraucht wurde — alle Verhaltenspunkte sind vollständig mit 21 Tests in `dashboard-tabs.test.tsx` und 17 in `dashboard-store.test.ts` abgedeckt. +2. **`grep -c "react-grid-layout|dnd|sortable" apps/web/package.json` liefert 2 statt 1.** Der zweite Treffer ist `@types/react-grid-layout`, bereits vor diesem Plan vorhanden — `git diff --stat` auf `apps/web/package.json` über alle fünf Commits ist leer. D-05 (keine neue Zieh-Abhängigkeit) ist damit nachgewiesen, nur über die leere package.json-Diff statt über die im Plan vorausgesagte Grep-Zahl. + +## Auto-fixed Issues (Deviations, Rule 1/3) + +**1. [Rule 1] `widget-module-map.spec.ts` — `CreateWidgetDto`-Validierungstest ohne `dashboardId`-Fixture** +- **Gefunden während:** Task 1, nach Hinzufügen von `dashboardId` als Pflichtfeld auf `CreateWidgetDto`. +- **Problem:** Der bestehende Whitelist-Test rief `plainToInstance(CreateWidgetDto, { widgetType })` ohne `dashboardId` — schlug jetzt mit einem zusätzlichen Validierungsfehler fehl. +- **Fix:** `dashboardId: 'dash-1'` fest mitgegeben, Kommentar ergänzt. +- **Commit:** 9c51823 (Task 1) + +**2. [Rule 3] `settings/dashboard/page.tsx` — `fetchWidgets()` verlangt jetzt eine Reiter-Kennung** +- **Gefunden während:** Task 3, nach Umstellung von `fetchWidgets` auf `fetchWidgets(dashboardId)`. +- **Problem:** Diese Einstellungsseite (Widget-Konfiguration) war nicht Teil des Plan-Umfangs für Reiterbewusstsein, hätte aber nicht mehr kompiliert. +- **Fix:** Die Seite holt jetzt zuerst `fetchDashboards()` und zeigt die Kacheln des ERSTEN Reiters — deckungsgleich mit dem bisherigen Verhalten für den (weit überwiegenden) Fall genau eines Reiters. Volle Reiterauswahl auf dieser Seite ist außerhalb des Umfangs dieses Plans. +- **Commit:** d34f682 (Task 3) + +**3. [Rule 3] `(portal)/page.test.tsx` — `mockStore` ohne die neuen Reiter-Felder** +- **Gefunden während:** Task 3, nach Einbau von `` in `page.tsx`. +- **Problem:** `dashboards` wäre `undefined` gewesen — `DashboardTabs` hätte auf `.map` einer `undefined`-Liste geworfen. +- **Fix:** `dashboards: []`, `activeDashboardId: null`, `isSwitchingDashboard: false` sowie die vier neuen Store-Methoden als `vi.fn()` ergänzt. +- **Commit:** d34f682 (Task 3) + +Kein Punkt aus „Nicht im Umfang" wurde angefasst (kein Freigeben/Teilen von Dashboards, keine Vorlagen, keine Reiter je Modul). Keine neue Abhängigkeit in `apps/web/package.json` oder `apps/api/package.json`. `DashboardGrid` selbst ist unverändert. + +## Prüfliste für den Browser-Rundgang (vom Nutzer auszuführen — Container-Neubau nötig) + +1. Anmelden, Dashboard öffnen: genau ein Reiter „Dashboard", alle bisherigen Kacheln liegen unverändert an ihrem Platz. +2. Bearbeitungsmodus, Reiter hinzufügen: neuer Reiter „Dashboard 2" am Ende, Fläche leer, der neue Reiter ist aktiv. +3. Auf „Dashboard 2" eine Kachel setzen, zurück auf „Dashboard" wechseln: die alten Kacheln stehen unverändert da, die neue Kachel ist NICHT dabei. +4. Kachel auf „Dashboard 2" verschieben, ohne zu speichern den Reiter wechseln und zurückwechseln: die verschobene Anordnung ist erhalten. +5. „Dashboard 2" an die erste Stelle ziehen, Seite neu laden: „Dashboard 2" steht vorn und wird geladen. +6. „Dashboard 2" umbenennen, Seite neu laden: der neue Name steht da. +7. „Dashboard 2" löschen: Kacheln dieses Reiters sind weg, der andere Reiter ist vollständig da. +8. Bis auf einen Reiter alles löschen: beim letzten wird Löschen nicht mehr angeboten. +9. Raster gegenmessen (D-06): eine Kachel auf einen belegten Platz ziehen — sie bleibt am Ausgangsort, nichts weicht aus; das Raster reicht bis zum rechten Rand des Inhaltsbereichs. + +## Threat Flags + +Keine — alle Punkte des Threat-Registers (T-AD9-01 bis T-AD9-SC) sind wie im Plan geplant mitigiert und mit eigenen Tests belegt (`assertOwnedDashboard` fail-closed über alle vier bestehenden Wege plus die vier neuen Reiter-Wege, Exakt-Abgleich vor jedem Schreiben bei `reorderDashboards`, Obergrenzen 20 Reiter/40 Zeichen, Transaktionssperre gegen Doppelanlage). Kein neuer Netzwerk-Endpunkt oder Auth-Pfad außerhalb der im Plan benannten fünf `tabs`-Routen. + +## Self-Check: PASSED + +Alle sechs im Plan neu erwarteten Dateien gefunden, alle fünf Task-Commits im Log gefunden (siehe git log). + +## Rundgang durch den Orchestrator (23.09.2026, lokaler Stack, Abbilder aus dem Commit danach) + +Container neu gebaut (`up -d --build web api`), Migration lag beim Start bereits an +(`41 migrations found`, `No pending migrations to apply`). Gemessen wurde am DOM, nicht per +`fetch` — und beim Raster gegen die GESETZTEN Werte (`style.width`), nicht gegen die gemalte +Box (Messfalle aus quick-260922-vdk). + +| # | Geprueft | Ergebnis | +|---|----------|----------| +| 1 | Bestand nach der Migration | EIN Reiter „Dashboard" mit allen **5** vorhandenen Kacheln — nichts verloren | +| 2 | Reiterleiste vorhanden | `