Files
tessera-ctl/.planning/quick/260916-dyv-dashboard-nachbesserung-mindestgroessen-/260916-dyv-SUMMARY.md
T
schalli 963fa36cd7
Tessera CI/CD / Lint & Type Check (push) Successful in 48s
Tessera CI/CD / Tests (push) Successful in 1m7s
Tessera CI/CD / Build & Publish Images (push) Successful in 2m43s
docs(quick-260916-dyv): Nachbesserung abgeschlossen, verifiziert und im Browser bewiesen (Rechner-Minimum korrigiert) — Zusammenfassung, Verifikation, Aktenstand, Ledger #39 fixed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
2026-09-16 11:03:42 +02:00

36 KiB
Raw Blame History

phase, plan, subsystem, tags, requires, provides, affects, actuals, plan_head_before, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals plan_head_before tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
quick-260916-dyv 01 ui, dashboard
dashboard
react-grid-layout
react-draggable
drag-and-drop
tailwind
vitest
i18n
phase provides
quick-260916-bwo 24-Spalten/20-px-Raster, verdoppelte WIDGET_CONSTRAINTS, Container-Query-Widgets, gespeicherte Anordnungen mit minW/minH
WIDGET_CONSTRAINTS mit inhaltsgetriebenen Minima (clock 2/2, search 6/2, calendar 3/3, note 4/4, calculator 3/9, favorites 3/3, link 3/2, stopwatch 4/3), Vorgaben unveraendert, Test A pinnt alle 32 Werte
effectiveLayouts in dashboard-grid.tsx: gespeicherte minW/minH werden in jedem Breakpoint aus den Konstanten ueberschrieben, zu kleine w/h auf das Minimum angehoben (Addendum des Plan-Pruefers)
Feste Aktionsleiste unten rechts (Stift/Haekchen + Widget hinzufuegen), Grid direkt im Container p-2 — Rand oben 28 px statt 60 px
Ganze Kachel als Griff, Overlay-Kopfleiste 20 px mit Griff-Symbol und Tooltip, cancel-Selektor fuer Eingabefelder/Knoepfe/Links/[data-no-drag]/.widgetNoDrag, threshold 3
FREE_PLACEMENT_COMPACTOR = noCompactor + preventCollision: true — kein Ueberlappen beim Ablegen, Vergroessern stoppt am Nachbarn, freie Platzierung bleibt
Stoppuhr-Bedienleiste kompakt (px-2 py-1 text-xs), damit die Kachel auf 4x3 schrumpfen kann
Anwenderhandbuch: Schalter unten rechts, ganze Kachel ziehbar, Ablegen nur auf freiem Platz, Mindestgroesse = gerade noch bedienbar
dashboard
anwenderhandbuch
changelog
tokens tasks commits
11339 3 3
ec1b0ce582
added patterns
Konstanten schlagen persistierte Werte: gespeicherte Layout-Eintraege werden vor der Uebergabe an react-grid-layout aus WIDGET_CONSTRAINTS normalisiert (useMemo, keine Mutation der Store-Objekte)
react-grid-layout-Mock per vi.mock(..., async (importOriginal) => ({ ...original, Responsive: Mock })) — echte Helfer (noCompactor) bleiben testbar
Drag-Griff = ganze Karte, Ausnahmen per cancel-Selektor (exportierte Konstante, im DOM-Test mit matches/closest gegen den echten Selektor geprueft)
Overlay-Kopfleiste (absolute) statt Kopfleiste im Fluss, damit die Hoehenkette Karte -> Rumpf fuer Container-Queries definit bleibt
created modified
apps/web/src/app/(portal)/page.test.tsx
apps/web/src/components/dashboard/widget-registry.tsx
apps/web/src/components/dashboard/widget-registry.test.tsx
apps/web/src/components/dashboard/dashboard-grid.tsx
apps/web/src/components/dashboard/dashboard-grid.test.tsx
apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx
apps/web/src/components/dashboard/widgets/widget-wrapper.tsx
apps/web/src/components/dashboard/edit-mode-toggle.tsx
apps/web/src/app/(portal)/page.tsx
apps/web/src/messages/de.json
apps/web/src/messages/en.json
docs/anleitung-anwender.md
Gespeicherte minW/minH werden beim Rendern aus WIDGET_CONSTRAINTS ueberschrieben (effectiveLayouts) — RGL 2.2.3 nimmt gespeicherte Eintraege woertlich; ohne die Ueberschreibung aendert die Konstante fuer bestehende Widgets nichts
Zu kleine gespeicherte w/h werden auf das Minimum angehoben (Plan-Pruefer-Addendum): RGL rendert eine zu kleine Kachel woertlich und klemmt erst beim naechsten Resize
preventCollision statt Kompaktierung: freie Platzierung (Commit c8f3361) bleibt, Ablegen auf belegtem Feld springt an den Ausgangsort zurueck
Overlay-Kopfleiste statt Kopfleiste im Fluss: Rumpf bleibt h-full, cqh loest weiter auf
Stoppuhr-Bedienleiste als einzige Innen-Aenderung; Rechner bekommt statt Umbau ein hoeheres Minimum 3x9
Test 7 pinnt die Identitaets-Kopie ueber die Kernfelder plus moved/static false (Messbefund: cloneLayoutItem normalisiert), nicht per toEqual gegen die Eingabe
Falsifizierungen an unkommittierten Dateien per sed/Python zuruecksetzen, nie per git checkout (das setzt die ganze Datei auf HEAD)
QUICK-260916-DYV
id description requirement verification human_judgment
D1 Inhaltsgetriebene Mindestgroessen je Widget-Typ, Vorgaben unveraendert QUICK-260916-DYV
kind ref status
unit apps/web/src/components/dashboard/widget-registry.test.tsx#Test A (quick-260916-dyv) pass
false
id description requirement verification human_judgment
D2 Gespeicherte minW/minH werden in jedem Breakpoint ueberschrieben, zu kleine w/h angehoben QUICK-260916-DYV
kind ref status
unit apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260916-dyv Test 9 pass
kind ref status
unit apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260916-dyv Test 9b pass
false
id description requirement verification human_judgment rationale
D3 Bearbeiten-Schalter in fester Leiste unten rechts, Grid direkt im Container (Rand 28 px) QUICK-260916-DYV
kind ref status
unit apps/web/src/app/(portal)/page.test.tsx#Test 1..3 pass
kind ref status
automated_ui Browser-Nachweis Schritt 2/3 (Orchestrator, Playwright MCP) unknown
true Die 28 px und die Lage der Leiste sind nur im gerenderten Browser messbar
id description requirement verification human_judgment rationale
D4 Ganze Kachel als Griff, cancel-Selektor, Kopfleiste, kein Ueberlappen (preventCollision) QUICK-260916-DYV
kind ref status
unit apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260916-dyv Test 6/7/8 pass
kind ref status
automated_ui Browser-Nachweis Schritt 6/7 (Orchestrator) unknown
true Drag-Verhalten von react-draggable/RGL laeuft nicht in jsdom
id description requirement verification human_judgment rationale
D5 Stoppuhr-Bedienleiste kompakt, Kachel auf 4x3 bedienbar QUICK-260916-DYV
kind ref status
unit apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx (8 Tests gruen) pass
kind ref status
automated_ui Browser-Nachweis Schritt 4 (Stoppuhr bei Minimum) unknown
true Ob Stop/Runde/Reset in 4 Spalten passen, ist eine Layout-Messung im Browser
id description requirement verification human_judgment
D6 Anwenderhandbuch beschreibt Schalter, Ziehen, Mindestgroessen QUICK-260916-DYV
kind ref status
other grep -c: Unten rechts 1 / Oben rechts 0 / gerade noch bedienbar 1 / an ihren Ausgangspunkt 1 pass
false
15min 2026-09-16 complete

