Files
schalli 0094a60d15
Tessera CI/CD / Lint & Type Check (push) Successful in 52s
Tessera CI/CD / Tests (push) Successful in 1m18s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 18s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m11s
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) <noreply@anthropic.com>
2026-09-23 08:29:50 +02:00

16 KiB

phase, verified, status, score, covered_files, covered_digest, behavior_unverified, overrides_applied, human_verification
phase verified status score covered_files covered_digest behavior_unverified overrides_applied human_verification
quick-260923-ad9 2026-09-23T08:30:00Z human_needed 10/10 must-haves verified
.planning/quick/260923-ad9-dashboard-reiter-mehrere-dashboards-je-b/260923-ad9-PLAN.md
.planning/quick/260923-ad9-dashboard-reiter-mehrere-dashboards-je-b/260923-ad9-SUMMARY.md
CHANGELOG.md
apps/api/prisma/migrations/20260923120000_dashboard_tabs/migration.sql
apps/api/prisma/schema.prisma
apps/api/src/dashboard/dashboard.controller.spec.ts
apps/api/src/dashboard/dashboard.controller.ts
apps/api/src/dashboard/dashboard.service.spec.ts
apps/api/src/dashboard/dashboard.service.ts
apps/api/src/dashboard/dto/create-widget.dto.ts
apps/api/src/dashboard/dto/rename-dashboard.dto.ts
apps/api/src/dashboard/dto/reorder-dashboards.dto.ts
apps/api/src/dashboard/dto/save-layout.dto.ts
apps/api/src/dashboard/widget-module-map.spec.ts
apps/web/src/app/(portal)/page.test.tsx
apps/web/src/app/(portal)/page.tsx
apps/web/src/app/(portal)/settings/dashboard/page.tsx
apps/web/src/components/dashboard/dashboard-tabs.test.tsx
apps/web/src/components/dashboard/dashboard-tabs.tsx
apps/web/src/lib/dashboard-api.ts
apps/web/src/lib/stores/dashboard-store.test.ts
apps/web/src/lib/stores/dashboard-store.ts
apps/web/src/messages/de.json
apps/web/src/messages/en.json
docs/anleitung-anwender.md
docs/mandantentrennung-zugriffsklassifikation.md
v1:sha256:9504969e14709ebba347c4443d9b00de67a4cbdc10aa49695103728d822abec9 0 0
test expected why_human
Browser-Rundgang Punkte 1-9 aus dem SUMMARY (Anmelden, Reiter anlegen, Kachel setzen und Reiter wechseln, ungespeichert wechseln, per Ziehen an erste Stelle, umbenennen, löschen, letzter Reiter, Raster gegenmessen) Alle neun Punkte laufen wie im SUMMARY beschrieben, insbesondere Punkt 9 (Raster reagiert unverändert, D-06) Erfordert einen laufenden Container mit echtem Datenbestand und echte Maus-Interaktion (Drag-and-Drop, visuelle Prüfung des Rasterverhaltens) — kann nicht durch Grep/Codeanalyse ersetzt werden. Der Nutzer baut/startet die Container selbst (Projektregel: kein Docker-Deploy durch Claude auf Testservern/lokal).

Quick-Aufgabe 260923-ad9: Dashboard-Reiter — mehrere Dashboards je Benutzer Verification Report

Phase Goal: Dashboard-Reiter — mehrere Dashboards je Benutzer, jeder Reiter mit eigenen Kacheln und eigener Anordnung; Reiter per Ziehen sortierbar; der erste Reiter ist der Standard und wird beim Öffnen geladen; anlegen, umbenennen, löschen (der letzte bleibt); bestehende Dashboards werden per Migration zum ersten Reiter, ohne dass jemand Kacheln verliert.

Verified: 2026-09-23T08:30:00Z Status: human_needed Re-verification: No — initial verification

Goal Achievement

Observable Truths

