# Local family dashboard schedule / timetable module pattern

Use when a local family dashboard/PWA adds a child-facing weekly timetable plus PIN-protected admin editing, and the timetable must drive morning/timeline logic.

## Scope discipline

- Keep the child-facing timetable read-only. All writes stay behind the existing Admin PIN/session guard.
- Do not build OCR/image-recognition as a product feature. If a timetable photo exists, treat it as visual/reference input only; encode seed data manually and document uncertainty in `docs/seed-notes.md`.
- Prefer a touch-stable MVP editor (forms, large selects, confirmation before delete) over fragile drag/resize on iPad. Drag/resize can be a later iteration.

## Backend pattern

1. Add/confirm structured tables:
   - `ScheduleTimeSlot`: `label`, `start_time`, `end_time`, `sort_order`, `is_lunch_break`, `active`.
   - `ScheduleDay`: `child_id`, ISO weekday `1=Mon..7=Sun`, `has_school`, optional `school_start/school_end`, notes, validity range.
   - `ScheduleBlock`: `child_id`, `weekday`, `title`, `starts_at/ends_at`, `block_type`, optional subject/location/color/notes, `counts_as_school`, `requires_sports_gear`, `affects_timeline`, `active`.
2. If upgrading an existing SQLite app, add a small idempotent startup migration for newly-added nullable/default columns before seeding. `SQLModel.metadata.create_all()` will not alter existing tables.
3. Use ISO weekdays consistently (`date.isoweekday()`); avoid mixing Python `weekday()` (`0=Mon`) with persisted schedule weekdays (`1=Mon`).
4. Add serializers that include child display metadata, weekday labels, and normalized `HH:MM` time strings so the frontend does not do N+1 lookups.
5. Implement public endpoints:
   - `GET /api/schedule/current` → weekdays, children, time slots, days, blocks.
   - `GET /api/schedule/summary?date=YYYY-MM-DD` → per-child school start/end, leave time, afternoon-free, sports-gear flag, today blocks, external activities, next block.
6. Implement Admin endpoints under the existing PIN/session dependency:
   - `GET /api/admin/schedule`
   - `GET/POST/PATCH /api/admin/schedule/time-slots`
   - `POST/PATCH/DELETE /api/admin/schedule/blocks`
   - optional `POST /api/admin/schedule/seed-from-reference` that replays manual seed data, not OCR.
7. Enforce invariants:
   - `end_time > start_time`.
   - `Turnen` and `block_type=sports` imply `requires_sports_gear=true`.
   - `counts_as_school=true` controls school start/end; external/music/sports activities do not count unless explicitly marked.
   - Detect per-child/day overlaps and return a visible warning unless `allow_overlap=true` is intentionally passed.

## Dashboard/timeline integration

- School start = first active block for that child/day with `counts_as_school=true`.
- School end = last active block for that child/day with `counts_as_school=true`.
- Leave-home time = school start minus the child's commute minutes. For this app the default commute is 15 minutes; keep it in `ChildSettings` rather than hard-coding Emilia/Valerie rules.
- Add a morning milestone like `Turnzeug packen` when any today block requires sports gear.
- Split the day into explicit phases instead of one morning/evening bar: `morning`, `school_running`, `lunch_break`, `afternoon_school`, `afternoon_free`, `evening`, `weekend`, `special_day`.
- For lunch/afternoon logic, derive per child from schedule blocks: last morning school block before noon, home-arrival = morning end + commute, first afternoon school block after noon, afternoon leave-home = afternoon start - commute, and next external activity after noon.
- Never label a day generically as `Schulfrei` just because afternoon school is absent. Prefer `Nachmittag frei`, `Heute keine Schule mehr`, and optional `Nächster Termin: <title> HH:MM` for external activities.
- After leave-home time has passed, do not show only `Loslaufen vorbei`; choose a useful state: `Jetzt loslaufen` before school start, then `Schule läuft bis HH:MM` once school has begun.
- External activities should appear as timeline/day hints but must not extend school end by default.
- Gracefully fallback when no structured timetable exists: log a backend warning and show a free/special-day fallback rather than crashing.

## Frontend pattern