Quick 260916-dyv: Dashboard-Nachbesserung — Mindestgroessen inhaltsgetrieben, Schalter unten rechts, ganze Kachel als Griff Summary

Jedes Widget schrumpft wieder auf seine kleinste bedienbare Kachel — auch die bereits gespeicherten, weil dashboard-grid.tsx die in der Datenbank persistierten minW/minH beim Rendern aus WIDGET_CONSTRAINTS ueberschreibt (und zu kleine gespeicherte Groessen auf das Minimum anhebt); der Bearbeiten-Schalter sitzt in einer festen Leiste unten rechts, sodass das Grid 28 statt 60 px unter der Kopfzeile beginnt; im Bearbeitungsmodus ist die ganze Kachel der Griff, Eingabefelder/Knoepfe/Links/.widgetNoDrag starten kein Ziehen (cancel), und preventCollision: true am noCompactor verhindert Ueberlappen beim Ablegen und beim Vergroessern in einen Nachbarn. Drei Commits auf main (dc992c9, dbbd54f, cf97b5b), gepusht (7a6f42e..cf97b5b, nimmt die zwei Akten-Commits df16f46, ec1b0ce mit), CI-Lauf siehe unten.

Performance

  • Duration: 15 min bis zum Push (Start 08:32:57Z, Push 08:43:00Z), CI-Ende 08:47:45Z
  • Tasks: 3/3
  • Files: 12 (11 Web, 1 Handbuch) — git diff --stat df16f46 -- . ':!.planning' -> 12 files changed, 538 insertions(+), 99 deletions(-)

Ausgangslage und Bezugspunkt

HEAD bei Start ec1b0ce (Plan-Commit), Arbeitsbaum sauber, main 2 Akten-Commits vor origin/main (df16f46, ec1b0ce). Code-Gates gegen df16f46 (Code seit 1aefaa3 unveraendert). Vorbedingung Task 3: curl localhost:3002/api/v1/version -> {"version":"1.26.2"}, docker ps | grep -c '^gitea-runner$' -> 1.

Baseline frisch (Dashboard-Suite): Test Files 10 passed (10) / Tests 63 passed (63) — identisch mit der Planung.

Zweig-Hinweis: Alle Commits liegen auf main — Projektpraxis (branching_strategy: none in .planning/config.json, alle Vorgaenger-Quick-Tasks und der Plan-Commit ebenfalls auf main), vom Orchestrator so vorgeschrieben (Commit + git push). Der generische Executor-Schutz „nicht auf den Default-Zweig committen“ wurde deshalb bewusst nicht angewandt.

Abweichung von den Planzahlen (Addendum des Plan-Pruefers)

Der Plan-Pruefer hat eine Test-9-Variante verlangt (gespeicherte Groesse unter dem neuen Minimum wird angehoben). Umgesetzt als Test 9b in dashboard-grid.test.tsx. Dadurch verschieben sich alle Zaehlungen um +1:

Gate Plan Gemessen Befehl
Task 1 Dashboard-Suite 10 / 64 Test Files 10 passed (10) / Tests 65 passed (65) pnpm -C apps/web exec vitest run src/components/dashboard
dashboard-grid.test.tsx nach Task 1 6 Tests 7 passed (7) pnpm -C apps/web exec vitest run src/components/dashboard/dashboard-grid.test.tsx
Task 2 Teil-Suite 13 / 76 Test Files 13 passed (13) / Tests 77 passed (77) pnpm -C apps/web exec vitest run src/components/dashboard "src/app/(portal)/page.test.tsx" src/messages
dashboard-grid.test.tsx nach Task 2 9 Tests 10 passed (10) wie oben, einzelne Datei
Web gesamt 47 / 293 Test Files 47 passed (47) / Tests 294 passed (294) pnpm -C apps/web exec vitest run

