# FamilyDashboard moving status/todo timeline

Session learning from the Home/Todo timeline redesign.

## Trigger
Use this when the user asks to improve FamilyDashboard child status/todo timelines, especially readability of `timelineSteps` in `frontend/src/components/TimelineHeader.tsx` and `frontend/src/styles/app.css`.

## Current preferred design
- Keep the horizontal timeline.
- For **Heute**, do **not** use a static all-day bar with a left-to-right progress fill.
- Use a moving viewport instead:
  - fixed now cursor at ~24% from the left (`NOW_PIN_PERCENT = 24`)
  - blocks positioned relative to a viewport around current local time
  - little past, more future
  - calm background; no shimmer/loading sheen
  - fade left/right edges
- For non-today dates, do **not** compress the day into 100% width. Use a horizontally scrollable pixel-based full-day axis so iPad users can swipe through the day.
- Prefer pixel scaling over percent scaling for both today and selected/future days:
  - `pxPerMinute` around 2.4–2.7
  - Full day width commonly ~1800–2300px for 06:30–20:15
  - selected/future days start scrolled near the first relevant morning step; no auto-follow
- For today, use the same pixel axis but scrollLeft so the now cursor appears at ~24% of the visible shell; allow manual swipe and resume auto-follow after ~5–8 seconds. Provide a `Jetzt` button.
- Today-focused tracks must fill the available iPad viewport. Compute `trackWidthPx = max(viewportWidth, visibleWindowMinutes * pxPerMinute)` and, if needed, scale `pxPerMinute` up so a small phase window (especially evening `17:00–20:15/20:30`) does not look artificially narrow. Full-day/selected-day mode may remain wider than the screen and scroll.
- Morning mode needs extra compression resistance: focus the viewport on `now - ~25 min` through Loslaufen / Schulbeginn + ~20 min rather than including the first early morning step. Including sleep/early wake anchors can waste iPad width and make breakfast/teeth unreadable.
- For short important morning ranges, add a visual layout pass after proportional positioning: keep chronological order, apply semantic minimum widths, then push later blocks right instead of letting important blocks collapse into dots or overlap.
- Morning school departures should be modeled as **two separate concepts**:
  - `leave_home` is a real range block from departure time to the first school block start (`endTime = school_start`), not a point marker. Render it like a short activity with `🚶`, broad label `Loslaufen`, compact `Los`, and a 56–72px minimum.
  - `school_start` is a separate milestone at the first scheduled school block start with `🏫` and label `Schulbeginn`. Do not let it disappear into the walking block; keep it visually distinct and positioned by the timetable.
  - **Important anchoring pitfall:** if visual layout applies minimum widths and pushes blocks, do not position `school_start` from raw `xForTime(step.time)` alone. Anchor it to the rendered `leave_home` visual range (`visualLeft + visualWidth`) so the marker moves with the schoolway block and does not drift independently as time/viewport changes.
  - Place the `school_start` label/icon above or just outside the right edge of `leave_home`; keep it small enough and low-z-index enough that it stays visible without covering `Zähne` or `Loslaufen`. If text would overlap, prefer icon-only (`🏫`) over a label pill.
  - Robust anchoring helper pattern: do not hard-code only `leave_home`. Use a function with the argument order `findDepartureRangeForSchoolStart(steps, visualRangeByKey, schoolStartStep)` so the visual range map is available before marker placement. It should accept semantic range keys/labels (`leave_home`, `leave_external`, `way_to_school`, `school_walk`, `Loslaufen`, `Schulweg`, generic `Weg`) and select the range whose `endTime` matches `schoolStartStep.time` exactly or within ~1–2 minutes. Use `schoolStartX = departureRange.left + departureRange.width`; if right-side label space is tight or would cover another block, force icon-only `🏫`.
- For semantic consistency, add stable timeline type classes derived from step keys (e.g. `timeline-kind-wake`, `timeline-kind-school_start`) rather than relying only on state/obligation. Use these classes to normalize same-type styling across children (e.g. wake blocks must not look thicker for one child unless their state truly differs) and to give special milestones their own style.
- When a visual layout pass stretches/shifts range blocks, compute the `Jetzt` cursor inside the **visual** current range, not just from raw minutes. Pattern: factor this into a helper such as `findCurrentRangeStep(steps, nowMinutes)` that applies `start <= now < end`; if multiple ranges contain `now`, choose the shortest/specific range (e.g. `Zähne` over a broad routine block); if it has a visual range, interpolate `visualLeft + ((now-start)/(end-start))*visualWidth`. Detail-card selection has a slightly different priority: a tapped/manual step may show briefly (~5s) so blocks remain demonstrably clickable, then return to the locally computed current step; for non-today dates, manual selection may persist. Avoid using only backend `state === current` for either cursor or detail card when local time makes the current block unambiguous.

