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
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user