Unveraendert wie geplant: widget-registry.test.tsx 11, stopwatch-widget.test.tsx 8, page.test.tsx 3, src/messages 6, API Test Files 67 passed (67) / Tests 1078 passed (1078).

Was react-grid-layout mit einem zu kleinen Eintrag sonst taete (gemessen, 2.2.3): synchronizeLayoutWithChildren (chunk-WGL5FSZH.mjs:559-562) klont den gespeicherten Eintrag woertlich — h: 8 bleibt h: 8, auch wenn minH (aus der Ueberschreibung) 9 ist. Geklemmt wird nur beim Vergroessern/Verkleinern: minMaxSize.constrainSize (chunk-KDANGDDL.mjs:26-31, clamp(h, item.minH ?? 1, ...)) und die Pixel-minConstraints an Resizable (chunk-WGL5FSZH.mjs:472-475). correctBounds (chunk-76RTO6EO.mjs:257-277) prueft nur Spaltenueberlauf, nicht Minima. Folge ohne Anhebung: ein gespeicherter Rechner mit h: 8 bliebe bis zum ersten Anfassen des Groessen-Griffs unten abgeschnitten (unterste Tastenreihe fehlt) und spraenge dann auf 9. Mit der Anhebung in applyConstraintMinima (w: Math.max(entry.w, minW), h: Math.max(entry.h, minH)) ist er sofort vollstaendig; beim naechsten Speichern landen 9 Zeilen in der DB.

Task 1 — Mindestgroessen, Ueberschreibung, Stoppuhr (dc992c9, 5 Dateien)

RED pnpm -C apps/web exec vitest run src/components/dashboard/widget-registry.test.tsx src/components/dashboard/dashboard-grid.test.tsx:

× Test A (quick-260916-dyv): Minima = kleinste bedienbare Kachel je Typ, Vorgaben unveraendert
× quick-260916-bwo Test 5: Widget ohne gespeicherten Eintrag bekommt die Vorgaben und die inhaltsgetriebenen Minima als data-grid
× quick-260916-dyv Test 9: gespeicherte minW/minH werden in JEDEM Breakpoint aus WIDGET_CONSTRAINTS ueberschrieben, x/y/w/h bleiben, unbekannte Typen und Schluessel unveraendert
× quick-260916-dyv Test 9b: gespeicherte Groesse unter dem neuen Minimum wird auf das Minimum angehoben (Rechner h 8 -> 9, w bleibt wenn >= minW)
Tests  4 failed | 14 passed (18)

(Plan erwartete 3 failed; +1 durch Test 9b.)

GREEN — WIDGET_CONSTRAINTS auf die Tabelle, applyConstraintMinima + useMemo (effectiveLayouts, vor dem Leerzustand wegen Hook-Reihenfolge) in dashboard-grid.tsx, layouts={effectiveLayouts} und data-grid-Fallback aus effectiveLayouts.lg, Stoppuhr-Knoepfe px-2 py-1 text-xs und Zeile gap-1 py-1.

Gates: Dashboard-Suite 10 passed (10) / 65 passed (65); Greps minW: 3, minH: 9 1, minW: 2, minH: 2, defaultW: 4, defaultH: 4 1, defaultW: 12, defaultH: 4 1; effectiveLayouts 3; Stoppuhr px-2 py-1 text-xs 4, px-4 py-1.5 text-sm 0; TSC_web=0.

Falsifizierung (a) — clock.minW/minH in widget-registry.tsx auf den bwo-Wert 4/4: Tests 3 failed | 15 passed (18): „Test A (quick-260916-dyv): Minima = kleinste bedienbare Kachel je Typ, Vorgaben unveraendert“, „quick-260916-bwo Test 5: …“, „quick-260916-dyv Test 9: …“. Zurueckgesetzt, gruen. (Lehre: git checkout -- <Datei> an der noch unkommittierten Datei hat die gesamte Task-1-Aenderung zurueckgesetzt — erneut angewandt, Gates erneut gruen; bei (b)/(c) deshalb per sed/Python zurueckgestellt.)

Task 2 — Leiste unten rechts, Griff, cancel, preventCollision (dbbd54f, 8 Dateien)

RED pnpm -C apps/web exec vitest run src/components/dashboard/dashboard-grid.test.tsx "src/app/(portal)/page.test.tsx":

× Test 1: Ansichtsmodus — Stift in fester Leiste unten rechts, kein "Widget hinzufuegen", kein mt-8/top-2, Grid direkt im Container p-2
× Test 2: Bearbeitungsmodus — "Widget hinzufuegen" links neben dem Haekchen in derselben Leiste, Grid im Bearbeitungsmodus
× quick-260916-dyv Test 6: dragConfig-Pin — handle Karte, cancel fuer Interaktives, threshold 3; resizeConfig folgt dem Bearbeitungsmodus
× quick-260916-dyv Test 7: Compactor-Pin — echter noCompactor plus preventCollision: true, compact ist Identitaets-Kopie (freie Platzierung)
× quick-260916-dyv Test 8: cancel/handle-Semantik im DOM — Karte ist Griff, Loesch-Knopf/Eingaben/widgetNoDrag passen auf cancel, Kopfleiste als Overlay nur im Bearbeitungsmodus
Tests  5 failed | 8 passed (13)

page-Test 3 (Umschalten) war wie vorhergesagt bereits gruen. importOriginal fuer react-grid-layout laeuft in jsdom — kein Rueckfall auf den Objekt-Mock noetig.

