Files
F-r-ldraledigplannerare/NextSteps260913.md
T
jacobh2 76b1e712b9 Complete NextSteps roadmap: planning engine UI, calendar cluster, flexible wishes
Works through every numbered item in NextSteps260913.md section 3, plus the
three known verification issues in section 4.1.

Surface what the backend already computed:
- 3.1 render surplus_advice in PlanResults, mirroring the shortfall remedies
- 3.2 surface VacationBalance.is_assumed so assumed statutory years are visible
- 3.3 age-4 expiry given the proposed plan, via a new Age4Projection reusing
  the existing _age4_projections arithmetic (Dashboard stat verified, not rebuilt)

Calendar cluster (one pass over the holiday data):
- new GET /api/calendar/days exposing roda dagar, klamdagar and day overrides,
  paginated four years at a time
- 3.4 shade roda dagar/klamdagar, with R/K badges so colour is not the only signal
- 3.6 click-to-override behind an explicit mode toggle, leaving drag multi-select intact
- 3.7 click month/year to advance
- 3.8 per-year red-days list in Settings, statutory days plus overrides

i18n:
- 3.5 five backend-generated Swedish surfaces converted to message_key + data,
  following the tips_service pattern; old string fields replaced, not kept

Flexible wishes (3.9):
- PLANNING_ENGINE_SPEC section 10: window_mode, candidate search, cost function,
  deterministic tie-break, two-pass ordering, test matrix rows 15-24
- backend: entity, migration 006, _FULL_SCHEMA sync, repository, allocator
- frontend: season-preset toggle in WishEditor, unplaced outcome rendering

Fixes (4.1):
- pin typecheck to the local tsc rather than npx resolving a newer TypeScript
- ignore generated coverage/ and dist/ in the ESLint flat config
- fix the flaky usePlanApply test properly, with waitFor rather than a retry

Tests: backend 361 -> 404, frontend 540 -> 704. Coverage 89.47% -> 96.52% lines.

Not yet verified in a browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
2026-09-14 02:33:03 +02:00

282 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.