docs(quick-260922-vdk): Plan - Dashboard-Raster misst seine Breite auch aus dem Leerzustand
This commit is contained in:
+201
@@ -0,0 +1,201 @@
|
|||||||
|
---
|
||||||
|
phase: quick-260922-vdk
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
autonomous: true
|
||||||
|
requirements: [QUICK-260922-VDK]
|
||||||
|
|
||||||
|
files_modified:
|
||||||
|
- apps/web/src/components/dashboard/dashboard-grid.tsx
|
||||||
|
- apps/web/src/components/dashboard/dashboard-grid.test.tsx
|
||||||
|
- CHANGELOG.md
|
||||||
|
|
||||||
|
estimate:
|
||||||
|
tokens: 55000
|
||||||
|
raw_tokens: 55000
|
||||||
|
tasks: 2
|
||||||
|
confidence: low
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Ein Dashboard, das beim Öffnen leer ist, misst die verfügbare Breite, sobald die erste Kachel erscheint: das Raster füllt den Inhaltsbereich bis zum rechten Rand, rechts bleibt kein toter Streifen, in den sich keine Kachel ziehen lässt."
|
||||||
|
- "Die Messung überlebt den Wechsel Leerzustand → gefüllt, weil sie am eingehängten Knoten selbst hängt (Ref-Rückruf) und nicht an einem Effekt, der nur beim ersten Einhängen läuft."
|
||||||
|
- "Gemessen wird synchron in der Commit-Phase, bevor gezeichnet wird — der bisherige Zwischenzustand mit dem angenommenen Startwert wird nie sichtbar (auch nicht auf dem heute schon funktionierenden Pfad „Neuladen mit Kacheln“)."
|
||||||
|
- "Ändert sich die Fenstergröße, folgt die Rasterbreite; ein Fenster-Horcher ist das Sicherheitsnetz für den Fall, dass der ResizeObserver nichts meldet."
|
||||||
|
- "Das Ziehverhalten bleibt exakt wie heute: FREE_PLACEMENT_COMPACTOR mit preventCollision, ein belegtes Feld bleibt blockiert, nichts wird zur Seite geschoben (Nutzerentscheidung 22.09.2026); Leerzustand, BREAKPOINTS, COLS, rowHeight, margin und applyConstraintMinima sind unverändert."
|
||||||
|
- "Alle Tore grün: 12 Tests in dashboard-grid.test.tsx (10 alte unverändert + 2 neue), Web gesamt ≥ 661 Tests in 81 Dateien, `tsc --noEmit` 4/4, Biome 5/5 mit weiterhin genau 53 Warnungen in web, `as unknown as` in web weiterhin 6."
|
||||||
|
artifacts:
|
||||||
|
- "apps/web/src/components/dashboard/dashboard-grid.tsx — Messung über den Ref-Rückruf `measureRef` (synchrone Erstmessung + ResizeObserver am jeweils eingehängten Knoten, Trennen im null-Zweig), `applyWidth`-Wächter, Fenster-Horcher; deutscher Kommentarblock `quick-260922-vdk` mit dem Warum"
|
||||||
|
- "apps/web/src/components/dashboard/dashboard-grid.test.tsx — zwei neue Fälle (Leerzustand → gefüllt misst 1000; resize-Ereignis misst 1600) und `vi.unstubAllGlobals()` im bestehenden `afterEach`"
|
||||||
|
- "CHANGELOG.md — neuer Abschnitt „### Behoben“ unter „Unveröffentlicht“ mit einem Stichpunkt in Alltagssprache"
|
||||||
|
key_links:
|
||||||
|
- "Kachel hinzufügen → `widgets.length` wird 1 → der Zweig mit dem Raster rendert → React hängt den `<div>` ein → `measureRef(node)` → synchrone Messung + `observer.observe(node)` → `setWidth` noch vor dem Zeichnen → `Responsive width` → Spaltenbreite und Breakpoint stimmen"
|
||||||
|
- "Letzte Kachel entfernt → Knoten wird ausgehängt → `measureRef(null)` → `observerRef.current.disconnect()` (kein zurückgegebener Aufräum-Rückgabewert, damit React den null-Aufruf beibehält) → kein weiterlaufender Beobachter"
|
||||||
|
- "`window` resize → Horcher → `nodeRef.current.getBoundingClientRect().width` → `applyWidth` (verwirft 0 und nicht endliche Werte) → `setWidth`"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick-Aufgabe 260922-vdk: Das Dashboard-Raster misst seine Breite auch aus dem Leerzustand heraus
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Auf einem Dashboard, das beim Öffnen leer ist, bleibt das Raster für die ganze Sitzung bei der angenommenen Breite von 1200 Pixeln stehen. Die erste hinzugefügte Kachel rechnet deshalb mit 20 statt 24 Spalten und 51,6 px Spaltenbreite, das Raster endet bei 1200 px und rechts davon liegt ein toter Bereich (gemessen: ~460 px bei 1920 px Bildschirmbreite), in den sich keine Kachel ziehen lässt. Diese Aufgabe hängt die Messung an den Knoten statt an den ersten Einhäng-Zeitpunkt, misst vor dem ersten Zeichnen und ergänzt einen Fenster-Horcher als Netz.
|
||||||
|
|
||||||
|
Purpose: Fehlerbehebung aus einer bereits abgeschlossenen Messung — die Ursache steht fest, dieser Plan setzt nur noch um und sichert sie mit einem Regressionstest ab.
|
||||||
|
Output: geänderte `dashboard-grid.tsx` mit deutschem Warum-Kommentar, zwei neue Tests (zuerst rot), ein Changelog-Stichpunkt, alle Tore grün, Prüfliste für den Browser-Rundgang im SUMMARY.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
## Befund (gemessen, nicht neu zu untersuchen)
|
||||||
|
|
||||||
|
Der bisherige Code legt den Beobachter in einem Effekt mit leerer Abhängigkeitsliste an und bricht ab, wenn der Ref noch leer ist. Hängt `DashboardGrid` ein, während das Dashboard null Kacheln hat, greift der frühe Rücksprung in den Leerzustand **vor** dem `<div>` mit dem Ref: der Ref ist leer, der Effekt bricht ab — und läuft wegen der leeren Abhängigkeitsliste nie wieder, auch nicht, wenn später Kacheln erscheinen und der `<div>` tatsächlich entsteht. Die Breite bleibt für die ganze Sitzung beim Startwert.
|
||||||
|
|
||||||
|
Belege (bestätigt, nicht zu wiederholen):
|
||||||
|
|
||||||
|
- Echter Linux-Client (Tessera-1.3.0.AppImage, WebKitGTK), leeres Dashboard, eine Kalender-Kachel hinzugefügt: Kachel 469 px breit in einem 1000 px breiten Container — das ist die Rechnung für 1200.
|
||||||
|
- Derselbe Client nach einem Neuladen mit vorhandener Kachel: 459 px in 1176 px, also richtig — weil die Ladeschranke in `(portal)/page.tsx` das Raster aus- und wieder einhängt, der Ref beim Einhängen also existiert.
|
||||||
|
- Screenshot des Nutzers (1920×1045): Spaltenbreite 51,5 px, Platzhalter klebt an Spalte 13, rechte Rasterkante bei x = 1459 bei einem Inhaltsbereich bis ~1920.
|
||||||
|
|
||||||
|
`react-grid-layout` vergleicht den Breakpoint strikt größer als (`width > breakpoint`), 1200 ist damit **nicht** `lg`, sondern `md` → 20 Spalten.
|
||||||
|
|
||||||
|
## Gebundene Entscheidungen (nicht neu verhandeln)
|
||||||
|
|
||||||
|
1. **Ref-Rückruf statt Einmal-Effekt.** Die Messung hängt am jeweils eingehängten Knoten und überlebt Aus- und Einhängen.
|
||||||
|
2. **Synchron vor dem ersten Zeichnen.** Der Ref-Rückruf läuft in der Commit-Phase; die dort ausgelöste Zustandsänderung wird vor dem Zeichnen abgearbeitet. Ein zusätzlicher `useLayoutEffect` ist damit überflüssig.
|
||||||
|
3. **Fenster-Horcher als Netz**, zusätzlich zum ResizeObserver, nicht statt seiner.
|
||||||
|
4. **Verhalten sonst unverändert.** `FREE_PLACEMENT_COMPACTOR` mit `preventCollision` bleibt (Nutzerentscheidung 22.09.2026: ein belegtes Feld bleibt blockiert, nichts wird zur Seite geschoben). Leerzustand, `BREAKPOINTS`, `COLS`, `rowHeight`, `margin`, `applyConstraintMinima` bleiben wortgleich.
|
||||||
|
5. **Startwert 1200 bleibt.** Er lebt nur noch bis zur Commit-Phase desselben Einhängens. Genau diesen Übergang 1200 → gemessen macht der Pfad „Neuladen mit Kacheln“ heute schon in Produktion, und er ist nachweislich richtig (459 px in 1176 px) — ein anderer Startwert würde eine bisher unerprobte Breakpoint-Folge einführen, ohne etwas zu verbessern.
|
||||||
|
6. **Nur `apps/web`.** Keine API, kein Prisma, kein Docker, keine neuen Pakete (ResizeObserver und resize sind Browser-Schnittstellen).
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||||
|
@~/.claude/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/STATE.md
|
||||||
|
@apps/web/src/components/dashboard/dashboard-grid.tsx
|
||||||
|
@apps/web/src/components/dashboard/dashboard-grid.test.tsx
|
||||||
|
@apps/web/src/test/fake-resize-observer.ts
|
||||||
|
@apps/web/src/test/setup.ts
|
||||||
|
</context>
|
||||||
|
|
||||||
|
## Schnittstellen, die der Executor kennen muss
|
||||||
|
|
||||||
|
- `apps/web/src/test/fake-resize-observer.ts` exportiert `stubResizeObserver({ width, height }): void`; es ersetzt den globalen ResizeObserver per `vi.stubGlobal` durch eine Klasse, deren `observe()` den Rückruf **sofort und synchron** mit `new DOMRectReadOnly(0, 0, width, height)` aufruft. Zurückgesetzt wird nur durch `vi.unstubAllGlobals()` — `vi.restoreAllMocks()` reicht dafür nicht.
|
||||||
|
- `apps/web/src/test/setup.ts` legt global einen ResizeObserver-Ersatz an, der immer 1200×800 meldet. Ohne `stubResizeObserver` misst jeder Test also 1200 — der neue Test wäre damit blind für genau diesen Fehler.
|
||||||
|
- `dashboard-grid.test.tsx` ersetzt `Responsive` durch einen Durchreicher, der die Props in `captured.props` ablegt (`vi.hoisted`, Mock per `importOriginal`, damit `noCompactor` echt bleibt). Die gemessene Breite ist dadurch als `captured.props?.width` prüfbar.
|
||||||
|
- jsdom liefert für `getBoundingClientRect()` ohne Zutun 0 — die synchrone Erstmessung schlägt im Test also nicht durch, der gestubbte Beobachter liefert den Wert. Für den Fenster-Test wird `Element.prototype.getBoundingClientRect` gezielt überschrieben.
|
||||||
|
- `new DOMRect(0, 0, w, h)` gibt es in jsdom (die Datei `fake-resize-observer.ts` nutzt bereits `DOMRectReadOnly`) — damit braucht der Test **keinen** Cast, und der Zähler `as unknown as` bleibt bei 6.
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="tracer" tdd="true">
|
||||||
|
<name>Aufgabe 1: Messung an den Knoten hängen (Ref-Rückruf, synchrone Erstmessung, Fenster-Horcher) — Ende-zu-Ende „leeres Dashboard → erste Kachel füllt die volle Breite“, zuerst rot</name>
|
||||||
|
<files>apps/web/src/components/dashboard/dashboard-grid.test.tsx, apps/web/src/components/dashboard/dashboard-grid.tsx</files>
|
||||||
|
<behavior>
|
||||||
|
Zuerst die Tests, beide müssen vor der Änderung rot sein:
|
||||||
|
|
||||||
|
- **Test 10 — Leerzustand → gefüllt:** `stubResizeObserver({ width: 1000, height: 800 })`; `captured.props` auf null setzen; mit `widgets: []` und leeren Layouts rendern → die Überschrift des Leerzustands steht da und `captured.props` ist weiterhin null (das Raster ist nicht eingehängt); danach **`rerender`** derselben Instanz mit einer Uhr-Kachel und einem passenden `lg`-Eintrag → `captured.props?.width` ist 1000. Rot vorher: der Wert bleibt 1200.
|
||||||
|
- **Test 11 — Fenstergröße:** `stubResizeObserver({ width: 1000, height: 800 })`; mit einer Uhr-Kachel rendern → Breite 1000; danach `Element.prototype.getBoundingClientRect` per `vi.spyOn(...).mockReturnValue(new DOMRect(0, 0, 1600, 800))` überschreiben und `window.dispatchEvent(new Event('resize'))` in `act(...)` auslösen → `captured.props?.width` ist 1600. Rot vorher: der Wert bleibt 1000, weil es keinen Fenster-Horcher gibt.
|
||||||
|
|
||||||
|
Die zehn bestehenden Fälle bleiben unverändert und grün — insbesondere Test 4 (Raster-Konstanten), Test 7 (Compactor-Pin mit preventCollision) und Test 9/9b (Minima).
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. **`dashboard-grid.test.tsx`** (zuerst, rot): `act` zusätzlich aus `@testing-library/react` importieren, `stubResizeObserver` aus `@/test/fake-resize-observer`. Im bestehenden `afterEach` nach `vi.restoreAllMocks()` eine Zeile `vi.unstubAllGlobals()` ergänzen (ohne sie bliebe der gestubbte Beobachter für alle folgenden Dateien stehen). Am Ende des bestehenden `describe('DashboardGrid', …)` die zwei Fälle aus dem `<behavior>`-Block anfügen, benannt „quick-260922-vdk Test 10: …“ und „quick-260922-vdk Test 11: …“, Stil und Kommentarsprache wie die Nachbarfälle (deutsch, mit dem Warum in einer Zeile). Rot-Lauf ausführen und die Fehlermeldungen für das SUMMARY festhalten.
|
||||||
|
|
||||||
|
2. **`dashboard-grid.tsx`**: `useCallback` zum Import aus `react` hinzufügen. Die bisherige Kombination aus `containerRef` und dem Effekt mit leerer Abhängigkeitsliste ersetzen durch:
|
||||||
|
- zwei Refs: `nodeRef` (Typ `HTMLDivElement | null`, hält den gerade eingehängten Knoten für den Fenster-Horcher) und `observerRef` (Typ `ResizeObserver | null`, hält den laufenden Beobachter);
|
||||||
|
- `applyWidth` als `useCallback` mit leerer Abhängigkeitsliste: nimmt eine Zahl, ruft `setWidth` nur bei endlichem Wert größer 0 auf. React verwirft gleiche Werte selbst, ein zusätzlicher Vergleich ist unnötig und eine Rückkopplungsschleife damit ausgeschlossen (T-VDK-01);
|
||||||
|
- `measureRef` als `useCallback` über `applyWidth`, Signatur nimmt `HTMLDivElement | null` und gibt **nichts** zurück: erst einen eventuell laufenden Beobachter trennen und `observerRef` leeren, dann `nodeRef` auf den Knoten setzen, bei `null` zurückspringen, sonst `applyWidth(node.getBoundingClientRect().width)` (das ist die synchrone Erstmessung in der Commit-Phase), dann einen neuen `ResizeObserver` anlegen, der `entries[0].contentRect.width` an `applyWidth` weitergibt, ihn auf den Knoten setzen und in `observerRef` merken. Wichtig: keine Aufräumfunktion zurückgeben — React 19 ruft den Rückruf sonst beim Aushängen nicht mehr mit `null` auf, und genau dieser Zweig ist hier der Aufräumpfad;
|
||||||
|
- einen `useEffect` über `applyWidth`, der `resize` am `window` anmeldet und im Aufräumschritt wieder abmeldet; der Horcher misst `nodeRef` erneut, wenn dort ein Knoten liegt;
|
||||||
|
- am Raster-`<div>` `ref={measureRef}` setzen (exakt dieser Name, ein Tor prüft ihn).
|
||||||
|
|
||||||
|
Alle vier Hooks stehen **vor** dem frühen Rücksprung in den Leerzustand, damit die Hook-Reihenfolge stabil bleibt — derselbe Grund, den der Kommentar bei `effectiveLayouts` bereits festhält.
|
||||||
|
|
||||||
|
3. **Warum-Kommentar** über der Messung, deutsch, im Stil der vorhandenen Blöcke (`quick-260916-dyv`, `quick-260916-bwo`), Präfix `quick-260922-vdk:`. Inhalt in eigenen Worten: der frühe Rücksprung in den Leerzustand rendert den gemessenen Knoten gar nicht erst, ein Effekt mit leerer Abhängigkeitsliste sieht ihn deshalb nie wieder; der Ref-Rückruf folgt dem Knoten über Aus- und Einhängen hinweg und misst in der Commit-Phase, also vor dem Zeichnen; die Folge der alten Annahme war ein `md`-Breakpoint mit 20 Spalten (strikter Größer-Vergleich in RGL), 51,6 px Spaltenbreite und ein toter Streifen rechts; der Fenster-Horcher ist ein Netz für Fälle, in denen der Beobachter nichts meldet; der Wächter verwirft 0 und nicht endliche Werte, damit eine kurzzeitig zusammengefallene Fläche das Raster nicht auf Null setzt. Den Startwert-Absatz (Entscheidung 5 oben) mit aufnehmen.
|
||||||
|
|
||||||
|
4. Nichts anderes anfassen: Leerzustand, `BREAKPOINTS`, `COLS`, `rowHeight`, `margin`, das fehlende `containerPadding`, `dragConfig`, `resizeConfig`, `FREE_PLACEMENT_COMPACTOR`, `applyConstraintMinima` und die `data-grid`-Erzeugung bleiben wortgleich.
|
||||||
|
|
||||||
|
Commit: `fix(quick-260922-vdk): Dashboard-Raster misst seine Breite auch aus dem Leerzustand heraus` (Wortlaut frei, Stil der Nachbarcommits, Co-Authored-By-Zeile).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/components/dashboard/dashboard-grid.test.tsx && pnpm --filter @tessera/web exec tsc --noEmit && grep -q "ref={measureRef}" apps/web/src/components/dashboard/dashboard-grid.tsx && grep -q "addEventListener('resize'" apps/web/src/components/dashboard/dashboard-grid.tsx && grep -q "unstubAllGlobals" apps/web/src/components/dashboard/dashboard-grid.test.tsx && grep -q "quick-260922-vdk" apps/web/src/components/dashboard/dashboard-grid.tsx</automated>
|
||||||
|
</verify>
|
||||||
|
<done>`dashboard-grid.test.tsx` hat 12 grüne Fälle (10 alte wortgleich, 2 neue); die zwei neuen waren nachweislich zuerst rot, die Rot-Meldungen („1200 statt 1000“ bzw. „1000 statt 1600“) stehen im SUMMARY. `tsc --noEmit` ohne Befund. Die Messung hängt am Ref-Rückruf `measureRef`, trennt den Beobachter im null-Zweig, misst synchron bei jedem Einhängen und hat einen Fenster-Horcher. Compactor, Konstanten, Leerzustand und `applyConstraintMinima` sind unverändert (Tests 4/7/9/9b belegen es). Ein Commit mit Scope `quick-260922-vdk`.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Aufgabe 2: Changelog-Stichpunkt, volle Tore, Zähler, Prüfliste für den Browser-Rundgang</name>
|
||||||
|
<files>CHANGELOG.md</files>
|
||||||
|
<action>
|
||||||
|
1. **`CHANGELOG.md`**: unter „## Unveröffentlicht“ nach der bestehenden Liste „### Geändert“ einen neuen Abschnitt „### Behoben“ anlegen (Reihenfolge wie in 1.3.0: Neu, Geändert, Behoben) mit genau einem Stichpunkt in Alltagssprache, Tonlage der Nachbarzeilen, ohne Fachbegriffe: „Dashboard: war das Dashboard beim Öffnen leer, nutzte die erste hinzugefügte Kachel nur einen Teil der Breite — rechts blieb ein toter Streifen, in den sich keine Kachel ziehen ließ; das Raster misst die verfügbare Breite jetzt in jedem Fall und folgt auch einer Änderung der Fenstergröße“.
|
||||||
|
|
||||||
|
2. **Volle Tore** ausführen und die Zahlen ins SUMMARY schreiben: `pnpm type-check` (4/4), `pnpm lint` (5/5), `pnpm --filter @tessera/web test`. Ausgangsmessung dieses Plans (22.09., vor der Änderung): Web 81 Dateien / 659 Tests grün, Biome web genau 53 Warnungen, `as unknown as` in web 6. Erwartet nachher: 81 Dateien / 661 Tests, Warnungen und Zähler unverändert. Weicht eine Zahl ab, im SUMMARY benennen statt stillschweigend anpassen.
|
||||||
|
|
||||||
|
3. **Prüfliste** für den Orchestrator ins SUMMARY schreiben (Browser, lokal, Playwright-MCP — **nicht** auf dem Testserver), Punkt für Punkt abhakbar; ausdrücklich dazuschreiben, dass Breiten am DOM gemessen werden (`getBoundingClientRect` der Elemente), **nie** per `fetch` aus der Seite heraus:
|
||||||
|
(a) Dashboard eines Benutzers ohne Kacheln öffnen (oder alle Kacheln entfernen und neu laden) → Leerzustand mit Bildmarke;
|
||||||
|
(b) Bearbeiten → „Widget hinzufügen“ → 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 höchstens der Rand von 8 px); vorher lag sie bei 1200 px unabhängig von der Fensterbreite;
|
||||||
|
(d) eine zweite Kachel auf ein belegtes Feld ziehen → sie springt zurück, nichts wird zur Seite geschoben (unverändert gewollt); Größe 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 hinzufügen → wieder volle Breite (der Beobachter wurde sauber getrennt und neu angehängt).
|
||||||
|
|
||||||
|
Commit: `docs(quick-260922-vdk): Changelog - Dashboard-Raster misst seine Breite auch aus dem Leerzustand` (nur CHANGELOG.md; Akte und STATE macht der Orchestrator; Co-Authored-By-Zeile).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/vicolab/projects/tessera-ctl && grep -q 'rechts blieb ein toter Streifen' CHANGELOG.md && awk '/^## Unveröffentlicht/{f=1} /^## 1\.3\.0/{f=0} f&&/^### Behoben/{n++} END{exit !(n==1)}' CHANGELOG.md && pnpm type-check && pnpm lint && pnpm --filter @tessera/web lint 2>&1 | grep -q 'Found 53 warnings' && test "$(grep -rn 'as unknown as' apps/web/src --include=*.ts --include=*.tsx | wc -l)" -eq 6 && pnpm --filter @tessera/web test</automated>
|
||||||
|
<human-check>Am Ende der Aufgabe: der Orchestrator geht die siebenpunktige Prüfliste im Browser durch (lokal, Playwright-MCP). Entscheidend ist Punkt (b)/(c): auf einem beim Öffnen leeren Dashboard füllt die erste Kachel den Inhaltsbereich bis zum rechten Rand, und die gemessene Rasterbreite stimmt mit der Breite des Inhaltsbereichs überein.</human-check>
|
||||||
|
</verify>
|
||||||
|
<done>Der Changelog trägt unter „Unveröffentlicht“ genau einen neuen Abschnitt „### Behoben“ mit dem einen Stichpunkt. `pnpm type-check` 4/4 und `pnpm lint` 5/5 ohne Befund der Stufe `error`, Biome web weiterhin genau 53 Warnungen, `as unknown as` in web weiterhin 6, Web-Tests 81 Dateien / 661 grün. Die siebenpunktige Prüfliste steht im SUMMARY. Insgesamt genau zwei Commits mit Scope `quick-260922-vdk` (`git log --oneline -2`).</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
ASVS-Stufe 1, Blockschwelle `high` (jede `high`-Bedrohung MUSS mitigiert sein).
|
||||||
|
|
||||||
|
## Vertrauensgrenzen
|
||||||
|
|
||||||
|
| Grenze | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| Browser-Layout → React-Zustand | Gemessene Breiten (ResizeObserver, `getBoundingClientRect`, `resize`) werden zu Zustand und steuern die Rastergeometrie; die Werte sind nicht vertrauenswürdig im Sinne von „immer sinnvoll“ (0 während eines Übergangs, nicht endlich bei zusammengefallener Fläche) |
|
||||||
|
| Knoten-Lebensdauer → Beobachter-Lebensdauer | Der ResizeObserver hängt an einem Knoten, der beim Wechsel Leerzustand ↔ gefüllt aus- und eingehängt wird |
|
||||||
|
| Neue Vertrauensgrenze | Keine. Kein Netzverkehr, keine API, keine Benutzereingabe, keine Persistenz in diesem Plan |
|
||||||
|
|
||||||
|
## STRIDE-Register
|
||||||
|
|
||||||
|
| ID | Kategorie | Komponente | Schwere | Disposition | Maßnahme |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| T-VDK-01 | Denial of Service (Rückkopplung Messung → Zustand → Layout → Messung, bis der Tab steht) | `applyWidth` / ResizeObserver-Rückruf in `dashboard-grid.tsx` | medium | mitigate | `applyWidth` ruft `setWidth` nur bei endlichem Wert größer 0; React verwirft gleiche Werte (Object.is) und rendert dann gar nicht neu — der Beobachter meldet nach dem Einschwingen denselben Wert und die Kette endet. Zusätzlich beobachtet der Beobachter den äußeren `<div>`, dessen Breite nicht von der Rastergeometrie abhängt. |
|
||||||
|
| T-VDK-02 | Denial of Service (Ressourcenleck: Beobachter oder Fenster-Horcher überleben das Aushängen, bei jedem Wechsel Leerzustand ↔ gefüllt einer mehr) | `measureRef` null-Zweig, `useEffect`-Aufräumschritt | medium | mitigate | Der Ref-Rückruf trennt den laufenden Beobachter als erste Handlung und gibt bewusst **keine** Aufräumfunktion zurück, damit React ihn beim Aushängen mit `null` aufruft; der Fenster-Horcher meldet sich im Aufräumschritt des Effekts ab. Punkt (g) der Prüfliste geht den Wechsel im Browser durch. |
|
||||||
|
| T-VDK-03 | Denial of Service (Messung auf einer zusammengefallenen Fläche setzt die Breite auf 0 → Raster unbedienbar, Kacheln unerreichbar) | Synchrone Erstmessung in jsdom-losen Übergängen, `getBoundingClientRect` | low | mitigate | Wächter „größer 0 und endlich“; bleibt eine Messung aus, gilt weiter der letzte gültige Wert statt 0. |
|
||||||
|
| T-VDK-04 | Tampering (stille Verhaltensänderung beim Ziehen: ein belegtes Feld würde plötzlich nachgeben) | `FREE_PLACEMENT_COMPACTOR`, `dragConfig`, `resizeConfig` | medium | mitigate | Diese Stellen werden nicht angefasst; Test 7 pinnt den Compactor als echten `noCompactor` plus `preventCollision: true`, Test 6 die Zieh-Konfiguration, Test 4 die Raster-Konstanten — alle laufen im Tor der Aufgabe 1 mit. Prüfliste (d) belegt es zusätzlich im Browser. |
|
||||||
|
| T-VDK-05 | Information Disclosure | — | low | accept | Es werden nur Layout-Maße des eigenen Fensters gelesen; nichts verlässt den Browser, nichts wird geloggt oder gespeichert. |
|
||||||
|
| T-VDK-06 | Repudiation | — | low | accept | Reine Anzeige-Geometrie ohne Fremdwirkung; kein Audit-Log nötig, ASVS 1 genügt. |
|
||||||
|
| T-VDK-SC | Tampering (Lieferkette) | npm-Installationen | high | mitigate | Nicht ausgelöst: KEINE neuen Pakete — `ResizeObserver`, `getBoundingClientRect`, `window`-Ereignisse und `DOMRect` sind Browser-Schnittstellen, `act` und `vi.spyOn` sind bereits vorhanden. Will der Executor doch etwas installieren: Stopp und Rückfrage an den Orchestrator, keine Installation ohne ausdrückliche Freigabe. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Automatisch (Executor, in den `<verify>`-Blöcken): gezielter Testlauf von `dashboard-grid.test.tsx` (12 Fälle), `tsc --noEmit`, Struktur-Tore auf Ref-Rückruf, Fenster-Horcher, `unstubAllGlobals` und den Warum-Kommentar; danach Changelog-Tore, `pnpm type-check` 4/4, `pnpm lint` 5/5, Biome web genau 53 Warnungen, `as unknown as` web 6, voller Web-Testlauf.
|
||||||
|
|
||||||
|
Rot-Nachweis (Aufgabe 1): beide neuen Fälle laufen vor der Änderung rot; die Meldungen gehören ins SUMMARY.
|
||||||
|
|
||||||
|
Manuell (Orchestrator, Prüfliste aus Aufgabe 2 Punkt 3, lokal im Browser, nicht auf dem Testserver): leeres Dashboard → erste Kachel füllt die Breite; gemessene Rasterbreite gleicht der Breite des Inhaltsbereichs; belegtes Feld bleibt blockiert; Fenstergröße; Neuladen mit Kacheln unverändert; Leerzustand → gefüllt → Leerzustand → gefüllt.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- [ ] Auf einem beim Öffnen leeren Dashboard füllt die erste hinzugefügte Kachel den Inhaltsbereich; rechts kein toter Streifen (Test 10 + Prüfliste b/c)
|
||||||
|
- [ ] Die Messung folgt dem Knoten über Aus- und Einhängen und läuft synchron vor dem Zeichnen (Ref-Rückruf `measureRef`, Struktur-Tor + Prüfliste f/g)
|
||||||
|
- [ ] Fenstergröße ändern misst neu (Test 11 + Prüfliste e)
|
||||||
|
- [ ] Ziehverhalten unverändert: belegtes Feld bleibt blockiert, nichts wird verschoben (Tests 4/6/7/9/9b grün, Prüfliste d)
|
||||||
|
- [ ] 12 Fälle in `dashboard-grid.test.tsx`, Web gesamt 81 Dateien / 661 Tests grün
|
||||||
|
- [ ] `pnpm type-check` 4/4, `pnpm lint` 5/5, Biome web 53 Warnungen, `as unknown as` web 6
|
||||||
|
- [ ] Changelog: genau ein Stichpunkt unter „Unveröffentlicht → Behoben“
|
||||||
|
- [ ] Genau zwei Commits mit Scope `quick-260922-vdk`
|
||||||
|
- [ ] Nur `apps/web` und `CHANGELOG.md` angefasst; keine neuen Pakete
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
`.planning/quick/260922-vdk-dashboard-raster-misst-seine-breite-nich/260922-vdk-SUMMARY.md` schreiben, wenn beide Aufgaben fertig sind — mit Rot-Meldungen der zwei neuen Tests, den gemessenen Zahlen (Tests, Warnungen, Zähler) und der siebenpunktigen Prüfliste für den Browser-Rundgang.
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user