Messbefund Test 7: Der echte noCompactor.compact ist cloneLayout -> cloneLayoutItem (chunk-76RTO6EO.mjs:204-223): kopiert i/x/y/w/h unveraendert, normalisiert aber moved/static zu false und fuegt minW/maxW/minH/maxH/... als undefined hinzu. Das im Plan vorgesehene toEqual gegen die Eingabe scheitert deshalb an moved: false, static: false (erster GREEN-Lauf: 1 failed | 76 passed (77)). Test 7 pinnt nun toMatchObject({ i, x, y, w, h, moved: false, static: false }), neue Referenzen fuer Array und Element, und dass die Eingabe unveraendert bleibt — inhaltlich dieselbe Aussage (Identitaet als Kopie, keine Verschiebung).

GREEN — WIDGET_DRAG_HANDLE_SELECTOR, WIDGET_DRAG_CANCEL_SELECTOR, FREE_PLACEMENT_COMPACTOR: Compactor = { ...noCompactor, preventCollision: true }, dragConfig mit handle/cancel/threshold: 3; widget-wrapper.tsx neu (Karte im Bearbeitungsmodus widget-drag-handle cursor-grab active:cursor-grabbing border-primary/40, Overlay-Kopfleiste absolute inset-x-0 top-0 z-10 flex h-5 ... bg-muted/70 mit Sechs-Punkte-SVG, Tooltip dragHint, data-testid="widget-drag-head", Loesch-Knopf h-5 w-5 mit data-no-drag=""; Rumpf @container-size h-full als reines Literal); edit-mode-toggle.tsx shadow-lg, inaktiv border border-border bg-card; page.tsx feste Leiste fixed bottom-6 right-6 z-20 flex items-center gap-2 (erst „Widget hinzufuegen“, dann Umschalter), Grid direkt im Container; dragHint in de/en direkt nach deleteTooltip.

Gates: Teil-Suite 13 passed (13) / 77 passed (77) (dashboard-grid 10, page 3, messages 6); Greps preventCollision: true 2 (Definition + Kommentar), WIDGET_DRAG_CANCEL_SELECTOR 2, threshold: 3 2 (Wert + Kommentar), mt-8 0, top-2 0, Leiste 1, h-[6px] 0, data-no-drag 3, cursor-grab 2, "dragHint" je 1, W_EXIT=0, W_EMPTY=0, TSC_web=0.

Falsifizierungen (b) und (c) (Rueckstellung per sed/Python, danach Tests 13 passed (13) und nur die Arbeitsdateien im Diff):