## Frontend implementation pattern
Add/keep helpers in `TimelineHeader.tsx`:
- `minutesFromHHMM(value)`
- `buildTimelineScale(steps, mode, now)` / pixel-scale equivalent
- `xForTime(time, scale)` / pixel-position equivalent
- `getStepIcon(step)` with backend icon fallback mapping
- `getStepDisplayMode(step, widthPx)` using pixel width, not percent
- `getShortStepLabel(step)`

Viewport defaults:
- `nowPinPercent = 24`
- `visiblePastMinutes` by `dayPhase`:
  - `morning`: 35
  - `school_running`: 45
  - `lunch_break`: 25
  - `afternoon_school`: 45
  - `afternoon_free`: 35
  - `evening`: 35
  - default: 45
- `visibleFutureMinutes = visiblePastMinutes * ((100 - nowPinPercent) / nowPinPercent)`
- For `lunch_break`, ensure future >= 110 minutes so Loslaufen/school start remain visible.
- For `afternoon_free`, use future around 240 minutes so external activities and evening routine remain visible.

Position formula:
```ts
positionPercent = ((timeMinutes - viewportStart) / (viewportEnd - viewportStart)) * 100
```

## UX/label rules
- Short labels win; long details belong in the clickable detail panel.
- Render every relevant time window with an icon. Backend should provide `icon` where possible; frontend must fallback by key/label.
- Remove labels above the track; the track itself contains icons/short labels.
- Display mode should be based on available pixel width, with semantic thresholds so labels do not show as clipped fragments:
  - enough: icon + time + text
  - medium: icon + short label
  - narrow: icon only
  - tiny: neutral mini-dot only for unimportant spacers; important events still get an icon
  - food (`Frühstück`/`Morgenessen`, `Mittagessen`, `Nachtessen`, `Essen`) should fall back to `🍽️` before any `Essen` text can clip; for breakfast prefer broad `🍽️ Frühstück`, medium `🍽️ Essen`, narrow `🍽️`, never a black/neutral dot. In morning timelines, lower the full-label threshold for breakfast enough that 15–25 minute blocks can show `Frühstück` on iPad; do not let one child show `Frühstück` while another falls back to generic `Essen` when both have adequate width.
  - `Loslaufen` should be a range when travel time is known (`leave_home` / `leave_external` with `endTime`) and only a marker when no target time exists. Broad: `🚶 Loslaufen`; medium: `🚶 Los`; narrow: `🚶`.
  - `Schulbeginn` should be a distinct timetable milestone (`school_start`) with `🏫`, preferably rendered with a label pill so children understand the difference between leaving and class starting.
  - teeth/shoes/preparation should normalize to `🪥🚽` first; when shoes are relevant and width allows, use `🪥🚽 👟`. Do this semantic override before honoring older backend icons such as `🪥👟`; never show only WC and never degrade this block to a black/neutral dot.
  - school/ways/free should fall back to `🏫`/`🚶`/`🏠`/`🌿` rather than black dots
- Important events that should not degrade to black dots: `wake`, `leave_home`/`leave_external`, school blocks, breakfast/lunch/dinner, teeth/shoes/packing, external activities, sleep.
- Semantic minimum visual widths in morning timelines should be applied before display-mode selection: Aufstehen ~72px, breakfast/food ~90px, teeth/shoes/packing ~80px, leave markers ~56px icon, school ~120px, current free ~120px, ritual/sleep ~80px, external activities ~90px. If proportional width is smaller, use the min width and shrink spacer gaps / shift neighbours while preserving order.
- Use a neutral light mini-dot for unimportant spacers; avoid black dots as primary representation.
- Use:
  - `Ablaufen` → `Loslaufen` in UI/backend labels
  - `Zähneputzen / Schuhe anziehen` → `Zähne`
  - `Ankunft zuhause` → `Heim`
  - `Mittagspause` → `Mittag`
  - `Hausaufgaben` → `HA`, but hide homework from status timeline if it is not required there
  - `Abendritual` → `Ritual`

## Status copy preferences
- During school, show a prominent status like `Schule läuft bis HH:MM`.
- During lunch, show `Loslaufen in X Min.`.
- During free afternoon, show `Nachmittag frei`.