# Truth Status Evidence
1 Ein Benutzer hat mehrere Dashboards als Reiter, jeder mit eigenen Kacheln/Anordnung ✓ VERIFIED Dashboard model + dashboardId FK on WidgetInstance/DashboardLayout (schema.prisma:200-253); getWidgets/getLayout/addWidget/saveLayout all scope by dashboardId, not userId (dashboard.service.ts); dashboard-store.ts selectDashboard fully replaces layouts/widgets on tab switch, tests 9/10 in dashboard-store.test.ts confirm no merging
2 Erster Reiter (Position 0) ist Standard, wird beim Öffnen geladen, kein separates Standard-Feld ✓ VERIFIED listDashboards orders by position: 'asc', loadDashboard() in store takes dashboards[0]; no isDefault/starred field anywhere in schema; store test 7 confirms
3 Reiter per Maus ziehbar, Reihenfolge bleibt nach Neuladen; Klick ohne Ziehen wechselt nur ✓ VERIFIED dashboard-tabs.tsx pointer handlers with 4px threshold, computeReorderedIds; PUT /dashboard/tabs/order persists via reorderDashboards; tabs tests 13-21 cover click-only, drag right/left, preview/abort, first-tab promotion, save-failure rollback
4 Anlegen (Namensvergabe füllt Lücken), Umbenennen, Löschen (letzter bleibt, Server + UI) ✓ VERIFIED createDashboard gap-filling name loop (dashboard.service.ts); deleteDashboard throws ConflictException at count≤1; dashboard-tabs.tsx omits delete button when dashboards.length <= 1; service tests for last-tab-conflict and controller/service tests for rename/delete found
5 Migration: niemand verliert Kacheln/Anordnung; Benutzer ohne Bestand bekommt leeren Reiter beim ersten Öffnen ✓ VERIFIED Migration backfills exactly one Dashboard row per user with widgets-or-layout via DISTINCT ON, runs backfill before FKs; live-DB query returned 0 orphaned widgets, 0 orphaned layouts, 2 dashboards (matches pre-measured user count), 0 dashboards off position 0 (re-run by this verifier, see below); listDashboards auto-creates one tab for a user with none
6 Fail-closed gegen fremde Reiter auf allen sieben Wegen (read/write) ✓ VERIFIED assertOwnedDashboard called first in getLayout, saveLayout, getWidgets, addWidget, renameDashboard, deleteDashboard; reorderDashboards uses exact-match-in-transaction (same NotFoundException/BadRequestException, no existence oracle); dedicated tests found for all 7+ paths (grep: 8 "fremd/NotFound" test names across the exact methods)
7 Neue Tabelle trägt Mandant+Zeilenschutz (Form aus 20260911120000); RLS-Wächter grün; Zugriffsklassifikation nachgeführt ✓ VERIFIED Migration: ENABLE+FORCE ROW LEVEL SECURITY + tenant_isolation_policy with tenant AND user dimension; rls-coverage.spec.ts (generic schema/migration scanner, not hardcoded) passed 5/5 in this verifier's own full test run; docs/mandantentrennung-zugriffsklassifikation.md recomputed rows for dashboard/dashboardLayout/widgetInstance pairs, region and sum lines
8 Raster unverändert: FREE_PLACEMENT_COMPACTOR/preventCollision, Breitenmessung aus quick-260922-vdk ✓ VERIFIED git diff 84fe73e..HEAD --stat -- apps/web/src/components/dashboard/ shows only two NEW files (dashboard-tabs.tsx/.test.tsx); dashboard-grid.tsx has zero diff; FREE_PLACEMENT_COMPACTOR/preventCollision present unchanged; dashboard-grid.test.tsx stayed at 12 tests
9 Keine neue Abhängigkeit für das Ziehen (Pointer-Events wie xframe-config-form.tsx) ✓ VERIFIED git diff 84fe73e..HEAD -- apps/web/package.json apps/api/package.json is empty (no diff at all); dashboard-tabs.tsx uses native PointerEvent/setPointerCapture with jsdom guard, same pattern as xframe-config-form.tsx
10 Alle Tore grün mit den genannten Mindestzahlen ✓ VERIFIED Re-run by this verifier (not trusted from SUMMARY): api 1240/1240 tests in 77 files; web 693/693 tests in 82 files; pnpm type-check 4/4; pnpm lint 5/5 with exactly 53 warnings; prisma migrate diff --exit-code against live local DB returned "No difference detected." (exit 0)

Score: 10/10 truths verified (0 present, behavior-unverified)

Required Artifacts

