---
phase: quick-260922-vdk
plan: 01
subsystem: ui
tags: [react, react-grid-layout, ref-callback, resize-observer, dashboard, vitest]
requires: []
provides:
- "Dashboard-Raster misst seine Breite auch beim Wechsel Leerzustand -> gefuellt (Ref-Rueckruf statt Einmal-Effekt)"
- "Fenster-Horcher als Netz zusaetzlich zum ResizeObserver"
affects: [dashboard-grid, dashboard]
actuals:
tokens: 2392
tasks: 2
commits: 2
tech-stack:
added: []
patterns:
- "Ref-Rueckruf (callback ref) statt useEffect mit leerer Abhaengigkeitsliste, wenn eine Messung den tatsaechlich eingehaengten Knoten ueber Aus-/Einhaengen hinweg verfolgen muss"
key-files:
created: []
modified:
- apps/web/src/components/dashboard/dashboard-grid.tsx
- apps/web/src/components/dashboard/dashboard-grid.test.tsx
- CHANGELOG.md
key-decisions:
- "Ref-Rueckruf measureRef ersetzt containerRef + Einmal-Effekt; misst synchron in der Commit-Phase, kein useLayoutEffect noetig"
- "Kein Aufraeum-Rueckgabewert aus measureRef, damit React 19 den Rueckruf beim Aushaengen mit null aufruft (das ist der Aufraeumpfad)"
- "Fenster-Horcher zusaetzlich zum ResizeObserver, nicht als Ersatz"
- "Startwert bleibt 1200 (nur bis zur ersten Commit-Phase relevant)"
- "FREE_PLACEMENT_COMPACTOR/preventCollision, BREAKPOINTS, COLS, rowHeight, margin, applyConstraintMinima unveraendert"
patterns-established:
- "quick-260922-vdk Kommentarblock in dashboard-grid.tsx erklaert das Warum der Ref-Rueckruf-Messung"
requirements-completed: [QUICK-260922-VDK]
coverage:
- id: D1
description: "Dashboard-Raster misst seine Breite auch aus dem Leerzustand heraus (Ref-Rueckruf, synchrone Erstmessung, Fenster-Horcher)"
requirement: "QUICK-260922-VDK"
verification:
- kind: unit
ref: "apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260922-vdk Test 10: Leerzustand -> gefuellt misst die tatsaechliche Breite statt beim Startwert 1200 stehenzubleiben"
status: pass
- kind: unit
ref: "apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260922-vdk Test 11: Fenstergroesse aendert sich -> der Fenster-Horcher misst neu"
status: pass
human_judgment: true
rationale: "Die Unit-Tests beweisen die gemessene Breite in jsdom; ob im echten Browser rechts kein toter Streifen mehr bleibt und die Rasterbreite sichtbar der Breite des Inhaltsbereichs entspricht, verlangt einen Browser-Rundgang (Prueflliste unten)."
- id: D2
description: "Ziehverhalten unveraendert: belegtes Feld bleibt blockiert, nichts wird zur Seite geschoben"
verification:
- kind: unit
ref: "apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260916-dyv Test 7: Compactor-Pin"
status: pass
- kind: unit
ref: "apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260916-dyv Test 6: dragConfig-Pin"
status: pass
human_judgment: false
duration: ~9min
completed: 2026-09-22
status: complete
---
# Quick-Aufgabe 260922-vdk: Dashboard-Raster misst seine Breite auch aus dem Leerzustand heraus Summary
**Messung an einem Ref-Rueckruf (`measureRef`) statt an einem Einmal-Effekt: der Beobachter folgt dem eingehaengten `
` ueber Leerzustand <-> gefuellt hinweg, misst synchron in der Commit-Phase, plus Fenster-Horcher als Netz — 20-Spalten/`md`-Fehlmessung nach 1200 px behoben.**
## Performance
- **Duration:** ~9 min
- **Completed:** 2026-09-22
- **Tasks:** 2/2
- **Files modified:** 3
## Accomplishments
- Dashboard-Raster misst jetzt in jedem Fall die tatsaechliche Breite des Inhaltsbereichs, auch wenn `DashboardGrid` mit null Kacheln einhaengt und die erste Kachel erst spaeter erscheint
- Fenster-Horcher als Sicherheitsnetz zusaetzlich zum ResizeObserver
- Zwei neue Regressionstests, nachweislich zuerst rot
- Ziehverhalten (`FREE_PLACEMENT_COMPACTOR` mit `preventCollision`), Raster-Konstanten und Leerzustand unveraendert (per Test 4/6/7/9/9b bestaetigt)
## Task Commits
Each task was committed atomically:
1. **Aufgabe 1: Messung an den Knoten haengen (Ref-Rueckruf, synchrone Erstmessung, Fenster-Horcher)** - `d9f2af3` (fix)
2. **Aufgabe 2: Changelog-Stichpunkt, volle Tore, Zaehler, Pruefliste** - `cf67c8a` (docs)
_Ledger: `plan_head_before` = `3e8c0f4` (letzter Commit vor diesem Plan — die bereits committete Akte). `git rev-list --count 3e8c0f4..HEAD` = 2, deckt sich mit den zwei oben genannten Commits._
**Plan metadata:** wird vom Orchestrator nach diesem SUMMARY committet (STATE.md/ROADMAP.md ausserhalb dieses Plans).
## Rot-Nachweis (Aufgabe 1, vor der Aenderung)
Testlauf vor dem Umbau von `dashboard-grid.tsx` (nur die Test-Datei war schon geaendert):
```
FAIL src/components/dashboard/dashboard-grid.test.tsx > DashboardGrid > quick-260922-vdk Test 10: ...
AssertionError: expected 1200 to be 1000 // Object.is equality
FAIL src/components/dashboard/dashboard-grid.test.tsx > DashboardGrid > quick-260922-vdk Test 11: ...
AssertionError: expected 1000 to be 1600 // Object.is equality
```
10 von 12 Faellen gruen, genau die zwei neuen rot — wie erwartet: Test 10 blieb beim Startwert 1200 (der frueh zurueckspringende Leerzustand rendert den gemessenen Knoten nie, der Effekt mit leerer Abhaengigkeitsliste sieht ihn also nie), Test 11 blieb bei 1000, weil es vorher keinen Fenster-Horcher gab.
Nach dem Umbau: alle 12 Faelle gruen (`pnpm --filter @tessera/web exec vitest run src/components/dashboard/dashboard-grid.test.tsx`).
## Gemessene Zahlen (nachher, alle Tore)
| Tor | Ausgangsmessung (22.09., vor der Aenderung) | Nachher (gemessen) |
|---|---|---|
| `dashboard-grid.test.tsx` | 10 Faelle | 12 Faelle, alle gruen |
| Web Tests gesamt | 81 Dateien / 659 Tests | 81 Dateien / 661 Tests, alle gruen |
| `pnpm type-check` | — | 4/4 ohne Befund |
| `pnpm lint` | — | 5/5 ohne Befund der Stufe `error` |
| Biome web Warnungen | 53 | 53 (unveraendert) |
| `as unknown as` in web | 6 | 6 (unveraendert) |
Keine Abweichung — alle erwarteten Zahlen aus dem Plan treffen exakt zu.
## Files Created/Modified
- `apps/web/src/components/dashboard/dashboard-grid.tsx` - `measureRef`-Ref-Rueckruf ersetzt `containerRef` + Einmal-Effekt; `applyWidth`-Wächter (verwirft 0/nicht-endliche Werte); Fenster-Horcher (`resize`-Listener); deutscher Warum-Kommentarblock `quick-260922-vdk`
- `apps/web/src/components/dashboard/dashboard-grid.test.tsx` - zwei neue Faelle (Test 10: Leerzustand -> gefuellt misst 1000; Test 11: resize-Ereignis misst 1600), `stubResizeObserver`-Import, `vi.unstubAllGlobals()` im `afterEach`
- `CHANGELOG.md` - neuer Abschnitt „### Behoben" unter „## Unveroeffentlicht" mit einem Stichpunkt
## Decisions Made
Keine neuen Entscheidungen — die im Plan gebundenen Entscheidungen (Ref-Rueckruf statt Einmal-Effekt, synchron vor dem ersten Zeichnen, Fenster-Horcher als Netz, Ziehverhalten unveraendert, Startwert 1200 bleibt, nur `apps/web`) wurden wortgleich umgesetzt.
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
None.
## Pruefliste fuer den Browser-Rundgang (Orchestrator, lokal, Playwright-MCP, nicht Testserver)
Breiten werden am DOM gemessen (`getBoundingClientRect` der Elemente), **nie** per `fetch` aus der Seite heraus — das taeuscht in beide Richtungen.
- [ ] (a) Dashboard eines Benutzers ohne Kacheln oeffnen (oder alle Kacheln entfernen und neu laden) -> Leerzustand mit Bildmarke erscheint
- [ ] (b) Bearbeiten -> „Widget hinzufuegen" -> Uhr: die Kachel erscheint, und beim Ziehen reicht der Platzhalter bis an den rechten Rand des Inhaltsbereichs — kein toter Streifen
- [ ] (c) messen: Breite von `.react-grid-layout` gleicht der Breite des umgebenden Inhaltsbereichs (Abweichung hoechstens der Rand von 8 px); vorher lag sie bei 1200 px unabhaengig von der Fensterbreite
- [ ] (d) eine zweite Kachel auf ein belegtes Feld ziehen -> sie springt zurueck, nichts wird zur Seite geschoben (unveraendert gewollt); Groesse ziehen stoppt am Nachbarn
- [ ] (e) Fenster schmaler und wieder breiter ziehen -> das Raster folgt, Kacheln bleiben heil
- [ ] (f) Seite mit vorhandenen Kacheln neu laden -> weiterhin richtig (der bisher schon funktionierende Pfad), und beim ersten Zeichnen ist kein Sprung von schmal auf breit zu sehen
- [ ] (g) letzte Kachel entfernen -> Leerzustand erscheint, danach eine neue Kachel hinzufuegen -> wieder volle Breite (der Beobachter wurde sauber getrennt und neu angehaengt)
Entscheidend: Punkt (b)/(c) — auf einem beim Oeffnen leeren Dashboard fuellt die erste Kachel den Inhaltsbereich bis zum rechten Rand, und die gemessene Rasterbreite stimmt mit der Breite des Inhaltsbereichs ueberein.
## Threat Flags
Keine neue Vertrauensgrenze, keine neuen Pakete. Alle sechs T-VDK-Punkte aus dem Plan-Threat-Model sind mit Tests bzw. Struktur-Toren abgedeckt (siehe Rot-Nachweis und gemessene Zahlen oben); nichts Neues gefunden.
## Next Phase Readiness
Kein laufender Meilenstein, keine Folge-Phase direkt abhaengig. Naechster Schritt laut STATE.md bleibt: Widget-Modul-Kopplung (`WIDGET_MODULE_MAP`) und danach das Proxmox-Modul — unabhaengig von dieser Quick-Aufgabe.
## Self-Check: PASSED
- FOUND: apps/web/src/components/dashboard/dashboard-grid.tsx
- FOUND: apps/web/src/components/dashboard/dashboard-grid.test.tsx
- FOUND: CHANGELOG.md
- FOUND: .planning/quick/260922-vdk-dashboard-raster-misst-seine-breite-nich/260922-vdk-SUMMARY.md
- FOUND commit: d9f2af3
- FOUND commit: cf67c8a
---
*Phase: quick-260922-vdk*
*Completed: 2026-09-22*
## Rundgang durch den Orchestrator (22.09.2026, echter Linux-Client, nicht der Browser)
Der Nutzer hat den Fehler im **Linux-Client** gemeldet und ausdruecklich gesagt, ein
Browser-Test bringe nichts. Geprueft wurde deshalb im echten Paket: `Tessera-1.3.0.AppImage`
aus dem Gitea-Release, auf dem Entwicklungsrechner gestartet (`DISPLAY=:10`,
`WEBKIT_INSPECTOR_HTTP_SERVER=127.0.0.1:9230`), gesteuert ueber den WebKit-Remote-Inspektor
(Treiber `scratchpad/wk.mjs`, Target-Protokoll: `Target.sendMessageToTarget` +
`Runtime.evaluate`). Server: lokaler Stack.
**Ausgangsmessung VOR dem Fix** (gleiche Sitzung, gleicher Client):
| Weg | Bereich | Kachel | Rasterbreite laut Rechnung |
|-----|---------|--------|----------------------------|
| Neuladen MIT Kachel | 1176 px | 459 px | 1176 px — richtig |
| Seitenleiste auf/zu | 1000 ↔ 1176 px | folgt | richtig |
| **Leeres Dashboard, dann Kachel hinzufuegen** | 1000 px | **469 px** | **1200 px — falsch** |
Der dritte Weg ist der Fehlerfall: `DashboardGrid` haengt mit null Kacheln ein, der
gemessene `
` existiert nicht, der Einmal-Effekt bricht ab und laeuft nie wieder.
**Nach dem Fix, derselbe Weg** (Kachel entfernt, gespeichert, neu geladen -> leeres
Dashboard -> Kalender hinzugefuegt):
- `width`-Eigenschaft an `Responsive`: **1000** (= echte Bereichsbreite), Breakpoint `md`,
20 Spalten — aus dem React-Fiber ausgelesen, nicht geraten.
- Kachel `style.width`: **389 px** = `8 × 41,6 + 56`, exakt der Sollwert fuer 1000 px.
- Ziehen nach rechts (synthetische Maus-Ereignisse, jeweils mit Wartezeit, damit React
dazwischen rendert): Platzhalter laeuft 8 → 107 → 256 → 405 → 554 → **603** und bleibt
dort. `1000 − 8 − 389 = 603` — **der Platzhalter erreicht jetzt exakt den rechten Rand**.
Die Kachel wird auch dort abgelegt (`tileX 603`).
**Messfalle, dokumentiert damit sie niemanden noch einmal kostet:** `getBoundingClientRect()`
und `getComputedStyle().width` lieferten im Client weiter 469 px, obwohl `style.width`
bereits 389 px war. Grund: `.react-grid-item` hat `transition: width .2s`, und das
Client-Fenster lag im Hintergrund — WebKitGTK friert die Animationsuhr dann ein, der
Uebergang bleibt auf dem Startwert stehen (`getAnimations()` meldete eine laufende
`CSSTransition` auf `width`). **Im Client gegen die gesetzten Werte messen
(`style.width`, `style.transform`) oder gegen die React-Eigenschaften, nie gegen die
gemalte Box** — sonst misst man die eingefrorene Animation statt des Ergebnisses.
**Nicht angefasst, wie zugesagt:** belegte Plaetze bleiben gesperrt, nichts weicht aus
(`FREE_PLACEMENT_COMPACTOR` mit `preventCollision` unveraendert) — ausdrueckliche Ansage des
Nutzers am 22.09.
**Testreste der Pruefung:** lokaler Testbenutzer `clienttest` und die zeitweise auf
`NODE_ENV=development` gesetzte lokale API (WebKitGTK nimmt `secure`-Kekse ueber `http` nicht
an, Chromium macht fuer `localhost` eine Ausnahme) — beides nach der Pruefung wieder
zurueckgebaut.