## Refresh pattern
- Prefer robust simple refresh:
  - Dashboard query refetch every 30–60 seconds.
  - A small frontend tick state every minute is fine for moving the viewport without backend changes.

## Backend timeline normalization
- Add/keep a backend normalization pass such as `merge_adjacent_timeline_ranges(steps)` before returning `timelineSteps`:
  - merge directly adjacent ranges when `previous.endTime == next.time`
  - merge semantic free groups (`Frei`, `Freizeit`, `free_*`) into one long block
  - preserve the first block's label/shortLabel/icon/detail
  - do **not** merge semantically distinct blocks: school vs external activity, external vs free, teeth/shoes vs leave, dinner vs ritual
- After merging, re-run current-state marking for today so a merged long block still receives `state: current`.

## Current-block readability pattern
- Long current ranges can extend outside the visible scroll shell. Keep the block itself proportional, but ensure the current label stays visible only when it can fit **inside the visible portion of its own block**.
- Preferred explicit overlay helper for `Schule` and `Frei` only:
  - detect in `TimelineHeader.tsx`: `isToday`, `step.state === 'current'`, range has `time` + `endTime`, duration >= ~60 min, semantic key/label/shortLabel includes `Schule` or `Frei`/`free`
  - track `scrollLeft` from `.timeline-scroll-shell` `onScroll` and measure shell `clientWidth` (ResizeObserver is fine)
  - compute `blockLeftPx`, `blockRightPx`, `viewportLeftPx`, `viewportRightPx`, `visibleBlockLeft = max(blockLeftPx, viewportLeftPx)`, `visibleBlockRight = min(blockRightPx, viewportRightPx)`, and `stickyMaxWidth = visibleBlockRight - (visibleBlockLeft + 12px) - 12px`
  - render only if min-width fits inside `stickyMaxWidth`: `🌿 Frei` >= ~90px, `🌿 Frei bis HH:MM` >= ~160px, `🏫 Schule` >= ~110px, `🏫 Schule bis HH:MM` >= ~180px
  - set `style.left = visibleBlockLeft + 12px` and `style.maxWidth = stickyMaxWidth`; use `overflow: hidden; text-overflow: clip; white-space: nowrap; pointer-events: none`
  - if the visible portion is too small, render no sticky label rather than clipping or overlapping the next block
  - do **not** use `overflow: visible` on the normal `.timeline-range.current` just to reveal labels; that can cover following blocks
  - do **not** apply this overlay to short events: teeth/shoes, leave, way home, external lessons, dinner, ritual, sleep
- Remove duplicate status text under child names in the timeline card; show only avatar + enlarged child name because the large countdown/status card already carries the phase. Use a compact header structure (`.child-status-header` with identity left and status hero right) so iPad Pro width is used instead of a full-width status slab.

## Verification checklist
- Backend tests remain green, especially `backend/tests/test_dashboard_lunch_modes.py` and dashboard tests covering morning timeline shape.
- Add/keep regression coverage for `leave_home.endTime == school_start.time`, `leave_home.kind == 'range'`, `school_start.kind == 'milestone'`, and `current` detection at a boundary such as 07:05 where breakfast must be `past` and teeth/shoes must be `current`.
- Frontend typecheck/build via container if host has no node:
  - `docker compose exec -T frontend npm run build`
- Deploy without sudo using:
  - `/home/agent/bin/rebuild-familydashboard`
  - or `cd /home/agent/projects/FamilyDashboard && docker compose up -d --build`
- Verify:
  - `curl -fsS http://localhost:8000/api/health`
  - `curl -I http://localhost:5173`
  - live source contains `timeline-scroll-shell`, `timeline-now-button`, `timeline-step-content`, and sticky CSS for current labels
- Playwright container fix:
  - in `frontend/Dockerfile`, after `npm ci`/`npm install`, ensure exactly one `RUN npx playwright install --with-deps chromium`
  - ensure `frontend/playwright.config.ts` default `baseURL` matches the actual dev server port used inside the container (currently `http://localhost:5173`), while still allowing `FAMILYDASHBOARD_E2E_BASE_URL` override
  - verify Chromium exists with `docker compose exec -T frontend sh -lc 'npx playwright --version; find /root/.cache/ms-playwright -maxdepth 3 \( -name chrome-headless-shell -o -name chromium \) -print | head'`
  - run `docker compose exec -T frontend sh -lc 'npm run test:e2e'`; if it fails, distinguish container/browser setup failures from actual UI assertions