Artifact Expected Status Details
apps/api/prisma/schema.prisma Dashboard model, FK on WidgetInstance/DashboardLayout, no unique on position, DashboardLayout loses userId-unique ✓ VERIFIED Confirmed by direct read: @@index([userId]), @@index([tenantId]), no @@unique; DashboardLayout.dashboardId @unique, userId plain index
.../migrations/20260923120000_dashboard_tabs/migration.sql Hand-written, German header, RLS, backfill before FKs ✓ VERIFIED Confirmed by direct read: steps in the documented order, DISTINCT ON dedup for the "same user, two tenants" edge case on the INSERT (see minor note below)
apps/api/src/dashboard/dashboard.service.ts listDashboards/createDashboard/renameDashboard/deleteDashboard/reorderDashboards/assertOwnedDashboard; getLayout/saveLayout/getWidgets/addWidget per-tab ✓ VERIFIED All methods present, matches plan's documented locking/transaction reasoning
apps/api/src/dashboard/dashboard.controller.ts Five new tabs routes, tabs/order before :id routes ✓ VERIFIED Confirmed by direct read and by the passing source-order guard test in dashboard.controller.spec.ts
apps/api/src/dashboard/dto/ rename/reorder DTOs with caps, extended save-layout/create-widget DTOs ✓ VERIFIED RenameDashboardDto (trim + Length(1,40)), ReorderDashboardsDto (ArrayMinSize/MaxSize(20)/Unique)
apps/api/src/dashboard/dashboard.controller.spec.ts NEW, incl. source-order guard ✓ VERIFIED File exists, 8 tests, guard test present and passing
apps/web/src/components/dashboard/dashboard-tabs.tsx NEW tab bar, pointer-drag pattern ✓ VERIFIED Confirmed by direct read; wired into (portal)/page.tsx
apps/web/src/lib/stores/dashboard-store.ts dashboards/activeDashboardId/select/create/rename/delete/reorder, dedupe-load, save-before-switch ✓ VERIFIED Confirmed by direct read
apps/web/src/messages/de.json + en.json widgets.tabs.* keys, real umlauts ✓ VERIFIED Confirmed keys present with real umlauts (ä/ö/ü/ß)
docs/mandantentrennung-zugriffsklassifikation.md new row, recomputed sums ✓ VERIFIED Confirmed by direct read; numbers are internally consistent and recomputed with rationale, not copy-pasted
docs/anleitung-anwender.md + CHANGELOG.md plain-language description ✓ VERIFIED Confirmed by direct read; real German, Sie-form, no jargon
From To Via Status Details
Open → loadDashboard() → GET /dashboard/tabs → first tab active → widgets+layout fetch — store→api→controller→service ✓ WIRED Confirmed end-to-end by reading dashboard-store.ts loadDashboard, dashboard-api.ts, controller, service
Drag tabs → pointer events → PUT /dashboard/tabs/order → position rewrite in one transaction — tabs.tsx→store→api→service ✓ WIRED Confirmed; reorderDashboards service method matches FavoritesService.reorder pattern exactly
Delete tab → DELETE /dashboard/tabs/:id → assertOwnedDashboard → last-tab reject → transactional cascade delete + position renumber — tabs.tsx→store→api→service ✓ WIRED Confirmed by direct read of deleteDashboard
Add widget → store passes activeDashboardId → POST /dashboard/widgets with tab id → ownership check — store→api→service ✓ WIRED Confirmed addWidget in store reads activeDashboardId, service calls assertOwnedDashboard first
New Dashboard model with tenantId → rls-coverage.spec.ts requires ENABLE+Policy — migration→generic scanner ✓ WIRED Confirmed test passed in this verifier's own run (5/5), scanner is generic (parses schema+migrations dynamically, not hardcoded per table)
tenantPrisma.dashboard/tx.dashboard → rls-access-inventory.spec.ts → classification doc entry — service→inventory scanner→doc ✓ WIRED Confirmed test passed (30/30 in this verifier's run); doc row present with gebunden status

Data-Flow Trace (Level 4)

Artifact Data Variable Source Produces Real Data Status
dashboard-tabs.tsx dashboards prop useDashboardStore().dashboards GET /dashboard/tabs → Prisma query via forTenant Yes ✓ FLOWING
DashboardGrid layouts/widgets props store layouts/widgets GET /dashboard/layout/GET /dashboard/widgets scoped by activeDashboardId Yes ✓ FLOWING
Migration backfill counts (live DB) Dashboard/WidgetInstance/DashboardLayout rows Actual local Postgres container, re-queried by this verifier Yes ✓ FLOWING

Behavioral Spot-Checks

Behavior Command Result Status
API full test suite (not filtered) pnpm --filter @tessera/api test 1240 tests, 77 files, all passed ✓ PASS
Web full test suite (not filtered) pnpm --filter @tessera/web test 693 tests, 82 files, all passed ✓ PASS
pnpm type-check pnpm type-check 4/4 successful (cached) ✓ PASS
pnpm lint pnpm lint 5/5 successful, exactly 53 warnings in web ✓ PASS
Migration applied + no drift vs. live local DB prisma migrate diff --exit-code "No difference detected.", exit 0 ✓ PASS
Backfill correctness (live DB re-query) SELECT ... kacheln_ohne_reiter, anordnungen_ohne_reiter, reiter, reiter_nicht_an_position_null 0, 0, 2, 0 ✓ PASS
No new drag/DnD dependency git diff 84fe73e..HEAD -- apps/web/package.json apps/api/package.json empty diff ✓ PASS
Grid component untouched git diff 84fe73e..HEAD --stat -- apps/web/src/components/dashboard/ only 2 new files (dashboard-tabs.*), dashboard-grid.tsx absent from diff ✓ PASS

Requirements Coverage

This is a /gsd-quick task (no .planning/REQUIREMENTS.md entry expected/found for QUICK-260923-AD9 — confirmed by grep, consistent with how quick tasks are tracked in this project).

Anti-Patterns Found

No TBD/FIXME/XXX/TODO/HACK/PLACEHOLDER markers found in any of the core changed files (dashboard.service.ts, dashboard.controller.ts, dashboard-store.ts, dashboard-tabs.tsx, migration.sql). No stub patterns (return null/empty-return handlers/hardcoded-empty props) found in the reviewed files — every prop and returned value traces to a real query or real store state.

Minor observation (not a blocker): the migration's backfill INSERT (step 3) uses DISTINCT ON ("userId") to guarantee exactly one Dashboard row per user even in the theoretical "same user id, two tenant ids" case, exactly as the plan requires for that INSERT. However, the subsequent UPDATE "WidgetInstance"/UPDATE "DashboardLayout" backfill steps (4 and 5) join only on d."userId" = wi."userId", not also on tenantId — in that same theoretical edge case, rows belonging to the "losing" tenant would be reassigned to the single Dashboard row's tenant. The plan's own task text explicitly scopes the dedup requirement ("Gegen den theoretischen Fall...") to the INSERT's row selection, and the live local database has no such multi-tenant-same-user rows (measured backfill: 2 dashboards for 2 users with data, 0 orphans). This does not block the phase goal — it is a pre-existing, explicitly acknowledged theoretical edge case, not a regression — but is noted here for the record since it touches the "niemand verliert etwas" must-have's edge-case robustness, not its measured/observed correctness.

Human Verification Required

  1. Browser-Rundgang (9 Punkte aus dem SUMMARY) Test: Anmelden und Dashboard öffnen; Reiter anlegen/wechseln/Kachel setzen; ungespeichert wechseln; per Ziehen an die erste Stelle bringen und neu laden; umbenennen; löschen; letzten Reiter prüfen; Raster gegenmessen (D-06: Kachel auf belegten Platz ziehen — bleibt am Ausgangsort). Expected: Alle neun Punkte laufen wie im SUMMARY beschrieben. Why human: Erfordert einen neu gebauten laufenden Container mit echtem Datenbestand und echte Maus-Interaktion — Drag-and-Drop-Verhalten und visuelle Rasterreaktion lassen sich nicht per Codeanalyse abschließend beurteilen, und laut Projektregel baut/startet der Nutzer die Container selbst.

Gaps Summary

None. Every must-have truth from the plan's frontmatter, plus the four explicit verification-focus points requested (migration completeness, fail-closed ownership, grid untouched, the three auto-fixed files), is backed by direct code reading and/or a freshly re-run, non-trusted measurement (full test suites, type-check, lint, live-DB migration diff, live-DB backfill re-query, git diff on package.json and on the dashboard components directory). All three "Rule 1/3" auto-fixed files documented in the SUMMARY were read directly and are correct, narrowly-scoped fixes that do not hide a gap — they were compile/test breakages caused by the new required dashboardId field, fixed with reasoning matching what SUMMARY claims. The only finding is the minor, pre-acknowledged theoretical edge case noted above under Anti-Patterns, which does not affect the phase goal as measured against the actual local database.


Verified: 2026-09-23T08:30:00Z Verifier: Claude (gsd-verifier)