- Add a left-nav item and route/state path for `/schedule`.
- Build the read-only grid as: time axis left, Monday–Friday day columns, and two child subcolumns per day. Lunch break is a horizontal neutral separator.
- Use child-first styling: e.g. Emilia pink/magenta, Valerie green. Do not let external/music/sports category colours override the child colour unless the user explicitly asks; category can be expressed with text/icons/metadata.
- Match the paper/original timetable information density: normal school blocks show only the subject/title (no duplicated start/end time inside the block); external activities such as guitar, piano, Mädchenriege show their special time and location inside the block.
- Keep the child-facing `/schedule` page focused on the timetable: hide unrelated day switchers, install hints, and dashboard/timeline/status bars unless the user explicitly wants them there.
- For the Home/Todo day-status area, use per-child status cards rather than a dense shared timeline: avatar/name/phase, one large countdown/status card, next-step card, school/free/appointment card, then a compact timeline using only short labels. Long labels belong in the cards above, not under the bar.
- Lunch mode UX must be calm and prominent: `Loslaufen in N Min.` when afternoon school exists; `Nachmittag frei` / `Heute keine Schule mehr` when not; `Jetzt Schuhe/Jache anziehen` near leave time; `Jetzt loslaufen` after leave time but before school start; `Schule läuft bis HH:MM` after school starts.
- For compact timeline labels, use short labels like `Auf`, `Anziehen`, `Essen`, `Zähne`, `Turnzeug`, `Heim`, `Pause`, `Packen`, `Los`, `Schule`, `Aufgaben`, `Spielen`, `Abend`. Avoid multi-word pills that overlap on iPad.
- Keep text large and touch-friendly. For a row-based MVP grid, **do not render long blocks in every overlapped time row**: that creates repeated cards and visual overpainting on iPad. Render each block once in its start/display row; keep the true `start_time–end_time` in the card text only when the block type warrants it. For non-standard start times that do not fall inside a defined slot (e.g. 12:50), map the card to the next sensible visible slot while preserving the real times.
- Clamp/wrap text intentionally: short title line(s), small time/location line only for external events/admin mode, fixed cell/card heights, `overflow: hidden`, `minmax(0, 1fr)` child columns, and a sufficiently wide time axis so labels like `Mittagspause` do not bleed into the grid.
- Admin editor can live inside the existing Admin portal: make the timetable grid itself the editor. Existing blocks should be directly tappable to open the edit/delete modal; empty slots should be tappable to open a prefilled create form (child, weekday, start/end from slot, default school block). Avoid relying only on a separate horizontal block list.
- Implementation pitfall: avoid nested interactive elements. If the whole empty cell is clickable, do not wrap existing block buttons in another clickable cell; only empty cells need the cell-level click handler. Child view cards can be non-button `<div>`s, while admin cards are buttons.
- After schedule mutations, invalidate both `schedule` and `admin schedule` queries plus dashboard/timeline data if affected.
- If the app has a service worker, bump the shell cache name after adding major routes/UI so iPad PWAs pick up the new bundle.

## Seed/reset pitfalls

- A manual “seed from reference photo” endpoint should be able to reset deterministically: deactivate existing schedule blocks before inserting the current reference set, then verify the public schedule has the expected count and no duplicate `(child, weekday, title, start, end)` tuples.
- Idempotent startup seeding must look for matching **active** blocks only. Otherwise an old inactive block can be reactivated on app/container restart after an admin reset, producing stale extra blocks that are not exact duplicates.
- When manually updating live timetable data through Admin APIs, run a duplicate/stale-active check afterward. Duplicate or stale active blocks can look like CSS/layout bugs because stacked cards create slivers and apparent overlap even after the frontend is fixed.

## Testing strategy

Backend tests should prove:

- `school_start` is calculated from the first `counts_as_school` block.
- `school_end` is calculated from the last `counts_as_school` block.
- Morning and afternoon school are separated correctly: last morning school block before noon, first afternoon school block after noon.
- `leave_home_time = school_start - commute_minutes`; for this dashboard default commute is 15 minutes from `ChildSettings`.
- Lunch mode at representative times: around 12:10 gives `lunch_break` with minutes until leave-home; around 13:10 is `urgent`; after leave-home but before school start is `late`/`Jetzt loslaufen`; after afternoon start is `afternoon_school`/`Schule läuft bis HH:MM`.
- A child without afternoon school returns `afternoon_free` and never a false leave-home warning.
- Turnen/sports blocks set `requires_sports_gear_today`.
- External activity does not count as school end or afternoon school, but appears as `nextExternalActivity` after noon.
- Afternoon-free detection works.
- Different children can have different day phases at the same clock time.
- Overlap detection returns a warning.
- Admin CRUD persists and deleted/deactivated blocks disappear from public `schedule/current`.

Frontend / smoke tests should cover:

- `/schedule` loads with the new nav item.
- The weekly grid contains child-specific blocks and lunch break.
- Home/Todo renders Lunch Break and Afternoon Free status cards using mocked `/api/dashboard` data.
- Countdown/status card has large text and the compact timeline uses short labels only; assert long labels like `Tasche / Schuhe` are not rendered inside `.compact-step`.
- Admin remains PIN-protected.
- Admin can add/edit/delete a block with touch-sized controls.
- Home/Todo morning timeline shows sports-gear and leave-time effects from schedule data.

## Reference-photo update workflow after the module already exists

When the user later provides a newer timetable photo or corrected schedule details, treat it as a data-update task, not a new feature build:

1. Update the manual reference seed structure in code from the photo/corrections. Keep coloured-but-unlabelled school cells as a neutral `Schule` block when needed so school start/end calculations remain correct.
2. Preserve the no-OCR rule: store any supplied/converted image only under `docs/reference/` for documentation, never as a runtime dependency.
3. Update `docs/seed-notes.md` with the day-by-day interpreted timetable and explicitly note any ambiguity.
4. Ensure the admin “seed from reference” endpoint can reset/inactivate old active blocks before inserting the new reference set; otherwise stale seed blocks can survive alongside the corrected timetable.
5. If the running local dashboard already has a persisted SQLite volume, do not assume a code seed change updates live data. Either run the reset/seed admin endpoint or apply the structured changes through existing PIN-protected admin CRUD, then verify `/api/schedule/current` and one or two `/api/schedule/summary?date=...` cases.
6. Run the normal backend tests plus frontend build/smoke. Verify at least one external activity remains `counts_as_school=false` and at least one sports/Turnen day sets `requires_sports_gear_today=true`.

## Docs

- Add a README section with API and admin editing instructions.
- Add `docs/seed-notes.md` for manual reference-photo interpretation and uncertain seed cells.
- If keeping a converted reference image, place it under `docs/reference/` and make clear it is documentation only, not a runtime dependency.
