7a6f42ea74
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
358 lines
31 KiB
Markdown
358 lines
31 KiB
Markdown
---
|
||
phase: quick-260916-bwo
|
||
plan: 01
|
||
subsystem: ui, dashboard
|
||
tags: [dashboard, react-grid-layout, container-queries, tailwind, zustand, vitest, i18n]
|
||
|
||
requires:
|
||
- phase: 08-dashboard-widgets
|
||
provides: WIDGET_CONSTRAINTS, Uhr/Stoppuhr/Rechner-Widgets, Dashboard-Store, Widget-Einstellungen
|
||
provides:
|
||
- "Dashboard-Raster 24/20/12/8/2 Spalten, 20 px Zeilen, 8 px Abstand; alle 32 Widget-Konstanten verdoppelt"
|
||
- "Einmalige Umrechnung gespeicherter Anordnungen (grid-layout-migration.ts) mit Marker __gridVersion: 2 im gespeicherten JSON, nie im Zustand"
|
||
- "Uhr, Stoppuhr, Rechner skalieren per CSS-Container-Queries mit der Kachel (Widget-Rumpf ist @container-size)"
|
||
- "Uhr: optionale feste Schriftgroesse in Punkt (timeFontSizePt, 8..200, leer = automatisch) mit Zahlenfeld unter Einstellungen -> Dashboard"
|
||
- "Abstaende halbiert: Seitenrahmen p-3 (alle Seiten), Dashboard p-2, Grid 8 px, Widget-Innenabstaende"
|
||
affects: [dashboard, alle-seiten-rahmen, anwenderhandbuch]
|
||
|
||
actuals:
|
||
tokens: 72941
|
||
tasks: 3
|
||
commits: 4
|
||
plan_head_before: 50f201ecc1292232ff96e13226b62386cfc36e76
|
||
|
||
tech-stack:
|
||
added: []
|
||
patterns:
|
||
- "Marker im persistierten JSON, nie im Zustand: Store entfernt beim Laden, haengt bei JEDEM Speichern an (Idempotenz-Test + Store-Test pinnen es)"
|
||
- "Container-Query-Schriftgroessen als Tailwind-Klassen mit clamp()-Grenzen (jsdom verwirft clamp() im Inline-Style), feste Groesse als Inline-Style nur aus einer geprueften Zahl"
|
||
- "Grenzen fuer ein Config-Feld in EINER Quelldatei (clock-font-size.ts), von Formular und Widget benutzt"
|
||
- "react-grid-layout-Mock faengt Props per vi.hoisted ein, damit Raster-Konstanten testbar sind"
|
||
|
||
key-files:
|
||
created:
|
||
- apps/web/src/lib/grid-layout-migration.ts
|
||
- apps/web/src/lib/grid-layout-migration.test.ts
|
||
- apps/web/src/lib/stores/dashboard-store.test.ts
|
||
- apps/web/src/components/dashboard/widgets/clock-font-size.ts
|
||
- apps/web/src/components/settings/widget-settings-panel.test.tsx
|
||
modified:
|
||
- apps/web/src/components/dashboard/dashboard-grid.tsx
|
||
- apps/web/src/components/dashboard/dashboard-grid.test.tsx
|
||
- apps/web/src/components/dashboard/widget-registry.tsx
|
||
- apps/web/src/components/dashboard/widget-registry.test.tsx
|
||
- apps/web/src/lib/stores/dashboard-store.ts
|
||
- apps/api/src/dashboard/dashboard.service.spec.ts
|
||
- apps/web/src/components/dashboard/widgets/widget-wrapper.tsx
|
||
- apps/web/src/components/dashboard/widgets/clock-widget.tsx
|
||
- apps/web/src/components/dashboard/widgets/clock-widget.test.tsx
|
||
- apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx
|
||
- apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx
|
||
- apps/web/src/components/dashboard/widgets/calculator-widget.tsx
|
||
- apps/web/src/components/dashboard/widgets/calculator-widget.test.tsx
|
||
- apps/web/src/components/settings/widget-settings-panel.tsx
|
||
- apps/web/src/messages/de.json
|
||
- apps/web/src/messages/en.json
|
||
- apps/web/src/components/dashboard/widgets/link-widget.tsx
|
||
- apps/web/src/components/dashboard/widgets/favorites-widget.tsx
|
||
- apps/web/src/components/dashboard/widgets/calendar-widget.tsx
|
||
- apps/web/src/components/dashboard/widgets/search-widget.tsx
|
||
- apps/web/src/components/dashboard/widgets/note-widget.tsx
|
||
- apps/web/src/app/(portal)/page.tsx
|
||
- apps/web/src/components/layout/app-shell.tsx
|
||
- docs/anleitung-anwender.md
|
||
|
||
key-decisions:
|
||
- "Umrechnung im Frontend (neben COLS), nicht in der API: Einheiten sind Frontend-Konstanten, kein Schreiben auf GET, API-Dienst/DTOs unangetastet, reine Funktion ohne Prisma testbar; Preis: Marker persistiert erst mit gelungenem PUT, bis dahin rechnet jedes Laden erneut aus den unveraendert alten DB-Werten (korrekt, nie halb geschrieben)"
|
||
- "Marker __gridVersion nur im gespeicherten JSON, nie im Zustand (Store iteriert mit Object.keys + .filter); withGridVersion an BEIDEN Speicherstellen"
|
||
- "mt-8 vor dem Grid bleibt: Umschalter 36 px hoch (p-2 + 20-px-Symbol) bei 8..44 px; mit mt-4 begaenne das erste Widget bei 32 px im Umschalter, mit mt-8 bei 48 px"
|
||
- "Automatische Schriftgroesse als Tailwind-Klasse (clamp/min/cqw/cqh), feste Punktgroesse als Inline-Style aus einer geprueften Zahl — Trennung wegen jsdom UND als einfachere Form"
|
||
- "Listen-/Formular-Widgets (note, calendar, favorites, link, search) skalieren bewusst NICHT — mehr Platz zeigt mehr Inhalt, nicht groessere Schrift"
|
||
|
||
patterns-established:
|
||
- "Einmalige Datenumrechnung im Client: reine Funktion + Marker + Idempotenz-Test + Store-Test 'jedes Speichern traegt den Marker'"
|
||
|
||
requirements-completed: [QUICK-260916-BWO]
|
||
|
||
coverage:
|
||
- id: D1
|
||
description: "Raster 24/20/12/8/2, rowHeight 20, margin 8, 32 Konstanten verdoppelt, data-grid-Vorgaben"
|
||
requirement: QUICK-260916-BWO
|
||
verification:
|
||
- kind: unit
|
||
ref: "apps/web/src/components/dashboard/dashboard-grid.test.tsx#Test 4-5, widget-registry.test.tsx#Tabelle (Falsifizierung c)"
|
||
status: pass
|
||
human_judgment: false
|
||
- id: D2
|
||
description: "Einmalige Umrechnung mit Marker, Store rechnet um und speichert sofort, Marker bei jedem Speichern, API reicht durch"
|
||
requirement: QUICK-260916-BWO
|
||
verification:
|
||
- kind: unit
|
||
ref: "apps/web/src/lib/grid-layout-migration.test.ts#Test 1-7 (Falsifizierung a), dashboard-store.test.ts#Test 1-6, dashboard.service.spec.ts#Test A-B"
|
||
status: pass
|
||
human_judgment: true
|
||
rationale: "Dass eine ALTE Anordnung im Browser an derselben optischen Stelle bleibt und nach zwei Ladevorgaengen nicht erneut verdoppelt wird, zeigt nur die SQL-Probe im Browser (unten)"
|
||
- id: D3
|
||
description: "Container-Query-Skalierung Uhr/Stoppuhr/Rechner, feste Punktgroesse, Einstellungsfeld, i18n"
|
||
requirement: QUICK-260916-BWO
|
||
verification:
|
||
- kind: unit
|
||
ref: "clock-widget.test.tsx#Test 3-6 (Falsifizierung b), stopwatch-widget.test.tsx#+1, calculator-widget.test.tsx#+1, widget-settings-panel.test.tsx#Test 1-4, umlaut-guard.spec.ts"
|
||
status: pass
|
||
human_judgment: true
|
||
rationale: "jsdom rechnet keine Container-Queries; ob cqh im Browser aufloest (definite Hoehe) und die Uhr sichtbar mitwaechst, muss der Browser zeigen"
|
||
- id: D4
|
||
description: "Abstaende halbiert (Seitenrahmen, Dashboard, Widget-Ruempfe), Handbuch"
|
||
requirement: QUICK-260916-BWO
|
||
verification:
|
||
- kind: other
|
||
ref: "grep-Gates Task 2/3 (Klassen, Handbuch 2/1)"
|
||
status: pass
|
||
human_judgment: true
|
||
rationale: "Bounding-Boxen (28 px Rand, 8 px Luecke, 60 px oben, 12 px auf /admin/users) nur im Browser messbar"
|
||
|
||
duration: "18 min (07:04Z bis 07:22Z, davon ca. 4 min Suiten-Laeufe und 5,5 min CI-Beobachtung)"
|
||
completed: "2026-09-16"
|
||
status: complete
|
||
---
|
||
|
||
# Quick 260916-bwo Plan 01: Dashboard — feineres Raster, skalierende Widget-Inhalte, halbe Abstaende — Summary
|
||
|
||
Das Dashboard-Raster ist doppelt so fein (24 statt 12 Spalten, 20 statt 40 px Zeilenhoehe, 8 statt 16 px Abstand) bei optisch gleich grossen Widgets (alle 32 Konstanten in `WIDGET_CONSTRAINTS` verdoppelt und im Test festgenagelt); gespeicherte Anordnungen in alten Einheiten werden beim Laden GENAU EINMAL mit 2 multipliziert und mit `__gridVersion: 2` im gespeicherten JSON markiert (reine Funktion `migrateGridLayouts`, Marker nie im Zustand, Idempotenz-Test); der Widget-Rumpf ist ein CSS-Groessen-Container, sodass Uhrzeit, Stoppuhr-Anzeige und Rechner (Anzeige und Tasten) per `cqw`/`cqh` mit der Kachel wachsen; die Uhrzeit laesst sich unter Einstellungen -> Dashboard -> Uhr in Punkt fest einstellen (8..200, leer = automatisch, Fremdwerte fallen auf automatisch zurueck, T-BWO-01); Seitenrahmen (`app-shell.tsx`, alle Seiten), Dashboard-Container, Grid-Abstand und Widget-Innenabstaende sind halbiert. Vier Commits auf `main` (`3f5afb0`, `2d8efe1`, `a175c00`, `1aefaa3`), gepusht, CI-Lauf 351 `success` in 5 min 26 s, `api:beta` traegt `1aefaa3 beta`.
|
||
|
||
## Ausgangslage und Bezugspunkt
|
||
|
||
Alle Gates gegen `5f50c5f` (Code unangetastet seit Planung; HEAD bei Start `50f201e` = Plan-Commit, Arbeitsbaum sauber). `git status -sb` bei Start: `## main...origin/main [voraus 2]` — die beiden Plan-Commits des Orchestrators (`7eb2516`, `50f201e`) waren noch nicht gepusht; sie gingen mit dem Push in Task 3 mit (`5f50c5f..1aefaa3`). Vorbedingung Task 3: `curl localhost:3002/api/v1/version` -> `{"version":"1.26.2"}`, `docker ps | grep -c '^gitea-runner$'` -> `1`.
|
||
|
||
Baseline vor jeder Aenderung (frisch gemessen, identisch mit der Planung):
|
||
|
||
| Suite | Vorher | Nachher (Ziel des Plans) |
|
||
|---|---|---|
|
||
| Web `pnpm -C apps/web exec vitest run` | `Test Files 43 passed (43)` / `Tests 260 passed (260)` | `Test Files 46 passed (46)` / `Tests 286 passed (286)` |
|
||
| API `pnpm -C apps/api exec vitest run` | `Test Files 67 passed (67)` / `Tests 1076 passed (1076)` | `Test Files 67 passed (67)` / `Tests 1078 passed (1078)` |
|
||
| `tsc --noEmit` shared / api / web | 0 / 0 / 0 | 0 / 0 / 0 |
|
||
|
||
Bestand an Anordnungen (nur lesend, `docker exec tessera-ctl-db-1 psql -U tessera -d tessera -At`): lokal `DashboardLayout` 0, `WidgetInstance` 0 (erneut gemessen 07:19Z); alpha 0/0 laut Planung (nicht erneut gemessen); live nicht gemessen — die Umrechnung bleibt Pflicht.
|
||
|
||
## Task 1 — Raster verdoppelt, Umrechnung, Store, API-Durchreich-Specs (`3f5afb0`, 9 Dateien)
|
||
|
||
**RED (Schritt A)** `pnpm -C apps/web exec vitest run src/lib/grid-layout-migration.test.ts src/lib/stores/dashboard-store.test.ts src/components/dashboard`:
|
||
|
||
```
|
||
FAIL src/lib/grid-layout-migration.test.ts — Failed to resolve import "./grid-layout-migration"
|
||
× dashboard-store Test 1: expected { i: 'a', x: 1, y: 1, w: 2, h: 2 } to deeply equal { i: 'a', x: 2, y: 2, w: 4, h: 4 }
|
||
× dashboard-store Test 2: expected [ 'lg', 'md', 'sm', 'xs', 'xxs', …(1) ] to not include '__gridVersion'
|
||
× dashboard-store Test 4: expected "vi.fn()" to be called with arguments: [ ObjectContaining{…} ]
|
||
× dashboard-store Test 5: expected 1 to be 2
|
||
× dashboard-store Test 6: expected [ { i: 'n1', x: +0, y: +0, …(2) } ] to deep equally contain { i: 'n1', x: +0, y: +0, w: 4, h: 4 }
|
||
× dashboard-grid Test 4: expected { Object (lg, md, ...) } to deeply equal { lg: 24, md: 20, sm: 12, xs: 8, …(1) }
|
||
× dashboard-grid Test 5: expected { x: +0, y: +0, w: 2, h: 2, …(2) } to deeply equal { x: +0, y: +0, w: 4, h: 4, …(2) }
|
||
× widget-registry Tabelle: expected { …(8) } to deeply equal { …(8) }
|
||
Test Files 4 failed | 8 passed (12)
|
||
Tests 8 failed | 55 passed (63)
|
||
```
|
||
|
||
(Store-Test 3 „leer -> kein Speichern“ war nach altem Code bereits gruen — erwartbar, alter Code speicherte beim Laden nie.)
|
||
|
||
**API-Spec (+2)** `pnpm -C apps/api exec vitest run src/dashboard/dashboard.service.spec.ts` -> `Test Files 1 passed (1)` / `Tests 31 passed (31)` (vorher 29 `it(`-Bloecke). Beide neuen Tests waren nach heutigem Code sofort gruen — das ist der Beleg der Durchreichung (`@IsObject()`, `return record.layouts`, `{ ...widget.config, ...dto.config }`), kein RED.
|
||
|
||
**GREEN (Schritt E)** und Verify-Block:
|
||
|
||
```
|
||
Web (4 Dateien): Test Files 4 passed (4) / Tests 29 passed (29)
|
||
API: Test Files 1 passed (1) / Tests 31 passed (31)
|
||
greps: COLS 1, rowHeight 1, margin 1, clock 1, search 1, calculator 1, withGridVersion( 2, migrateGridLayouts( 1, GRID_VERSION 1
|
||
U_EXIT=0 U_EMPTY=0 (dashboard.service.ts, dto/, prisma/ unangetastet)
|
||
TSC_web=0 TSC_api=0
|
||
```
|
||
|
||
**Messung widerspricht dem Plan (beide festgehalten):** Der Plan nennt fuer die vier Web-Dateien `Tests 22 passed (22)` („5 + 4 + 7 + 6“); gemessen `29`. Ursache: `widget-registry.test.tsx` hatte vorher 10 Testfaelle (1 + `it.each` ueber 8 Typen + 1), der Plan zaehlte 3 `it(`-Bloecke. Mit +1 sind es 11 (`pnpm -C apps/web exec vitest run src/components/dashboard/widget-registry.test.tsx` -> `Tests 11 passed (11)`): 5 + 11 + 7 + 6 = 29. Die Zielzahl der Gesamtsuite (260 + 26 = 286) ist davon nicht betroffen und wurde exakt erreicht.
|
||
|
||
## Task 2 — Teil 2a: Skalierung, Punktgroesse, i18n (`2d8efe1`, 12 Dateien)
|
||
|
||
**RED (Schritt A)** `pnpm -C apps/web exec vitest run src/components/dashboard/widgets src/components/settings/widget-settings-panel.test.tsx`:
|
||
|
||
```
|
||
FAIL src/components/dashboard/widgets/clock-widget.test.tsx — Failed to resolve import "./clock-font-size"
|
||
× stopwatch quick-260916-bwo: expected 'font-mono font-semibold tabular-nums …' to match /cqw/
|
||
× calculator quick-260916-bwo: expected 'flex min-h-[2.5rem] items-end justify…' to match /cqw/
|
||
× widget-settings-panel Test 1-4: It looks like undefined was passed instead of a matcher (de.widgets.clock.fontSizeLabel fehlte noch)
|
||
Test Files 4 failed | 5 passed (9)
|
||
Tests 6 failed | 39 passed (45)
|
||
```
|
||
|
||
**GREEN (Schritt H)** `... src/components/dashboard/widgets src/components/settings/widget-settings-panel.test.tsx src/messages` -> `Test Files 11 passed (11)` / `Tests 57 passed (57)`; je Datei: clock 6, stopwatch 8, calculator 7, widget-settings-panel 4, `src/messages` 6 (Umlaut-Waechter und tenderRadar-Paritaet gruen OHNE Aenderung an `umlaut-dictionary.ts`); `TSC_web=0`.
|
||
|
||
**Messung widerspricht dem Plan (beide festgehalten):** Der Plan nennt fuer Commit 2a „genau 11 Dateien“, zaehlt in Klammern aber 12 auf (widget-wrapper, clock-font-size, clock + Test, stopwatch + Test, calculator + Test, widget-settings-panel + Test, de.json, en.json). `git show --stat 2d8efe1` -> `12 files changed`. 9 + 12 + 7 + 1 = 29 stimmt mit der Gesamtzahl des Plans ueberein; die „11“ ist ein Zaehlfehler des Plans, nicht des Commits.
|
||
|
||
## Task 2 — Teil 2b: Abstaende halbiert (`a175c00`, 7 Dateien)
|
||
|
||
Tabelle aus dem Plan eins zu eins umgesetzt (link 238 `p-1`, favorites 174 `p-1`, calendar 112 `p-1.5` und 80/91/102 `p-2`, search 89 `px-1.5`, note 89 `px-1.5 py-1.5`, page.tsx `relative p-2` / `right-2 top-2` / `mt-8` bleibt mit Kommentar, app-shell `main` `p-3` mit Kommentar).
|
||
|
||
**Verify-Block Task 2** (volle Web-Suite und 29 greps):
|
||
|
||
```
|
||
Test Files 46 passed (46) / Tests 286 passed (286)
|
||
@container-size 2 | Uhr-Klasse 1 | data-font-mode 1 | vw clock 0 | vw stopwatch 0 | cqw stopwatch 1 | cqw calculator 5 | h-full min-h-7 1
|
||
MIN 1 | MAX 1 | id=clock-font-size 1 | fontSizeLabel de 1 | fontSizeInvalid en 1 | D2_EXIT=0 DICT_UNCHANGED=0
|
||
relative p-2 1 | right-2 top-2 1 | "mt-8" 1 | app-shell p-3 2 | app-shell " p-6" 0 (nach Korrektur, s. Abweichung 2)
|
||
link 1 | favorites 1 | calendar Rumpf 1 | calendar Zustandsansichten 3 | search 1 | note 1 | clock 1 | stopwatch 1 | calculator 1 | TSC_web=0
|
||
```
|
||
|
||
## Task 3 — Handbuch, Abschluss-Gates, Push, CI (`1aefaa3`, 1 Datei)
|
||
|
||
`grep -c "Schriftgröße" docs/anleitung-anwender.md` -> `2`; `grep -c "feinen Schritten"` -> `1`.
|
||
|
||
Abschluss-Gates (nach dem Handbuch, vor dem Commit):
|
||
|
||
```
|
||
Web: Test Files 46 passed (46) / Tests 286 passed (286)
|
||
API: Test Files 67 passed (67) / Tests 1078 passed (1078)
|
||
TSC_packages/shared=0 TSC_apps/api=0 TSC_apps/web=0
|
||
FROZEN=0 (pnpm install --frozen-lockfile)
|
||
GIT_EXIT=0 29 files changed, 895 insertions(+), 60 deletions(-) (git diff --stat 5f50c5f -- . ':!.planning')
|
||
U_EXIT=0 U_EMPTY=0 (.env*, Compose, Lockfile, package.json, prisma, dashboard.service.ts, dto/, globals.css unangetastet)
|
||
```
|
||
|
||
Push 07:15:03Z: `5f50c5f..1aefaa3 main -> main`; `git fetch -q && git status -sb | head -1` -> `## main...origin/main` (kein `[ahead`, kein `[behind`).
|
||
|
||
### CI-Lauf nach dem Push
|
||
|
||
| Feld | Versuch 1 (einziger) |
|
||
|---|---|
|
||
| Lauf-ID | 351 (event `push`, ref `main`, `head_sha` `1aefaa36c1e3ae7c3055e714b059223a1e5aff77`) |
|
||
| status / conclusion | `completed` / **`success`** |
|
||
| started_at / completed_at | 09:15:08 / 09:20:34 (+02:00) — **5 min 26 s** (17 Polls a 20 s, kein Warten in der Warteschlange: der Lauf war beim ersten Poll 11 s nach dem Push bereits `in_progress`) |
|
||
| Jobs | Lint & Type Check `success` (07:15:08-07:15:57Z), Tests `success` (07:16:00-07:16:57Z), Build & Publish Images `success` (07:16:59-07:20:33Z) |
|
||
| `api:beta` node `APP_VERSION APP_CHANNEL APP_COMMIT` | `v1.0.0-10-g1aefaa3 beta 1aefaa3` — Messinstrument-Befund: der Plan erwartet fuer `APP_VERSION` den „kurzen SHA“; seit dem Tag `v1.0.0` (260915) liefert `git describe` `v1.0.0-10-g1aefaa3`, der kurze SHA steht in `APP_COMMIT` = `1aefaa3` = `git rev-parse --short HEAD`. Beide Werte sind das erwartete Abbild des gepushten Stands |
|
||
| `docker image inspect Created` web:beta / api:beta | `2026-09-16T09:18:31+02:00` / `2026-09-16T09:19:29+02:00` (nach dem Lauf-Start; fuer die Probe wurde `api:beta` per `docker pull` geholt, kein Container gebaut oder gestartet) |
|
||
|
||
## Falsifizierungen (a)-(c) — rot/gruen
|
||
|
||
Jede einmal durchgefuehrt, Rueckstellung per `git checkout -- <Datei>`, danach `git status --porcelain` leer.
|
||
|
||
| Falsifizierung | Eingriff | Rot (Testnamen woertlich) | Zurueckgesetzt |
|
||
|---|---|---|---|
|
||
| (a) Marker-Pruefung entfernt | `grid-layout-migration.ts`: `const needsScaling = true;` | `Tests 4 failed | 9 passed (13)`: Migration „Test 2: markierte Anordnung (__gridVersion 2) bleibt unveraendert, migrated false“, „Test 4: Idempotenz — einmal umgerechnet und markiert wird nicht erneut verdoppelt (T-BWO-02)“, „Test 6: Zukunft und Robustheit …“; Store „Test 2: markierte Anordnung bleibt unveraendert, kein Speichern, kein Marker im Zustand“ | ja, gruen |
|
||
| (b) `resolveClockTimeFontSizePt` ohne Typpruefung | `clock-font-size.ts`: `const value = Number(config.timeFontSizePt)` | `Tests 2 failed | 4 passed (6)`: „Test 3 (quick-260916-bwo): resolveClockTimeFontSizePt laesst nur endliche Zahlen 8..200 durch (T-BWO-01)“ (`expected 36 to be null`), „Test 6 (quick-260916-bwo): Zeichenketten-Wert wird ignoriert — auto, kein Inline-Style (T-BWO-01)“ (`expected '36pt' to be ''`) — genau der Angriff: `'36'` wuerde zum Style | ja, gruen |
|
||
| (c1) eine Konstante auf den alten Wert | `widget-registry.tsx`: `clock.minW: 2` | `Tests 2 failed | 14 passed (16)`: „quick-260916-bwo: jede Groesse ist exakt das Doppelte der alten 12-Spalten-Werte“, dashboard-grid „quick-260916-bwo Test 5: Widget ohne gespeicherten Eintrag bekommt die verdoppelten Vorgaben als data-grid“ | ja, gruen |
|
||
| (c2) `rowHeight` auf 40 | `dashboard-grid.tsx` | `Tests 1 failed | 4 passed (5)`: „quick-260916-bwo Test 4: Grid-Props — 24/20/12/8/2 Spalten, rowHeight 20, margin 8, Breakpoints unveraendert, containerPadding folgt dem margin“ | ja, gruen |
|
||
|
||
## Gemessene Befunde und Entscheidungen (Kurzform)
|
||
|
||
- **jsdom 29.1.1 / cssstyle** (Planungsmessung, in der Ausfuehrung durch die Tests bestaetigt): `style.fontSize = 'clamp(...)'` wird verworfen (`''`), `'36pt'` bleibt. Folge: automatische Groessen als Tailwind-Klassen (`text-[clamp(12px,min(20cqw,50cqh),400px)]` usw.), feste Punktgroesse als Inline-Style; Tests lesen `className`, `data-font-mode` und `style.fontSize` — Test 5/6 der Uhr laufen exakt so gruen.
|
||
- **Tailwind 4.3.1** (Planungsmessung): `@container-size` -> `container-type: size`; `text-[clamp(12px,min(20cqw,50cqh),400px)]` kompiliert mit verschachtelten Funktionen. Alle Klassen stehen woertlich im JSX bzw. als String-Literale in derselben Datei (Scanner). Nicht erneut kompiliert — der Browser-Nachweis prueft das Ergebnis.
|
||
- **Umrechnung im Frontend** (siehe key-decisions): Einheiten sind Frontend-Konstanten; die API reicht durch (Spec Test A/B pinnen es); kein Schreiben auf GET; reine Funktion ohne Prisma testbar. Preis: bis zum ersten gelungenen PUT rechnet jedes Laden erneut aus den unveraenderten alten DB-Werten — korrekt, weil `saveLayout` Marker und Werte immer gemeinsam schreibt.
|
||
- **`mt-8` bleibt**: Umschalter `p-2` + 20-px-Symbol = 36 px, mit `top-2` bei 8..44 px; erstes Widget mit `mt-8` bei 8+32+8 = 48 px (4 px Abstand), mit `mt-4` bei 32 px (12 px IM Umschalter). Kommentar in `page.tsx`.
|
||
- **Annahme „Listen-Widgets skalieren nicht“**: note, calendar, favorites, link, search behalten ihre Schriftgroessen — bei mehr Platz zeigen sie mehr Inhalt. Nur ihre Aussen-Innenabstaende sind halbiert.
|
||
- **Beobachtung**: Das `ClockConfig`-Formular traegt weiterhin ein hart englisches Label „Timezone“ und die Zeile „Saving...“ (ausserhalb des Auftrags, unveraendert). Ebenso „Title“ im Notiz-Formular und der englische Kalender-Hinweis.
|
||
- **Rechner-Speicherzeile** (`h-7 text-xs`) bleibt bewusst klein; die Tasten fuellen ihre Rasterzelle (`h-full min-h-7`), damit das 4x5-Raster mitwaechst.
|
||
|
||
## Task Commits
|
||
|
||
1. **Task 1: Raster, Umrechnung, Store, API-Specs** — `3f5afb0` (feat) — 9 Dateien
|
||
2. **Task 2a: Skalierung, Punktgroesse, i18n** — `2d8efe1` (feat) — 12 Dateien
|
||
3. **Task 2b: Abstaende halbiert** — `a175c00` (feat) — 7 Dateien
|
||
4. **Task 3: Anwenderhandbuch** — `1aefaa3` (docs) — 1 Datei
|
||
|
||
`commits: 4` gemessen aus `git rev-list --count 50f201e..HEAD` (Ledger `plan_head_before` = `50f201ecc1292232ff96e13226b62386cfc36e76`). `actuals.tokens` = 291.763 Zeichen ueber die 29 geaenderten Dateien / 4 = 72.941 (Methode wie 260914-m97; der reine Diff waere 65.872 Zeichen = 16.468). Der Plan schaetzte 120.000 bei `confidence: low`.
|
||
|
||
`git log --oneline 50f201e..HEAD` (vor dem SUMMARY, 07:21Z):
|
||
|
||
```
|
||
1aefaa3 docs(quick-260916-bwo): Anwenderhandbuch — feines Raster, Uhrzeit skaliert mit der Kachel, Schriftgroesse in Punkt
|
||
a175c00 feat(quick-260916-bwo): Abstaende halbiert — Seitenrahmen p-3 (alle Seiten), Dashboard p-2, Grid 8 px, Widget-Innenabstaende
|
||
2d8efe1 feat(quick-260916-bwo): Widget-Inhalt skaliert per Container-Queries (Uhr, Stoppuhr, Rechner), Punktgroesse der Uhrzeit einstellbar
|
||
3f5afb0 feat(quick-260916-bwo): Dashboard-Raster verdoppelt (24 Spalten, 20 px, 8 px Abstand), Konstanten x2, einmalige Umrechnung gespeicherter Anordnungen mit Marker __gridVersion
|
||
```
|
||
|
||
`git status --porcelain` (vor dem SUMMARY): leer. `git status -sb | head -1`: `## main...origin/main`.
|
||
|
||
## Deviations from Plan
|
||
|
||
### Auto-fixed Issues
|
||
|
||
**1. [Rule 3 - Blocking] Versehentliches `git stash` waehrend Task 1, sofort zurueckgeholt**
|
||
- **Found during:** Task 1, zwischen GREEN-Lauf und Commit
|
||
- **Issue:** In einem Messbefehl (Zaehlen der Registry-Testfaelle) lief ein `git stash -q` mit, das die sechs geaenderten, noch nicht committeten Dateien in `stash@{0}` verschob (die drei neuen Dateien blieben als unversioniert liegen). Die Stash-Liste war vor Beginn leer (geprueft), der einzige Eintrag war mein eigener auf HEAD `50f201e`; kein Worktree, keine Nebenlaeufer.
|
||
- **Fix:** `git stash pop` (Ausgabe: alle sechs Dateien wieder geaendert, `refs/stash@{0} … geloescht`), Stash-Liste danach leer; Inhalt per grep und erneutem Testlauf (`Test Files 4 passed (4)` / `Tests 29 passed (29)`) bestaetigt, dann erst committet.
|
||
- **Files modified:** keine (Wiederherstellung des Arbeitsbaums)
|
||
- **Commit:** — (`3f5afb0` enthaelt den unveraenderten Stand)
|
||
|
||
**2. [Rule 1 - Bug] Gate `grep -c " p-6"` in `app-shell.tsx` musste 0 sein, lieferte 1**
|
||
- **Found during:** Task 2b, Verify-Block
|
||
- **Issue:** Die Klasse `p-6` war korrekt durch `p-3` ersetzt, aber mein neuer JSX-Kommentar enthielt wörtlich „statt p-6 (24 px)“ und traf das Muster.
|
||
- **Fix:** Kommentar umformuliert („statt vorher 24 px (Stufe 6)“); Gate danach `0`, `p-3` weiterhin `2` (Kommentar + Klasse), tsc 0. Innerhalb der Allowlist, vor dem Commit 2b.
|
||
- **Files modified:** `apps/web/src/components/layout/app-shell.tsx`
|
||
- **Commit:** `a175c00`
|
||
|
||
### Messungen, die dem Plan widersprechen (beide Zahlen oben festgehalten)
|
||
|
||
1. Task 1 Web-Vitest-Zeile: Plan `22`, gemessen `29` — `it.each` ueber 8 Typen in `widget-registry.test.tsx` (Plan zaehlte `it(`-Bloecke). Gesamtsuite 286 wie geplant.
|
||
2. Commit 2a: Plan „11 Dateien“, gemessen `12` — der Plan listet selbst 12 auf; 9 + 12 + 7 + 1 = 29 stimmt.
|
||
3. Abbild-Probe: Plan „`APP_VERSION` -> kurzer SHA“, gemessen `v1.0.0-10-g1aefaa3` (`git describe` seit dem Tag v1.0.0); der kurze SHA `1aefaa3` steht in `APP_COMMIT`.
|
||
|
||
## Was bewusst offen bleibt
|
||
|
||
- **Browser-Nachweis** (Bounding-Boxen, Rasterstufe, Skalierung, Punktgroesse, SQL-Probe der Umrechnung, Rechner bei Mindestgroesse): nicht im Executor moeglich (jsdom rechnet keine Container-Queries, keine Container gebaut) — Human-Check `end-of-phase`, Anleitung unten.
|
||
- **Rechner bei Mindestgroesse (4x8 = 8 Zeilen x 20 px + 7 x 8 px = 216 px)**: Der Plan nennt den heutigen Ueberlauf bei alter Mindestgroesse; ob der Rechner mit `h-full min-h-7`-Tasten (5 x 28 px = 140 px + Anzeige 40 + Speicherzeile 28 + Luecken ca. 24 = ca. 232 px) bei 216 px bedienbar bleibt, ist im Browser zu messen (Punkt 7 unten). Faellt er durch, ist die Rueckfalloption des Plans „nur die Anzeige skalieren“ — vorher messen.
|
||
- **Hart englische Texte im Einstellungsformular** („Timezone“, „Saving...“, „Title“, Kalender-Hinweis): ausserhalb des Auftrags, unveraendert.
|
||
- **Marker-Persistenz bei dauerhaft scheiterndem PUT**: bis zum ersten gelungenen Speichern rechnet jedes Laden erneut aus den alten DB-Werten (korrekt, aber jedes Laden loest einen PUT-Versuch aus, `console.error` je Versuch).
|
||
- **Live (`tessera.ctl.de`)**: Bestand nicht gemessen; nach dem naechsten Deploy rechnet jeder Benutzer seine Anordnung beim ersten Laden selbst um.
|
||
|
||
## Fuer den Verifizierer/Orchestrator (Browser-Nachweis)
|
||
|
||
Vorher `docker compose up -d --build web` (Web-Abbild vom 14.09.; `up` allein baut nicht neu). Die API braucht keinen Neubau. Anmelden mit Benutzername `admin`, Kennwort `admin123`. Alle Bounding-Boxen per Playwright `boundingBox()`, nie per `fetch` aus der Seite.
|
||
|
||
1. **Anordnung anlegen:** Dashboard -> „Dashboard bearbeiten“ -> Uhr und Suchleiste hinzufuegen, Uhr rechts neben die Suchleiste ziehen, Bearbeitungsmodus beenden (speichert).
|
||
2. **Abstaende:** `main.app-shell-main` links vs. erstes `[data-widget-id]` links -> 28 px (+/-1; vorher 56); zwei horizontal benachbarte Widgets -> Luecke 8 px (+/-1; vorher 16); `main` oben vs. erstes Widget -> 60 px (+/-1; vorher 88). Zweite Seite `/admin/users`: `main`-Rand zum ersten Inhaltselement 12 px (vorher 24).
|
||
3. **Raster:** Uhr im Bearbeitungsmodus um EINE Stufe nach rechts ziehen -> Versatz eine Spaltenbreite `(Containerbreite - 8*23 - 8*2) / 24` px (bei 1200 px ca. 41 px, vorher ca. 83 px).
|
||
4. **Skalierung:** Uhr auf 12x8 vergroessern -> `getComputedStyle(time).fontSize` deutlich groesser als bei 4x4 (Zahlen notieren; Erwartung 4x4 ca. 38-42 px, 12x8 ca. 80+ px); verkleinern -> schrumpft; `time.dataset.fontMode === 'auto'`. Ist `cqh` 0 (Uhr unsichtbar klein), fehlt die definite Hoehe — dann `widget-wrapper.tsx` pruefen (Rumpf `h-full` in der Karte `h-full`).
|
||
5. **Punktgroesse:** Einstellungen -> Dashboard -> Widgets, „Uhr #1“ aufklappen, „Schriftgröße der Uhrzeit (Punkt)“ auf `36`, Feld verlassen -> zurueck zum Dashboard: `time.style.fontSize === '36pt'`, `getComputedStyle(time).fontSize === '48px'` bei 4x4 UND 12x8, `data-font-mode="fixed"`, Datum `14.4pt`. Dann `300` -> Fehlermeldung „Bitte geben Sie eine Zahl zwischen 8 und 200 ein oder lassen Sie das Feld leer.“, kein Speichern (Netzwerk: kein PATCH). Feld leeren, Enter -> wieder automatisch.
|
||
6. **Umrechnungs-Probe (SQL, lokale DB, Rolle `tessera` hat BYPASSRLS, Schalter AUS).** Zuerst den gespeicherten Stand lesen und die Kennungen notieren:
|
||
```
|
||
docker exec tessera-ctl-db-1 psql -U tessera -d tessera -At -c 'SELECT "userId", layouts::text FROM "DashboardLayout";'
|
||
docker exec tessera-ctl-db-1 psql -U tessera -d tessera -At -c 'SELECT id, "widgetType" FROM "WidgetInstance" ORDER BY "createdAt";'
|
||
```
|
||
Erwartung nach Schritt 1: `__gridVersion: 2`, Suchleiste `x: 0, w: 12, h: 4`, Uhr `x: 12, w: 4, h: 4` (o. ae.). Dann die Zeile in ALTE Einheiten ohne Marker zurueckschreiben (`<uhr-id>`/`<such-id>` aus der zweiten Abfrage; `userId` des lokalen Admins ist `1166431d-a556-47d0-9e19-6bd3616f148f`):
|
||
```sql
|
||
UPDATE "DashboardLayout"
|
||
SET layouts = jsonb_build_object(
|
||
'lg', jsonb_build_array(
|
||
jsonb_build_object('i','<uhr-id>','x',6,'y',0,'w',2,'h',2),
|
||
jsonb_build_object('i','<such-id>','x',0,'y',0,'w',6,'h',2)),
|
||
'md','[]'::jsonb,'sm','[]'::jsonb,'xs','[]'::jsonb,'xxs','[]'::jsonb),
|
||
"updatedAt" = now()
|
||
WHERE "userId" = '1166431d-a556-47d0-9e19-6bd3616f148f';
|
||
```
|
||
**Falls noch keine Widgets/Anordnung existieren** (lokal waren es zum Zeitpunkt der Ausfuehrung 0/0), stattdessen alles in einem Rutsch anlegen — Uhr und Suchleiste als `WidgetInstance` plus die Anordnung in ALTEN Einheiten (Admin-User `1166431d-a556-47d0-9e19-6bd3616f148f`, Mandant `e2592907-0118-4cce-a407-7e27d375b68d`; beide Kennungen am 16.09. aus `"User"` gelesen, bei einer neu aufgesetzten DB erneut per `SELECT id, "tenantId" FROM "User" WHERE username = 'admin';` holen):
|
||
```sql
|
||
INSERT INTO "WidgetInstance" (id, "userId", "tenantId", "widgetType", config, "createdAt", "updatedAt") VALUES
|
||
('probe-suche-1', '1166431d-a556-47d0-9e19-6bd3616f148f', 'e2592907-0118-4cce-a407-7e27d375b68d', 'search', '{}'::jsonb, now(), now()),
|
||
('probe-uhr-1', '1166431d-a556-47d0-9e19-6bd3616f148f', 'e2592907-0118-4cce-a407-7e27d375b68d', 'clock', '{"timezone":"Europe/Berlin","showDate":true}'::jsonb, now() + interval '1 second', now());
|
||
INSERT INTO "DashboardLayout" (id, "userId", "tenantId", layouts, "createdAt", "updatedAt") VALUES
|
||
('probe-layout-1', '1166431d-a556-47d0-9e19-6bd3616f148f', 'e2592907-0118-4cce-a407-7e27d375b68d',
|
||
'{"lg":[{"i":"probe-suche-1","x":0,"y":0,"w":6,"h":2},{"i":"probe-uhr-1","x":6,"y":0,"w":2,"h":2}],"md":[],"sm":[],"xs":[],"xxs":[]}'::jsonb,
|
||
now(), now());
|
||
```
|
||
(Alte Einheiten: Suchleiste 6x2 ab Spalte 0, Uhr 2x2 ab Spalte 6 — die frueheren Vorgabegroessen, Uhr direkt rechts neben der Suchleiste.) Seite laden -> Uhr steht rechts neben der Suchleiste (Uhr links = Suchleiste rechts + 8 px, Bounding-Boxen), nicht halb so gross und nicht verrutscht; danach:
|
||
```
|
||
docker exec tessera-ctl-db-1 psql -U tessera -d tessera -At -c 'SELECT layouts->''__gridVersion'', layouts->''lg'' FROM "DashboardLayout";'
|
||
```
|
||
-> `2` und Suchleiste `x: 0, w: 12, h: 4`, Uhr `x: 12, w: 4, h: 4`. Seite ein ZWEITES Mal laden -> Werte unveraendert (keine erneute Verdopplung). Aufraeumen der Probe: `DELETE FROM "DashboardLayout" WHERE id = 'probe-layout-1'; DELETE FROM "WidgetInstance" WHERE id IN ('probe-suche-1','probe-uhr-1');` (nur falls per INSERT angelegt).
|
||
7. **Rechner und Stoppuhr:** beide hinzufuegen, vergroessern -> Anzeige und Tasten wachsen mit, das Tastenraster bleibt 4x5 und ueberlaeuft nicht; auf Mindestgroesse (Rechner 4x8, Stoppuhr 4x4) verkleinern -> Rechner bedienbar, Stoppuhr-Anzeige lesbar. Ueberlaeuft der Rechner bei 4x8: Hoehe der Tasten und der Anzeige notieren (siehe „Was bewusst offen bleibt“).
|
||
|
||
## Fuer den Changelog
|
||
|
||
- Das Dashboard-Raster ist doppelt so fein: Widgets lassen sich in kleineren Schritten verschieben und in der Groesse ziehen, bleiben dabei aber genauso gross wie bisher. Bereits gespeicherte Anordnungen werden beim ersten Aufruf automatisch uebernommen und verrutschen nicht.
|
||
- Uhrzeit, Stoppuhr und Rechner wachsen und schrumpfen jetzt mit ihrer Kachel — eine grosse Uhr-Kachel zeigt eine grosse Uhrzeit.
|
||
- Die Uhr hat eine neue Einstellung: unter Einstellungen > Dashboard laesst sich die Schriftgroesse der Uhrzeit fest in Punkt (8 bis 200) vorgeben; leer gelassen passt sie sich weiter automatisch an.
|
||
- Die Raender sind ueberall enger: der aeussere Seitenrahmen auf allen Seiten, der Abstand zwischen den Widgets und die Innenabstaende in den Widgets sind halbiert.
|
||
- Das Anwenderhandbuch beschreibt das feine Raster, die mitwachsende Uhrzeit und die neue Schriftgroessen-Einstellung.
|
||
|
||
## Self-Check: PASSED
|
||
|
||
- Dateien: alle 5 neu angelegten Dateien vorhanden (`grid-layout-migration.ts/.test.ts`, `dashboard-store.test.ts`, `clock-font-size.ts`, `widget-settings-panel.test.tsx`).
|
||
- Commits: `3f5afb0`, `2d8efe1`, `a175c00`, `1aefaa3` in `git log --oneline --all` gefunden; `git ls-remote origin main` = `1aefaa3`; `main == origin/main`.
|
||
- `commits: 4` = `git rev-list --count 50f201e..HEAD`; `git status --porcelain` leer vor dem SUMMARY.
|