# Next Steps — handoff, 2026-09-13 Written at the end of a long session so a fresh session can pick up without re-deriving anything. **Read `CLAUDE.md` and `ARCHITECTURE.md` first — both were brought up to date today and are trustworthy again.** `PLANNING_ENGINE_SPEC.md` holds the approved design of the planning engine, including decisions the product owner already made (don't relitigate them — see §6). **Addendum:** the four product-owner todos from `todo_260912.txt` are now folded into §3 (marked **NEW**), and the product owner's answers to the clarifying questions are recorded inline (2026-09-13). Only two small sub-points remain open, in §3.11. `todo_260912.txt` can be deleted once the todos land. --- ## 1. Where the app is The core user journey works end to end: > log in → set up child, vacation days per year, planning goal → describe breaks as > *"6 weeks every summer, 4 weeks every Christmas"* → get a costed multi-year plan showing which > days are already free, which cost a semesterdag, which cost a föräldrapenningdag → save it to > the calendar. Built today: the whole planning engine (`LeaveWish` → allocator → propose → apply), a Swedish calendar cache with user overrides, the Planering tab, bilingual sv/en UI, and all five items from the old `todo.txt` (which is now done and can be deleted). **State at handoff** - Backend: **361 tests passing**, `ruff check` clean, `ruff format --check` clean - Frontend: **540 tests**, coverage exits 0 (89.47% stmts / 85.46% branches / 88.27% funcs) - Three verification issues outstanding — see §4.1, none of them from feature work --- ## 2. Running it ```bash cd frontend && npm run dev # starts BOTH vite (5173) and uvicorn (8000) ``` If the servers die (they did today), restart individually: ```bash cd backend && uv run uvicorn app.main:app --reload --port 8000 cd frontend && npx vite ``` **Gotcha that cost an hour:** a stray `python -m http.server 8000` was squatting `*:8000` over IPv6. macOS resolves `localhost` → `::1` first, so Vite's `/api` proxy hit the squatter and every login returned `501`. `frontend/vite.config.ts` now targets `127.0.0.1` explicitly so this can't recur. If the API behaves impossibly, check `lsof -nP -iTCP:8000 -sTCP:LISTEN` for extra listeners. --- ## 3. What's left — merged roadmap (original items + product-owner todos) Phases, roughly in order: - **Phase A (3.1–3.3, 3.5): finish what the backend already computes.** `surplus_advice` and `is_assumed` already reach the wire; wiring them up is high-value, low-risk. 3.5 (the i18n `message_key` fix) must land **before** 3.9, because flexible wishes will generate new engine-produced Swedish strings — build on the `tips_service.py` pattern from day one instead of creating more leaks to retrofit. - **Phase B (3.4, 3.6–3.8): the calendar cluster.** All four touch `Calendar.tsx`, the holiday cache or Settings — one pass over the holiday data, not four. - **Phase C (3.9): flexible wishes** — the only item needing engine **and** spec changes. The product-owner decisions are in (minimise paid days, re-pick yearly, propose like every other plan; two small open points in 3.11); do it as a dedicated session after the UI foundations. - **Phase D (3.10): page coverage** — interleaved whenever, don't batch it. ### 3.1 Show the surplus advice in the UI ← highest value The backend computes it and `POST /api/plan/propose` now returns `surplus_advice`, but **`PlanResults.tsx` doesn't render it**. Right now the app tells a family *"you'll lose 195 days"* and stops. The advice that exists and isn't shown: - `wish_more_leave` — "ni har råd med ytterligare N veckor — lägg till en vecka på sommaren eller ett höstlov" - `shift_mix_to_fp` — spend föräldradagar rather than semester, since FP is what expires - `front_load_early_years` — take more days while the child is small - `use_less_vacation` — bounded by `min_vacation_days_per_year` (statutory 20) Render it like the existing shortfall remedies (same shape, deliberately). This is the difference between a calculator and a planner. ### 3.2 Surface `is_assumed` on vacation years `VacationBalanceResponse.is_assumed` means *"no vacation_days row existed for this year, so we assumed the statutory 25"*. It reaches the wire now but nothing displays it, so the app silently plans around a guess. Prompt the user to confirm those years. ### 3.3 **NEW (todo):** show age-4 expiries *given the plan* Request: the "Förfaller vid 4 års ålder" stat is nice, but the app should also show how many days expire **assuming the plan actually gets taken** — while acknowledging that life may not work out that way. **Decision (product owner):** two surfaces, both plan-aware: 1. **Dashboard** — show how much is left given the *currently committed* plan. The existing stat already does this: `analytics_service.py` computes `expiring_at_b4 = max(projected_left − 96, 0)` with `projected_used = used + planned(date < B4)`, and the Dashboard renders it (`dashboard.expiringAtB4`). Verify it, don't rebuild it. 2. **Plan tab, propose time** — before committing, show how much would be left *if this plan were applied*. This is the actual work item: run the same projection for the proposed allocation and render it in `PlanResults.tsx` next to 3.1's surplus advice. Same component pass, so do it right after 3.1. ### 3.4 Shade röda dagar / klämdagar in the Kalender page The data exists (`backend/app/data/swedish_calendar.json`, exposed via `app/utils/holidays.py`) but `Calendar.tsx` doesn't use it. Users can't see which days are already free — arguably the most obvious missing visual in the app. ### 3.5 Backend-generated Swedish leaks through in English mode Four surfaces in `PlanResults.tsx` render Swedish regardless of locale, all produced by `backend/app/services/`: 1. `outcome.explanation` (per-window explanation) 2. `remedy.description` (shortfall remedies) 3. `warning.message` (warning body; the *label* is translated) 4. `applyResult.rejections` (from `planning_service.py`) **Do not fix this by string-matching client-side.** The right fix is the pattern `tips_service.py` already uses: return a `message_key` plus a `data` dict and let the frontend render it. Tips are already bilingual this way — copy that. ### 3.6 Day-override UI is settings-only You can add/remove overrides in Inställningar, but clicking a day in the Kalender to mark it worked/off would be far more natural. Pairs well with 3.4. ### 3.7 **NEW (todo):** month/year fast navigation in Kalender Clicking the month/year should let you jump faster than the arrows. Interpreted as: click month name = next month, click year = next year (backwards via the existing arrows or modifier click). Small, independent change to `pages/Calendar.tsx` — do it in the Phase B pass. ### 3.8 **NEW (todo):** red-dags list in Settings A per-year list of all röda dagar (i.e. the non-Sunday holidays) so it's at a glance which days the app treats as free. The data already exists — `backend/app/data/swedish_calendar.json` exposed via `app/utils/holidays.py` — same source as 3.4, so batch the two. Expose it through a proper API endpoint (not a file fetch from the frontend), and reuse the existing day metadata (name, type, whether it's a klämdag relative to a weekend). **Decision (product owner):** the list must show **both** the statutory red days **and** the user's day overrides (worked/off), and it paginates **four years ahead at a time** (the full 10-year cache is available but not displayed at once). ### 3.9 **NEW (todo), big:** "när det passar bäst" wishes with red-day-dense proposals A wish like "en vecka på våren" should be expressible as either **explicit dates** or **"whenever it fits best"** — in which case the engine picks the window, preferring places where there are the most röda dagar/klämdagar (e.g. around Easter) because that costs the fewest semesterdagar. Today `LeaveWish` is always a fixed month/day window (`backend/app/entities/leave_wish.py`). One-off explicit windows are already expressible via `recurrence='once'`, but *engine picks the window* doesn't exist. What's involved: - **Spec first** — extend `PLANNING_ENGINE_SPEC.md` with a new window mode (e.g. `window_mode: 'fixed' | 'flexible'`) and its allocation semantics. §6's decisions stay untouched; this is an addition. Don't start implementing before the spec addition is agreed. - `LeaveWish` entity + hand-written Alembic migration — and `_FULL_SCHEMA` in `app/db/connection.py` (gotcha from §4.2: forgetting it = 500s with no error log). - New allocation logic in `allocation_service.py` — keep the `Allocator` pure (no DB, no `date.today()`, §6). - UI in `WishEditor.tsx`: explicit window vs "när det passar bäst" toggle. - Every new user-facing string must use the 3.5 `message_key` + `data` pattern from day one — that's why 3.5 lands before this item. - **Decision (product owner):** "best" = **fewest paid days to fulfil the wish** — i.e. the engine minimises the semester- + parental-leave days that must be spent, using the red-day map (already known to the planner) to find windows where most of the week is free. Easter moves each year, so a yearly flexible wish re-picks **every year** (e.g. a spring week lands on the Easter-adjacent window of that specific year). The engine's output is a **suggestion** in the existing propose→apply flow — see the residual open points in 3.11. ### 3.10 Test coverage on pages `Auth.tsx`, `Tips.tsx` and `DevFetch.tsx` were at 0% (Auth and Tips got tests during the i18n work). Coverage thresholds are a **regression ratchet, not a target** — see §6. ### 3.11 Residual open points — small, confirm before 3.9 implementation The main product-owner questions were answered on 2026-09-13 (recorded inline in 3.3, 3.8 and 3.9). Two sub-points of 3.9 remain unresolved — neither blocks starting the spec draft: 1. **Apply vs propose.** Interpreted as *propose*: the engine picks the window, the family sees the concrete dates in the Plan tab and confirms, like every other plan. Flag if the wish was auto-apply. 2. **Conflicts.** Interpreted as *fixed wishes always win*: a flexible wish fills whatever space is left and can never displace a fixed one (displacing a user-chosen date would be surprising). Priority still orders things among several flexible wishes. Flag if a high-priority flexible wish should be allowed to push a low-priority fixed one. Remember the Valborgsmässoafton precedent (§6): when in doubt about a default, ask. --- ## 4. Known issues ### 4.1 Three verification failures, none from feature work 1. **`npx tsc --noEmit` reports `tsconfig.json(19,5): error TS5101: Option 'baseUrl' is deprecated`.** The project pins `typescript ^5.6.3`, and this check was clean earlier the same day, so `npx` is very likely resolving a newer TypeScript than the local one. Check with `./node_modules/.bin/tsc --version` and run the local binary directly before believing it. Not caused by any source change. 2. **ESLint reports 3 warnings** — in `block-navigation.js`, `prettify.js`, `sorter.js`. These are **generated artifacts** under `coverage/lcov-report/`, not source. They only appear after `npm run coverage` has run. Either ignore `coverage/` in the ESLint config or clean it before linting. CI uses `--max-warnings 0`, so this will fail CI once coverage has been generated. 3. **One flaky test:** `usePlanApply › handles replace parameter correctly` (`src/hooks/usePlanApply.test.ts:117`) — failed once with `expected null to deeply equal {...}` and passed in the very next run of the same suite. Almost certainly a missing `await`/`waitFor` on async state. Worth fixing properly rather than re-running until green. ### 4.2 Traps that have already caused real bugs here - **The `holidays` library returns combined names.** Easter Sunday comes back as the single string `"Påskdagen; Söndag"`. Filtering with `"Söndag" in name` silently deletes **Påskdagen, Pingstdagen, Nationaldagen and Annandag jul**. Match the exact string only. - **`_FULL_SCHEMA` in `backend/app/db/connection.py` must stay in sync with Alembic migrations.** Test fixtures apply that DDL string directly, so forgetting it means every test runs against a stale schema. Migration 002 is a good worked example. - **`vi.mock('@/context')` does NOT intercept direct submodule imports** like `@/context/AuthContext`. This caused 14 test failures. Mock the specifier the component actually imports. `Calendar.test.tsx` / `Settings.test.tsx` show the right idiom. - **The dataclass → response mapping in `app/models/plan.py` is hand-written.** Anything added to the engine is invisible to the UI until someone maps it. Two fields were silently dropped there today (`surplus_advice`, `is_assumed`). When adding an engine output, check that boundary and add an **API-level** test, not just a service test. - **`localStorage` is unreliable in this test sandbox** (Node's experimental webstorage shadows jsdom's). Existing tests work around it with `vi.stubGlobal('localStorage', mock)` — follow that. - **`git grep` only searches tracked files.** Much of the new backend code is still untracked, so `git grep` will silently miss it. Use plain `grep` when checking new work. --- ## 5. How to verify ```bash cd backend && uv run ruff check . && uv run ruff format --check . && uv run pytest cd frontend && npx tsc --noEmit && npm run lint && npx vitest run && npm run coverage ``` **Cost rule (in `CLAUDE.md`):** large models must NOT run these themselves — spawn a **Haiku** subagent to run them and paste raw output. Haiku agents run them directly. Always give the verifier the expected baseline and require verbatim output; several agents today reported gates as passing that were in fact failing. **And verify in a browser.** Every significant bug found today — the shortfall contradiction, the dubbeldag rejection, the stale save banner, julafton being charged as a working day, the dropped API fields — was found by clicking through the app while both test suites were green. The suites confirmed the code did what it was written to do, not what it should do. --- ## 6. Decisions already made — don't relitigate - **Planning horizon runs past the child's 4th birthday.** After B4 the FP pool is hard-capped at `min(remaining, 96)`. The app plans it anyway and shows the squeeze rather than refusing. - **Weekly intensity is derived from the planning goal**, not stored on a wish. - **Preschool planning days are reserved off the top**, per-child configurable (default 6). - **Vacation vs föräldrapenning is goal-driven, not a fixed order.** In a red-day-dense window the goals *converge* — semester over klämdagar both protects pay and preserves FP days. See `PLANNING_ENGINE_SPEC.md` §3.3 (three-tier resolution). - **Valborgsmässoafton is NOT a default day off.** It was added, then removed — 30 April is an ordinary working day for most Swedes. Users who get it off add it via a day override. A wrong default day off is worse than a missing one. - **No API calls at runtime in the planner.** The Swedish calendar is fetched at build time (`backend/scripts/fetch_swedish_calendar.py`, 10 years cached) so planning stays offline and deterministic. - **The `Allocator` is pure** — no DB, no `date.today()`. Only `AllocationInputLoader` touches the world. This exists because clock-reading inside `tips_service.py` made tests pass or fail depending on the calendar date. - **Coverage thresholds are a regression ratchet, not a target** (lines 80 / functions 85 / branches 85 / statements 80). They were deliberately lowered from 90 when `src/pages/**` stopped being excluded from measurement. Raise them as coverage grows; don't "fix" them back to 90. - **i18n is hand-rolled on purpose** — no `react-i18next`. English derives its type from Swedish so a missing key is a compile error. --- ## 7. Repo pointers - `PLANNING_ENGINE_SPEC.md` — the engine design (wishes, three-tier allocation, shortfall remedies, surplus advice, test matrix) - `ARCHITECTURE.md` — system design; §13 covers the calendar cache and allocation service - `backend/scripts/demo_plan.py` — runs the allocator standalone on Swedish defaults and prints a readable plan. Best way to sanity-check engine changes without a browser. - `backend/scripts/fetch_swedish_calendar.py` — regenerates the 10-year calendar cache - The many `*_REPORT.md` / `*_TODO.md` files at the repo root are **historical handoff notes, not spec**. `todo.txt` is fully done and can be deleted.