Falsifizierung Eingriff Rot (Testname woertlich)
(b1) Zeile cancel: WIDGET_DRAG_CANCEL_SELECTOR, aus dragConfig entfernt `Tests 1 failed
(b2) FREE_PLACEMENT_COMPACTOR = { ...noCompactor } (ohne preventCollision) `Tests 1 failed
(c) <div className="mt-8"> um DashboardGrid wieder eingefuegt `Tests 1 failed

Task 3 — Handbuch, Abschluss-Gates, Push, CI (cf97b5b, 1 Datei)

Handbuch-Greps: Unten rechts auf dem Dashboard 1, Oben rechts auf dem Dashboard 0, gerade noch bedienbar 1, an ihren Ausgangspunkt 1.

Abschluss-Gates (alle nach dem Handbuch-Edit, vor dem Commit):

Gate Ergebnis
Web pnpm -C apps/web exec vitest run Test Files 47 passed (47) / Tests 294 passed (294) (Plan 293, +1 Test 9b)
API pnpm -C apps/api exec vitest run Test Files 67 passed (67) / Tests 1078 passed (1078)
tsc --noEmit shared / api / web TSC_packages/shared=0, TSC_apps/api=0, TSC_apps/web=0
pnpm install --frozen-lockfile FROZEN=0
git diff --stat df16f46 -- . ':!.planning' GIT_EXIT=0, 12 files changed, 538 insertions(+), 99 deletions(-)
Unantastbar-Stichprobe (.env*, Compose, Lockfile, package.json, prisma, api/src/dashboard, globals.css, umlaut-dictionary.ts, dashboard-store.ts, grid-layout-migration.ts) U_EXIT=0, U_EMPTY=0 (leer)
git status -sb vor Push / nach git fetch ## main...origin/main [voraus 5] / ## main...origin/main

Push: 7a6f42e..cf97b5b main -> main (fuenf Commits: df16f46, ec1b0ce Akten; dc992c9, dbbd54f, cf97b5b Code/Handbuch).

CI-Lauf nach dem Push

Feld Versuch 1 (einziger)
Lauf-ID 353 (event push, ref main, head_sha cf97b5b64b48ac08cb410966e98cf532f600681d)
status / conclusion completed / success
started_at / completed_at 10:43:05 / 10:47:45 (+02:00) — 4 min 40 s (15 Polls a 20 s; beim ersten Poll 7 s nach dem Push bereits in_progress, keine Warteschlange)
Jobs Lint & Type Check success (08:43:05-08:43:53Z), Tests success (08:43:55-08:44:51Z), Build & Publish Images success (08:44:53-08:47:45Z)
api:beta node APP_VERSION APP_CHANNEL APP_COMMIT v1.0.0-16-gcf97b5b beta cf97b5b — APP_COMMIT = git rev-parse --short HEAD = cf97b5b; APP_VERSION im git describe-Format (wie in 260916-bwo gemessen)
docker image inspect Created api:beta 2026-09-16T10:46:42+02:00 (per docker pull geholt, kein Container gebaut oder gestartet)

Beobachtung per Hintergrund-Skript (watch-ci.sh im Scratchpad, Token nur in einer Shell-Variablen, nie ausgegeben), Gitea-API GET /repos/schalli/tessera-ctl/actions/runs?limit=5, Eintrag mit head_sha == PUSHED. Keine Infrastruktur- oder Code-Fehler, kein erneuter Lauf noetig.

Git-Stand vor dem SUMMARY (Wahrheit)

git status --porcelain (leer):

git log --oneline ec1b0ce..HEAD:

cf97b5b docs(quick-260916-dyv): Anwenderhandbuch — Bearbeiten-Schalter unten rechts, ganze Kachel ziehbar, Mindestgroessen
dbbd54f feat(quick-260916-dyv): Bearbeiten-Schalter unten rechts (Rand oben 28 px), ganze Kachel als Griff mit Kopfleiste, cancel-Selektor, kein Ueberlappen beim Ablegen
dc992c9 fix(quick-260916-dyv): Mindestgroessen inhaltsgetrieben (kleinste bedienbare Kachel je Typ), gespeicherte Minima ueberschrieben, Stoppuhr-Bedienleiste kompakt

commits: 3 gemessen: git rev-list --count ec1b0ce5823f5f5275d0e79e231e8378f04ad0ea..HEAD -> 3. actuals.tokens 11339 = Zeichen des Diffs git diff df16f46 -- . ':!.planning' (45356) / 4; zum Vergleich Zeichen der 12 geaenderten Dateien gesamt 170986 / 4 = 42746. Plan-Schaetzung 90000 (confidence low) — deutlich zu hoch fuer den Diff.

Gemessene Befunde und Entscheidungen

  • Zweiter Grund fuer „nicht klein genug“ (der im Auftrag nicht genannte): Gespeicherte minW/minH schlagen data-grid. synchronizeLayoutWithChildren liest data-grid nur fuer Kinder ohne Eintrag; alle bestehenden Widgets haben einen Eintrag mit den in 260916-bwo verdoppelten Minima. Ohne effectiveLayouts aendert die Konstantentabelle fuer den User NICHTS Sichtbares — Test 9 ist der entscheidende Test, nicht Test A.
  • Kollision und preventCollision: noCompactor = { type: null, allowOverlap: false, compact: cloneLayout }, preventCollision fehlt (false). Beim Ziehen auf ein belegtes Feld (moveElement -> moveElementAwayFromCollision, chunk-76RTO6EO.mjs:279-328, Zweig collisionNorth && compactType === null): das GEZOGENE springt auf die Zeile des getroffenen (collidesWith.y = itemToMove.y), das getroffene rutscht um seine EIGENE Hoehe nach unten (itemToMove.y += itemToMove.h), keine Kaskade, keine Nachpruefung; compact ist Identitaet — Ueberlappungen bleiben. Beim Vergroessern mit dem se-Griff ist shouldMoveItem false, moveElement wird nicht gerufen: die Ueberlappung entsteht stumm. Mit preventCollision: true: if (hasCollisions && preventCollision) { l.x = oldX; l.y = oldY; l.moved = false; return layout; } — Platzhalter bleibt am Ursprung; Vergroessern prueft getAllCollisions am neuen Mass und behaelt das alte. Entscheidung: preventCollision statt verticalCompactor, weil die freie Platzierung (Commit c8f3361, „prevent auto-compaction on drag“) gewollt ist — Kompaktierung wuerde Luecken automatisch schliessen. preventCollision lebt am Compactor-Objekt (compactor.preventCollision ?? false, chunk-WGL5FSZH.mjs:666), daher ein eigenes Objekt.
  • Griff/cancel: react-draggable 4.7.0 prueft cancel NACH handle, beides vom Ereignisziel aufwaerts bis zum RGL-Element (Draggable.js:417, matchesSelectorAndParentsTo). Die Karte ist Kind des RGL-Elements, also passt jedes Ziel in der Karte auf den Griff; cancel gewinnt fuer Eingabefelder/Knoepfe/Links/[contenteditable]/[data-no-drag]/.widgetNoDrag. RGL haengt .react-resizable-handle selbst voran (chunk-WGL5FSZH.mjs:526). Die 12 bislang toten widgetNoDrag-Stellen (Favoriten 5, Link 7) sind damit wirksam. Test 8 fuehrt matches/closest mit dem echten Selektor in jsdom aus (T-DYV-02: ungueltiger Selektor wuerfe dort).
  • Overlay-Kopfleiste statt Kopfleiste im Fluss: Der Rumpf bleibt h-full, die Hoehenkette Karte -> Rumpf bleibt definit, cqh loest weiter auf (Grund fuer h-full in 260916-bwo). Eine Kopfleiste im Fluss haette den Rumpf im Bearbeitungsmodus um 20 px gekuerzt und den Rechner bei Mindestgroesse beschnitten. Preis: die obersten 20 px des Widget-Inhalts liegen im Bearbeitungsmodus unter der halbtransparenten Leiste (bg-muted/70) — im Ansichtsmodus gibt es keine Leiste.
  • Stoppuhr-Bedienleiste als einzige Innen-Aenderung: Bei Mindestbreite 4 Spalten (lg 191 px, Rumpf 179 px) ueberlief die laufende Stoppuhr (Stop + Runde + Reset mit px-4 text-sm ca. 222 px) — Reset abgeschnitten; kompakt (px-2 text-xs) ca. 151 px passt, Zeile 32 statt 48 px. Ohne diese Aenderung waere das inhaltsgetriebene Minimum 6x4 — BREITER als heute, das Gegenteil des Auftrags. Der Rechner wird NICHT umgebaut: er bekommt das hoehere Minimum 3x9 (heute 4x8), weil bei 8 Zeilen (216 px) die unterste Tastenreihe (+/-, 0, Komma, =) abgeschnitten ist (Anzeige 40 + Speicherzeile 28 + 5 Reihen 28 + Abstaende = 240 px > 216 px).
  • Stift unten rechts: Im Ansichtsmodus kann der 36x36-px-Stift (bei bottom-6 right-6) im schlimmsten Fall die untere rechte Ecke eines Widgets verdecken, das bis an den rechten Rand und bis unter den Viewport-Rand reicht — hingenommen (der bisherige „Widget hinzufuegen“-Knopf lag bereits dort, nur im Bearbeitungsmodus).
  • fixed und z-20: fixed bezieht sich auf das Viewport (kein transformierter Vorfahr — der bisherige Knopf funktionierte an derselben Stelle); z-20 liegt ueber RGL-Elementen (gezogenes Element z-index: 3).
  • Mock per importOriginal: laeuft in jsdom; noCompactor ist echt, Responsive bleibt Mock.

Mindestgroessen-Tabelle (gerechnet; Browser-Bestaetigung durch den Orchestrator)

Spaltenbreite lg bei 1200 px = 41,67 px (Kachelbreite(w) = 49,67w - 8), beim Orchestrator-Viewport von 260916-bwo 60 px; Kachelhoehe(h) = 28h - 8 px.

Typ bwo-Minimum neu px bei Spalte 41,67 / 60 Rechnung Bewusste Grenze
clock 4/4 2/2 91x48 / 128x48 Zeit min(20cqw, 50cqh) ca. 17-20 px, 8 Zeichen tabular ca. 80 px in 83 px Datum (falls an) 10 px darunter, eng
search 6/4 6/2 290x48 / 400x48 Auswahl 120 + Eingabe >= 80 + Knopf 40 + Abstaende 28 = 268 <= 278 Rumpf; Eingabe h-8 32 px in 48 px 3 Spalten (141 px) waeren unbrauchbar — Breite bleibt 6
calendar 6/6 3/3 141x76 / 196x76 eine Terminzeile 48 px + p-1.5 = 60 <= 76; Leertext 3 Zeilen text-sm 60 px knapp; mehr Termine brauchen mehr Hoehe
note 4/6 4/4 191x104 / 264x104 Kopfzeile ca. 36 + Vorschau >= 2 Zeilen (40) = 76 <= 104; Editiermodus Werkzeugleiste ca. 29 + 2 Zeilen —
calculator 4/8 3/9 141x244 / 196x244 Breite 4 Tasten x >= 30 + 3x4 + p-1 8 = 140 <= 141; Hoehe 40 + 4 + 28 + 4 + 5x28 + 4x4 = 156 + p-1 8 = 240 <= 244 Minimum HOEHER als 4x8, weil bei 8 Zeilen die unterste Tastenreihe abgeschnitten ist; gespeicherte 4x8-Rechner werden auf h 9 angehoben
favorites 4/6 3/3 141x76 / 196x76 Umschaltzeile (nur Bearbeitungsmodus) ca. 110 <= 133; drei Listenzeilen (20 + 4) in 68 px; Kachelraster grid-cols-3 ca. 40-px-Kacheln —
link 4/4 3/2 141x48 / 196x48 Listenzeile Symbol 20 + Titel in 40 px Rumpf; Umschaltzeile <= 133 Kachelansicht (Symbol 32 + Text 16 + Abstand) braucht 3 Zeilen — der User zieht eine Zeile hoeher
stopwatch 4/4 4/3 191x76 / 264x76 NUR mit kompakter Leiste: laufend Stop+Runde+Reset ca. 151 <= 179; Zeile 32 px; Anzeige min(16cqw, 35cqh) = 26,6 px im Rest von 32 px Rundenliste (max-h-32) braucht mehr Hoehe — Nebenfunktion

Vorgaben (defaultW/defaultH) unveraendert: clock 4/4, search 12/4, calendar 8/12, note 6/8, calculator 6/10, favorites 6/10, link 4/4, stopwatch 6/6 — neue Widgets erscheinen wie gewohnt; alle Minima <= Vorgabe (it.each prueft es).

Durchsicht Dashboard (gerechnet; Browser-Bestaetigung durch den Orchestrator)

Widget Standardgroesse Mindestgroesse Befund
Uhr 4x4, Zeit skaliert (bwo) 2x2: Zeit ca. 17-20 px lesbar gut
Suche 12x4 6x2: Auswahl, Eingabe >= 80 px, Knopf gut
Kalender 8x12 3x3: Leertext oder eine Terminzeile auffaellig: knapp, aber bedienbar
Notizen 6x8 4x4: Titel + 2 Zeilen gut
Rechner 6x10, Tasten skalieren (bwo) 3x9: alle 5 Tastenreihen inkl. „=“ gut; Todo: Innenaufbau kompakter, damit < 9 Zeilen reichen
Favoriten 6x10 3x3: Umschaltzeile + Listenzeilen gut
Link 4x4 3x2: eine Listenzeile auffaellig: Kachelansicht braucht 3 Zeilen
Stoppuhr 6x6 4x3: Anzeige 26,6 px, Stop/Runde/Reset sichtbar gut; Todo: Rundenliste bei Minimum

Bleibt als Todo (nicht Teil dieses Auftrags):

  • Rechner-Innenaufbau kompakter (Speicherzeile/Anzeige), damit weniger als 9 Zeilen reichen.
  • Rundenliste / laufende Stoppuhr bei Minimum 4x3: die Liste (max-h-32) hat dort keinen Platz.
  • Hart englische Texte im Einstellungsformular (aus 260916-bwo: „Timezone“, „Saving...“, „Title“, englischer Kalender-Hinweis).
  • Link-Kachelansicht bei 2 Zeilen (braucht 3).

Deviations from Plan

Auto-fixed Issues

1. [Addendum des Plan-Pruefers] Zu kleine gespeicherte w/h werden angehoben

  • Found during: Task 1 (vom Pruefer vorgegeben)
  • Issue: Ein gespeicherter Rechner mit h: 8 bliebe trotz minH: 9 unten abgeschnitten, bis er einmal angefasst wird (RGL klemmt nur beim Resize).
  • Fix: applyConstraintMinima setzt w: Math.max(w, minW), h: Math.max(h, minH) im selben Durchlauf; Test 9b.
  • Files modified: dashboard-grid.tsx, dashboard-grid.test.tsx
  • Commit: dc992c9

2. [Rule 1 - Messbefund] Test 7 nicht per toEqual gegen die Eingabe

  • Found during: Task 2 GREEN
  • Issue: Der echte cloneLayoutItem normalisiert moved/static zu false; toEqual scheiterte.
  • Fix: toMatchObject auf Kernfelder + moved: false, static: false, Referenzen ungleich, Eingabe unveraendert.
  • Files modified: dashboard-grid.test.tsx
  • Commit: dbbd54f

3. [Prozess] Falsifizierung (a) hat per git checkout die unkommittierte Registry-Aenderung mit zurueckgesetzt

  • Fix: Aenderung erneut angewandt, alle Task-1-Gates erneut gemessen (identisch), dann committet. (b)/(c) per sed/Python zurueckgestellt.

Sonst: Plan exakt wie geschrieben ausgefuehrt. Keine neuen Pakete, kein Schema, keine .env*/Compose/Lockfile-Aenderung, umlaut-dictionary.ts unangetastet.

Threat Flags

Keine neue Angriffsflaeche ausserhalb des <threat_model>: keine API-Aenderung, keine Benutzerdaten in Selektoren oder Styles, effectiveLayouts ersetzt manipulierte Minima (T-DYV-01, Test 9 mit unbekanntem Typ ohne Absturz).

Known Stubs

Keine.

Was bewusst offen bleibt

  • Der Browser-Nachweis (8 Schritte, unten) — Sache des Orchestrators/Verifizierers; das lokale Web-Abbild stammt aus 1aefaa3 und muss VORHER neu gebaut werden (docker compose up -d --build web).
  • Die vier Todos aus der Durchsicht (Rechner-Innenaufbau, Rundenliste, englische Formulartexte, Link-Kachelansicht).
  • Die 20 px hohe Overlay-Kopfleiste verdeckt im Bearbeitungsmodus den obersten Streifen des Widget-Inhalts halbtransparent — bewusst (Hoehenkette), im Browser beurteilen.
  • Der Stift kann im Ansichtsmodus 36x36 px eines Eck-Widgets verdecken.
  • Mandantenfaehigkeit/alpha/live: nichts geaendert; gespeicherte Anordnungen dort tragen die alten Minima, bis der Benutzer einmal speichert — sichtbar ist das nicht, weil effectiveLayouts sie beim Rendern ohnehin ersetzt.

Fuer den Verifizierer/Orchestrator (Browser-Nachweis)

Vorher: docker compose up -d --build web (die API braucht keinen Neubau). Playwright MCP gegen http://localhost:3000, Bounding-Boxen per boundingBox(), nie per fetch aus der Seite. Anmelden admin / admin123.

  1. Aufbau: „Dashboard bearbeiten“ (Stift UNTEN RECHTS), nacheinander Uhr, Suchleiste, Rechner, Stoppuhr, Notizen, Kalender, Favoriten, Link hinzufuegen (jedes erscheint unter dem vorigen), Bearbeitungsmodus beenden (Haekchen unten rechts), Seite neu laden.

  2. Rand oben: main.app-shell-main oben vs. erstes [data-widget-id] oben -> 28 px (+/-1; vorher 60). Linker Rand 28 px, Luecke zwischen Nachbarn 8 px (unveraendert).

  3. Feste Leiste: Bounding-Box des Stifts (aria-label „Dashboard bearbeiten“): Abstand zum rechten und unteren Viewport-Rand je 24 px, 36x36 px, Karten-Hintergrund + Rahmen + Schatten; im Bearbeitungsmodus „Widget hinzufuegen“ links neben dem Haekchen in derselben Leiste; kein Element mehr oben rechts im Dashboard-Container.

  4. Mindestgroessen: je Widget den Groessen-Griff unten rechts weit nach oben links ziehen; Bounding-Box mit der Tabelle vergleichen (Spaltenbreite (Containerbreite - 200)/24 px, Zeile 20 px + 8 px): Uhr 2x2, Suche 6x2, Kalender 3x3, Notizen 4x4, Rechner 3x9, Favoriten 3x3, Link 3x2, Stoppuhr 4x3. Screenshot je Widget. Bedienbarkeit: Uhrzeit lesbar; Suchfeld >= 80 px, Knopf sichtbar; Rechner ALLE fuenf Tastenreihen inkl. „=“; Stoppuhr starten -> Stop, Runde, Reset sichtbar und klickbar; Notiz Titel + 2 Zeilen; Kalender Leertext/Terminzeile; Favoriten Umschaltzeile + Listenzeilen; Link eine Listenzeile. Faellt ein Widget durch: Kachelgroesse und Grund notieren (dann Konstante + Test A anpassen — kleiner Korrekturlauf).

  5. Persistierte Minima: nach einem Speichern (Haekchen) docker exec tessera-ctl-db-1 psql -U tessera -d tessera -At -c 'SELECT layouts->''lg'' FROM "DashboardLayout";' -> Eintraege tragen die neuen minW/minH (und der Rechner h >= 9). Gegenprobe A (jsonb_set): beim Uhr-Eintrag minW: 8, minH: 8 setzen, Seite neu laden, Uhr auf 2x2 verkleinern -> geht. Gegenprobe B (kompletter Probe-Datensatz in NEUEN Raster-Einheiten mit verdoppelten Minima und einem Rechner mit h 8) — setzt voraus, dass fuer admin noch keine DashboardLayout-Zeile existiert (sonst vorher DELETE FROM "DashboardLayout" WHERE "userId" = (SELECT id FROM "User" WHERE username = 'admin');) und dass zwei WidgetInstance-Zeilen mit den Ids probe-clock und probe-calc angelegt werden; __gridVersion: 2 ist Pflicht, sonst verdoppelt migrateGridLayouts beim Laden erneut:

    INSERT INTO "WidgetInstance" (id, "userId", "tenantId", "widgetType", config, "createdAt", "updatedAt")
    SELECT 'probe-clock', u.id, u."tenantId", 'clock', '{}'::jsonb, now(), now() FROM "User" u WHERE u.username = 'admin';
    INSERT INTO "WidgetInstance" (id, "userId", "tenantId", "widgetType", config, "createdAt", "updatedAt")
    SELECT 'probe-calc', u.id, u."tenantId", 'calculator', '{}'::jsonb, now(), now() FROM "User" u WHERE u.username = 'admin';
    INSERT INTO "DashboardLayout" (id, "userId", "tenantId", layouts, "createdAt", "updatedAt")
    SELECT gen_random_uuid()::text, u.id, u."tenantId",
      '{"__gridVersion": 2,
        "lg":  [{"i":"probe-clock","x":0,"y":0,"w":4,"h":4,"minW":8,"minH":8},
                {"i":"probe-calc","x":4,"y":0,"w":6,"h":8,"minW":4,"minH":8}],
        "md":  [{"i":"probe-clock","x":0,"y":0,"w":4,"h":4,"minW":8,"minH":8},
                {"i":"probe-calc","x":4,"y":0,"w":6,"h":8,"minW":4,"minH":8}],
        "sm":  [], "xs": [], "xxs": []}'::jsonb,
      now(), now()
    FROM "User" u WHERE u.username = 'admin';
    

    Erwartung nach Neuladen: die Uhr (gespeichert minW/minH 8, also groesser als ihre eigene Kachel 4x4) laesst sich auf 2x2 verkleinern; der Rechner wird SOFORT mit 9 Zeilen gerendert (Bounding-Box-Hoehe 244 px statt 216 px), alle fuenf Tastenreihen sichtbar, ohne dass er angefasst wurde. Nach einem Speichern zeigt die SQL-Abfrage minW 2/minH 2 bei der Uhr und h 9, minW 3, minH 9 beim Rechner. Danach aufraeumen: DELETE FROM "DashboardLayout" WHERE "userId" = (SELECT id FROM "User" WHERE username = 'admin'); DELETE FROM "WidgetInstance" WHERE id IN ('probe-clock','probe-calc');

  6. Ziehen: im Bearbeitungsmodus die Uhr an ihrer MITTE (nicht an der Kopfleiste) 200 px nach rechts ziehen -> sie rastet versetzt ein; Kopfleiste 20 px hoch mit sechs Griff-Punkten und Tooltip „Ziehen Sie die Kachel, um sie zu verschieben“; mousedown im Suchfeld + 100 px Bewegung -> Text markiert, Suchleiste bewegt sich NICHT; Klick auf das Loesch-Symbol rechts in der Kopfleiste -> Widget verschwindet, kein Ziehen; Groessen-Griff funktioniert weiter.

  7. Ablegen auf belegter Stelle: die Uhr ueber die Suchleiste ziehen und loslassen -> die Uhr steht wieder am Ausgangsort, die Suchleiste ist nicht verschoben, nichts ueberlappt; die Suchleiste in Richtung Uhr vergroessern -> die Groesse stoppt vor der Uhr. Beobachtetes Verhalten woertlich notieren.

  8. Durchsicht: jedes Widget bei Standard- und Mindestgroesse (gut / auffaellig / Todo) in die VERIFICATION uebernehmen.

Nach der Probe: Probe-Datensaetze entfernen, Playwright-Artefakte entfernen.

Fuer den Changelog

  • Widgets lassen sich wieder deutlich kleiner ziehen — jedes Widget hat jetzt genau die Mindestgroesse, bei der es gerade noch bedienbar ist; das gilt auch fuer bereits platzierte Widgets.
  • Der Bearbeiten-Schalter des Dashboards sitzt jetzt unten rechts; dadurch beginnen die Widgets direkt unter der Kopfzeile.
  • Verschieben ist einfacher: im Bearbeitungsmodus laesst sich jedes Widget an einer beliebigen Stelle anfassen (ausser an Eingabefeldern und Knoepfen), ein grauer Griff am oberen Rand zeigt das an. Widgets ueberlappen sich beim Ablegen nicht mehr — ueber einer belegten Stelle springt das Widget an seinen Ausgangspunkt zurueck.
  • Die Stoppuhr hat kompaktere Knoepfe und passt so auch in kleine Kacheln.
  • Das Anwenderhandbuch beschreibt den neuen Schalter, das Ziehen und die Mindestgroessen.

Self-Check: PASSED

Alle 12 Dateien vorhanden (10 Code/Handbuch-Pfade einzeln geprueft, de.json/en.json eingeschlossen), Commits dc992c9, dbbd54f, cf97b5b in git log --all; nach git fetch: ## main...origin/main (nicht voraus, nicht zurueck); git status --porcelain zeigt nur das ungetrackte SUMMARY (Akten-Commit durch den Orchestrator).