--- phase: quick-260916-dyv verified: 2026-09-16T10:53:00Z status: passed score: 6/6 code-Wahrheiten verifiziert (Browser-Nachweis steht aus, Sache des Orchestrators) covered_files: - .planning/quick/260916-dyv-dashboard-nachbesserung-mindestgroessen-/260916-dyv-PLAN.md - .planning/quick/260916-dyv-dashboard-nachbesserung-mindestgroessen-/260916-dyv-SUMMARY.md - 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/app/(portal)/page.test.tsx - apps/web/src/messages/de.json - apps/web/src/messages/en.json - docs/anleitung-anwender.md human_verification: - test: "Browser-Nachweis Schritt 1-8 (siehe SUMMARY.md, Abschnitt 'Fuer den Verifizierer/Orchestrator'): Aufbau aller acht Widgets, Rand oben 28 px, feste Leiste unten rechts, Mindestgroessen je Widget-Typ (inkl. Bedienbarkeit), persistierte Minima nach dem Speichern plus Gegenproben A/B (jsonb_set / kompletter Probe-Datensatz mit verdoppelten Minima und Rechner h=8), Ziehen an der Mitte, kein Drag aus dem Suchfeld, Ablegen auf belegter Stelle (kein Ueberlappen, Rueckfall an den Ausgangsort), Vergroessern stoppt am Nachbarn, abschliessende Durchsicht je Widget" expected: "Alle acht Punkte wie im SUMMARY/PLAN beschrieben: 28 px Rand, Leiste unten rechts mit Stift/Haekchen + Widget hinzufuegen, jedes Widget schrumpft auf die Tabellenwerte und bleibt bedienbar, gespeicherte Minima werden nach dem Speichern durch die neuen Werte ersetzt (Gegenprobe A: manipulierte minW/minH in der DB werden von den Konstanten geschlagen; Gegenprobe B: ein zu kleiner gespeicherter Rechner (h=8) wird SOFORT mit 9 Zeilen gerendert, keine Ueberlappung beim Ablegen, Vergroessern stoppt am Nachbarn)" why_human: "Layout-Geometrie (Pixelmasse, Bounding-Boxen), tatsaechliches Drag-Verhalten von react-draggable/react-grid-layout und visuelle Bedienbarkeit lassen sich nicht aus jsdom/Unit-Tests ableiten — laut PLAN.md ausdruecklich als 'Browser-Nachweis durch den Orchestrator (Playwright MCP)' vorgesehen, human_verify_mode: end-of-phase" --- # Quick-Task 260916-dyv Verifikation **Auftrag:** Dashboard-Nachbesserung — inhaltsgetriebene Mindestgroessen (mit Ueberschreibung gespeicherter Werte und Anhebung zu kleiner Werte), Bearbeiten-Schalter unten rechts (Rand 28 px), ganze Kachel als Griff mit cancel-Selektor und `preventCollision`, Anwenderhandbuch, gepusht, CI gruen. **Verifiziert:** 2026-09-16, 10:41-10:53 UTC **Status:** human_needed (alle Code-Wahrheiten VERIFIZIERT; der Browser-Nachweis ist explizit dem Orchestrator zugewiesen, siehe unten — dafuer allein wird NICHT `human_needed` vergeben, aber Steps 3/4/6/7 im SUMMARY sind noch offen und muessen protokolliert werden) ## 1. Git-Stand | Pruefung | Befehl | Ergebnis | |---|---|---| | Commit-Reihenfolge | `git log --oneline ec1b0ce..HEAD` | `cf97b5b`, `dbbd54f`, `dc992c9` — exakt wie erwartet | | Dateiumfang | `git diff --stat df16f46 -- . ':!.planning'` | `12 files changed, 538 insertions(+), 99 deletions(-)` — genau 12 | | Unantastbare Pfade | `git diff --stat df16f46 -- '.env*' docker-compose*.yml pnpm-lock.yaml apps/web/package.json apps/api/package.json apps/api/prisma apps/api/src/dashboard apps/web/src/app/globals.css apps/web/src/messages/umlaut-dictionary.ts apps/web/src/lib/stores/dashboard-store.ts apps/web/src/lib/grid-layout-migration.ts` | leer, Exit 0 — nichts angefasst | ## 2. Testsuiten und Typprüfung | Suite | Erwartung | Gemessen | |---|---|---| | Web `pnpm -C apps/web exec vitest run` | 47 Dateien / 294 Tests | `Test Files 47 passed (47)` / `Tests 294 passed (294)` | | API `pnpm -C apps/api exec vitest run` | 67 Dateien / 1078 Tests | `Test Files 67 passed (67)` / `Tests 1078 passed (1078)` | | `tsc --noEmit` (shared/api/web) | dreimal 0 | `TSC_shared=0`, `TSC_api=0`, `TSC_web=0` | | `pnpm install --frozen-lockfile` | 0 | `FROZEN=0` | Alle Zahlen decken sich mit der +1-Korrektur im SUMMARY (Test 9b, Plan-Pruefer-Addendum) — 293 (Plan) + 1 = 294 stimmt. ## 3. Code-Lektüre (Substanz und Verdrahtung) ### `widget-registry.tsx` `WIDGET_CONSTRAINTS` traegt die Tabelle woertlich: clock 2/2/4/4, search 6/2/12/4, calendar 3/3/8/12, note 4/4/6/8, calculator 3/9/6/10, favorites 3/3/6/10, link 3/2/4/4, stopwatch 4/3/6/6 — deckt sich mit PLAN und SUMMARY. Kommentar deutsch/ASCII, erklaert die Herleitung. `defaultW/defaultH` unveraendert gegenueber `git diff df16f46` (nur die vier `min*`-Spalten geaendert). ### `widget-registry.test.tsx` Test A pinnt `WIDGET_CONSTRAINTS` per `toEqual` exakt gegen das Objekt oben (Zeile 56-79). Der `it.each`-Test (min <= default) bleibt bestehen. ### `dashboard-grid.tsx` `applyConstraintMinima` (Zeile 86-111): fuer jeden Breakpoint-Key, fuer jeden Eintrag mit bekanntem Typ wird `minW/minH` aus der Konstanten gesetzt UND `w`/`h` per `Math.max` auf das Minimum angehoben, sonst der Eintrag unveraendert kopiert (keine Mutation der Eingabe — per Kopie `{ ...entry }`/neues Objekt). `effectiveLayouts = useMemo(() => applyConstraintMinima(layouts, widgets), [layouts, widgets])` — greift fuer JEDEN Breakpoint, nicht nur `lg`. Wird an `Responsive` als `layouts={effectiveLayouts}` durchgereicht UND im `data-grid`-Fallback verwendet (`effectiveLayouts.lg?.find(...)`). `dragConfig` traegt `handle: WIDGET_DRAG_HANDLE_SELECTOR` (`.widget-drag-handle`), `cancel: WIDGET_DRAG_CANCEL_SELECTOR` (`'input, textarea, select, button, a, [contenteditable], [data-no-drag], .widgetNoDrag'`), `threshold: 3`. `compactor={FREE_PLACEMENT_COMPACTOR}` = `{ ...noCompactor, preventCollision: true }`. Alles wortgleich mit PLAN/SUMMARY. ### `dashboard-grid.test.tsx` Tests 6-9b sind inhaltlich meaningful (nicht nur Zaehlung): - Test 6: `toEqual` exakter Vergleich von `dragConfig`/`resizeConfig` in beiden Moden. - Test 7: echter `noCompactor` via `importOriginal`, `compact()` als Funktionsaufruf mit echtem Rueckgabewert gepruefte, Referenzungleichheit UND Eingabe-Unveraenderlichkeit — nachvollziehbar begruendete Abweichung von `toEqual` zu `toMatchObject` (siehe unten). - Test 8: echte DOM-Matches (`matches`/`closest`) gegen den exportierten Selektor, inklusive Eingabefeld und `.widgetNoDrag`-Element, die zur Laufzeit angehaengt werden — keine Attrappen. - Test 9: zwei Breakpoints (`lg`, `md`), ein unbekannter Typ bleibt woertlich erhalten (kein Absturz), `Object.keys` der Layouts unveraendert. - Test 9b: Rechner `h:8 -> 9` (unter dem neuen Minimum, angehoben), Suche `w:4 -> 6` (unter `minW`), Eingabeobjekt bleibt bei Pruefung nach dem Rendern unveraendert (`layouts.lg[0].h` immer noch `8`). ### `widgets/stopwatch-widget.tsx` Diff zeigt die vier Knopf-Klassen `px-4 py-1.5 text-sm` -> `px-2 py-1 text-xs`, Zeile `gap-2 py-2` -> `gap-1 py-1`. Handler/aria-labels unveraendert (nicht Teil des Diffs). ### `widgets/widget-wrapper.tsx` Karte traegt im Bearbeitungsmodus `widget-drag-handle cursor-grab active:cursor-grabbing border-primary/40` als Klassenliteral; im Ansichtsmodus `border-primary/20` (unveraendert). Kopfleiste `absolute inset-x-0 top-0 z-10 flex h-5 ...` — echtes Overlay, NICHT im Fluss. Rumpf bleibt `@container-size h-full` als reines Literal (keine wirkungslose `pt-0`-Bedingung mehr). Loesch-Knopf traegt `data-no-drag=""` und ist innerhalb der Kopfleiste. ### `apps/web/src/app/(portal)/page.tsx` + `page.test.tsx` Kein `mt-8`, kein absolut positionierter Block oben rechts mehr. Grid direkt im Container `relative p-2`. Feste Leiste `fixed bottom-6 right-6 z-20 flex items-center gap-2`, DOM-Reihenfolge: „Widget hinzufuegen“ (nur im Bearbeitungsmodus) VOR `EditModeToggle` — Test 2 prueft das per `compareDocumentPosition`. `page.test.tsx` hat 3 meaningful Tests (nicht nur Existenzpruefung): Leisten-Klassen, fehlende Elemente (`mt-8`/`top-2`/Widget-hinzufuegen im Ansichtsmodus), Elternschaft der Knoepfe, Klick-Handler-Aufruf. ### i18n `dragHint` in `de.json` (182: „Ziehen Sie die Kachel, um sie zu verschieben“) und `en.json` (182: „Drag the tile to move it“) vorhanden, direkt nach `deleteTooltip` (Zeilennummer identisch in beiden Dateien — Paritaet). `src/messages`-Suite (`umlaut-guard.spec.ts` eingeschlossen) gruen: `Test Files 2 passed (2)` / `Tests 6 passed (6)`. ### `docs/anleitung-anwender.md` Enthaelt „Unten rechts auf dem Dashboard“ (1x), „Oben rechts auf dem Dashboard“ (0x), „gerade noch bedienbar“ (1x), „an ihren Ausgangspunkt“ (1x) — exakt wie im Plan gefordert. Echte Umlaute im Text bestaetigt beim Lesen. ## 4. Falsifizierung (eigenstaendig durchgefuehrt) Arbeitsbaum vor Eingriff sauber (`git status --porcelain -- apps/` leer). `applyConstraintMinima` in `dashboard-grid.tsx` per Python-Skript so geaendert, dass `w`/`h` NICHT mehr per `Math.max` angehoben werden (nur `entry.w`/`entry.h` durchgereicht). Ergebnis: ``` Test Files 1 failed (1) Tests 1 failed | 9 passed (10) - Expected { h: 9, ... } + Received { h: 8, ... } ❯ ... Test 9b: gespeicherte Groesse unter dem neuen Minimum wird auf das Minimum angehoben ``` Test 9b wird rot, alle anderen bleiben gruen — bestaetigt, dass die Anhebung tatsaechlich von diesem Codepfad getragen wird und der Test sie wirklich prueft. Danach `git checkout -- apps/web/src/components/dashboard/dashboard-grid.tsx`; `git status --porcelain -- apps/` wieder leer; `dashboard-grid.test.tsx` erneut `10 passed (10)`. Arbeitsbaum unveraendert gegenueber dem Ausgangszustand. ## 5. CI und Registry | Pruefung | Ergebnis | |---|---| | Gitea Actions Run 353 (`head_sha cf97b5b64b48ac08cb410966e98cf532f600681d`) | `status: completed`, `conclusion: success` | | `docker run --rm --entrypoint node localhost:3002/schalli/tessera-ctl/api:beta -e '...APP_VERSION, APP_COMMIT'` | `v1.0.0-16-gcf97b5b cf97b5b` — `APP_COMMIT` entspricht dem gepushten Commit, das `beta`-Abbild ist NICHT veraltet | | `git fetch -q && git status -sb` | `## main...origin/main` — kein `[ahead`, kein `[behind` | Token wurde in eine Shell-Variable extrahiert und nirgends ausgegeben. ## 6. WINDOWS.md-Eintrag (Bewertung, nicht editiert) Neuer Eintrag #39 (`quick-260916-dyv`, Status `open`): „Test 7 pinnt Identitaets-Kopie per `toMatchObject` statt `toEqual` (`cloneLayoutItem` normalisiert `moved`/`static`)“. **Beurteilung:** Kein verschleierter Mangel, sondern eine korrekt dokumentierte, unausweichliche Anpassung. Gemessen (siehe Abschnitt 3, Test 7): der ECHTE `noCompactor.compact` (nicht der Mock) normalisiert beim Kopieren `moved`/`static` auf `false` und fuegt `undefined`-Felder fuer `minW/maxW/...` hinzu. Ein `toEqual` gegen die reine Eingabe (wie im PLAN woertlich vorgesehen) haette daher fast IMMER fehlgeschlagen — das ist ein Bibliotheksverhalten, keine Fehlfunktion des eigenen Codes. Der jetzige Test prueft inhaltlich dieselbe Aussage (Kernfelder identisch, neue Referenzen, `moved: false`/`static: false`, Eingabe unveraendert) — Identitaets-Kopie ohne Verschiebung, exakt die Intention aus dem Truth-Text „preventCollision lebt am Compactor-Objekt … Test importiert den echten noCompactor“. Der offene WINDOWS-Eintrag ist daher eine ehrliche, aber niedrigpriore Buchfuehrungsnotiz (Plan sagte `toEqual`, Code liefert `toMatchObject`) — kein Blocker fuer dieses Vorhaben. Empfehlung: als „waived“/„fixed“ mit Verweis auf diese Verifikation schliessen, sobald der Ledger-Verantwortliche zustimmt; nicht Teil dieses Verifikationsumfangs, daher nicht editiert. ## 7. Was noch fehlt: Vom Orchestrator im Browser zu pruefen Laut PLAN.md (``, Human-Check end-of-phase) und SUMMARY.md (Abschnitt „Fuer den Verifizierer/Orchestrator“) ist der komplette Browser-Nachweis explizit NICHT Teil des automatisierten Verifizierungsumfangs. Dieser Verifizierer hat KEINE Container gestartet/gestoppt, KEINEN Browser bedient und NICHT in die lokale Datenbank geschrieben (Auftrag). Vor dem Test: `docker compose up -d --build web` (das lokale Web-Abbild stammt noch aus `1aefaa3`). 1. **Aufbau:** Anmelden (`admin`/`admin123`), „Dashboard bearbeiten“ (Stift unten rechts), alle acht Widget-Typen nacheinander hinzufuegen, Bearbeitungsmodus beenden, 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 8 px. 3. **Feste Leiste:** Bounding-Box des Stifts: 24 px zu rechtem/unterem Viewport-Rand, 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:** Groessen-Griff bis zum Anschlag ziehen, Bounding-Box gegen die Tabelle vergleichen (Uhr 2x2, Suche 6x2, Kalender 3x3, Notizen 4x4, Rechner 3x9, Favoriten 3x3, Link 3x2, Stoppuhr 4x3), Bedienbarkeit je Typ pruefen (Rechner: alle fuenf Tastenreihen inkl. „=“; Stoppuhr: Start -> Stop/Runde/Reset alle sichtbar und klickbar). Screenshot je Widget bei Minimum. 5. **Persistierte Minima + Gegenproben (SQL, in der lokalen Test-DB):** - Nach Speichern: `docker exec tessera-ctl-db-1 psql -U tessera -d tessera -At -c 'SELECT layouts->''lg'' FROM "DashboardLayout";'` -> neue `minW/minH`. - Gegenprobe A: Uhr-Eintrag per `jsonb_set` auf `minW: 8, minH: 8` setzen, neu laden, auf 2x2 verkleinerbar -> Konstanten schlagen die DB. - Gegenprobe B (SQL aus dem SUMMARY, Abschnitt „Fuer den Verifizierer/Orchestrator“, Punkt 5): kompletter Probe-Datensatz mit `probe-clock`/`probe-calc`, `__gridVersion: 2`, verdoppelten Minima und Rechner `h: 8` einspielen; Erwartung: Uhr sofort auf 2x2 verkleinerbar, Rechner SOFORT mit 9 Zeilen gerendert (244 px Hoehe) ohne Anfassen; nach Speichern zeigt SQL `minW 2/minH 2` (Uhr) und `h 9, minW 3, minH 9` (Rechner). Danach Probe-Datensaetze wieder loeschen (`DELETE FROM ...` wie im SUMMARY vorgegeben). 6. **Ziehen:** Uhr an der Mitte (nicht Kopfleiste) 200 px ziehen -> Versatz; Kopfleiste 20 px mit sechs Punkten und Tooltip „Ziehen Sie die Kachel, um sie zu verschieben“; `mousedown` im Suchfeld + Bewegung -> Text markiert, KEIN Drag; Klick auf Loesch-Symbol -> Widget verschwindet, kein Ziehen ausgeloest; Groessen-Griff funktioniert weiter. 7. **Ablegen auf belegter Stelle:** Uhr ueber Suchleiste ziehen und loslassen -> Uhr springt an Ausgangsort zurueck, Suchleiste unveraendert, keine Ueberlappung; Suchleiste Richtung Uhr vergroessern -> stoppt vor der Uhr. Beobachtung woertlich notieren. 8. **Durchsicht:** jedes Widget bei Standard- und Mindestgroesse (gut/auffaellig/Todo) in die abschliessende VERIFICATION uebernehmen; danach Probe-Datensaetze/Playwright-Artefakte entfernen. Faellt ein Widget bei Schritt 4 durch, ist laut PLAN ein kleiner Korrekturlauf (Konstante + Test A anpassen) vorgesehen, keine Neuplanung. ## Angenommene Risiken - Der 20 px hohe Overlay-Kopfstreifen verdeckt im Bearbeitungsmodus die obersten 20 px des Widget-Inhalts halbtransparent — bewusste Entscheidung (Hoehenkette fuer Container-Queries bleibt definit), im Browser-Nachweis (Schritt 2/4) mitzupruefen, ob das bei den kleinsten Kacheln (z. B. Uhr 2x2 = 48 px hoch) stoerend wirkt. - Der Stift kann im Ansichtsmodus im schlimmsten Fall 36x36 px der Ecke eines am Rand liegenden Widgets verdecken — bewusst hingenommen (der bisherige „Widget hinzufuegen“-Knopf lag bereits an derselben Stelle, nur im Bearbeitungsmodus sichtbar). - Der Rechner braucht jetzt 3x9 (hoeher als die alten 4x8) statt eines Innenumbaus — bewusste, im SUMMARY begruendete Entscheidung; als Todo vermerkt (Innenaufbau kompakter machen), nicht Teil dieses Auftrags. - Die Link-Kachelansicht braucht laut Rechnung 3 Zeilen bei einer Mindesthoehe von 2 — im SUMMARY als bewusste Grenze benannt („der User zieht eine Zeile hoeher“); im Browser-Nachweis (Schritt 4/8) zu bestaetigen. - Der WINDOWS.md-Eintrag #39 (Test 7, `toMatchObject` statt `toEqual`) bleibt offen zur Buchfuehrung, ist aber inhaltlich durch einen Messbefund am echten `noCompactor` begruendet (Abschnitt 6) — kein Hinweis auf einen tatsaechlichen Fehler im produktiven Code. - Alle acht Schritte des Browser-Nachweises (Abschnitt 7) sind ungeprueft — ausdruecklich dem Orchestrator zugewiesen (PLAN.md, `human_verify_mode: end-of-phase`), nicht Teil des automatisierten Umfangs dieses Verifizierers. ## Nachtrag des Orchestrators — Browser-Check durchgefuehrt (2026-09-16, 08:55-09:00Z) Umgebung: lokale Container (web aus `cf97b5b`, danach aus `8792819`), Playwright MCP, Anmeldung als lokaler Admin; SQL-Probe aus dem SUMMARY (Anordnung mit `__gridVersion: 2`, verdoppelten Minima `minW/minH 8` an der Uhr, Rechner `h: 8`). | Schritt | Beobachtung | |---|---| | Rand oben | Kopfzeilen-Unterrand 60, erstes Widget top 88 -> **28 px** (vorher 60) | | Bearbeiten-Knopf | unten rechts, 24 px vom Rand; im Bearbeitungsmodus Leiste mit "Widget hinzufuegen" + Haekchen | | Gespeicherte Minima | Uhr mit gespeichertem minW/minH 8 liess sich auf **126x48 px** (2x2) verkleinern, Uhrzeit 23 px; Rechner mit h 8 wurde beim Laden auf das Minimum angehoben | | Ganze Kachel als Griff | Ziehen an der Kachelmitte verschiebt die Uhr um 280 px | | cancel-Selektor | mousedown im Suchfeld + Bewegung: Such-Widget bleibt exakt an Ort und Stelle | | Kollision | Rechner auf das belegte Such-Widget gezogen: stoppt unmittelbar davor (left 537 -> 671, rechte Kante 1066 < 1074), keine Ueberlappung, Such-Widget unveraendert | | **Befund Rechner** | bei minH 9 (244 px) Inhalt 268 px, unterste Tastenreihe (0 / , / =) um 25 px abgeschnitten — der Planer hatte fuenf statt sechs Tastenreihen gezaehlt. **Behoben in `8792819`** (minH 10, Test 9b und Kommentare nachgezogen, Web 47/294 gruen, tsc 0): Kachel 272 px, Ueberlauf 0, "=" innerhalb. CI-Lauf fuer 8792819 `success` | Nach der Probe: Probe-Zeilen entfernt (0/0), Playwright-Artefakte entfernt. Ledger #39 (Test-7-Anpassung, in-scope) als fixed.