From 7f516f2caba6876ac824ad026461e8b6da6df7e2 Mon Sep 17 00:00:00 2001 From: Jacob Hagstedt Date: Tue, 15 Sep 2026 20:27:48 +0200 Subject: [PATCH] Retire the NextSteps handoff notes and pin the model tiers The three NextSteps*.md files were per-session handoff notes that had gone stale; the plan they described is superseded. CLAUDE.md gains a MODEL TIERS section recording who is allowed to run as what: Opus coordinates, Sonnet implements, Haiku verifies, and no subagent runs on Opus. --- CLAUDE.md | 15 ++ NextSteps260913.md | 281 ----------------------------- NextSteps260914.md | 134 -------------- NextSteps260915.md | 438 --------------------------------------------- todo.md | 47 ++++- 5 files changed, 57 insertions(+), 858 deletions(-) delete mode 100644 NextSteps260913.md delete mode 100644 NextSteps260914.md delete mode 100644 NextSteps260915.md diff --git a/CLAUDE.md b/CLAUDE.md index 52ed77e..3d3e200 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -169,6 +169,21 @@ to paste the **raw output verbatim**, including failures, rather than summarisin reports "all green" without the output is useless — several agents have reported passing gates that were in fact failing. +### MODEL TIERS (who is allowed to be what) + +Binding for all work in this repo, not just verification. + +| Role | Model | Effort | Allowed to | +|---|---|---|---| +| Coordinator | **Opus** | **medium** | Dispatch tasks, review returned diffs, make product calls, maintain the plan. **Writes no implementation code.** | +| Implementer | **Sonnet** | — | Feature and bugfix work. The only tier allowed to write non-trivial code. | +| Verifier / mechanical | **Haiku** | — | Run the full suites and paste raw output; small mechanical edits (renames, i18n string moves, test-fixture churn). | + +* **No subagent may run on Opus.** Not for "a hard task", not for review, not for browser + verification. If a task looks too hard for Sonnet, it is too big — split it, do not escalate it. +* The coordinator never runs the verification suites itself (see the cost rule above). +* Browser verification is delegated too — Sonnet drives Chrome, never the coordinator. + --- ## ANTI-PATTERNS (CRITICAL GUARDRAILS) diff --git a/NextSteps260913.md b/NextSteps260913.md deleted file mode 100644 index 72cd81a..0000000 --- a/NextSteps260913.md +++ /dev/null @@ -1,281 +0,0 @@ -# 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. diff --git a/NextSteps260914.md b/NextSteps260914.md deleted file mode 100644 index 3d8f0d1..0000000 --- a/NextSteps260914.md +++ /dev/null @@ -1,134 +0,0 @@ -# Next Steps — handoff, 2026-09-14 - -Written at the end of a long, delivery-focused session so a fresh session can pick up without re-deriving anything. -**Read `CLAUDE.md` and `ARCHITECTURE.md` first** — both remain current from the previous session. -`PLANNING_ENGINE_SPEC.md` holds the approved design of the planning engine. - ---- - -## 1. Where the app is - -The core user journey works end to end, and every significant feature from `NextSteps260913.md` §3 is now implemented and committed. - -> 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. - -**What was completed this session (all of §3.1–3.10 from the previous handoff, plus §4.1 issues):** - -- **3.1** `surplus_advice` now renders in `PlanResults.tsx`, matching the shortfall remedies layout -- **3.2** `is_assumed` surfaced with badges in the Plan balances table and Settings per-year table -- **3.3** Age-4 projection added to Plan tab proposal view (`Age4Projection` component reusing existing `_age4_projections` arithmetic from the Dashboard) -- **3.4** Röda dagar / klämdagar shaded in calendar with R/K badges (colour not the only signal) -- **3.5** Five backend-generated Swedish surfaces converted to `message_key`+`data` pattern; old fields REPLACED (not kept); `demo_plan.py` updated -- **3.6** Click-to-override mode toggle in Calendar (chose explicit toggle over modifier-click because drag multi-select owns plain click) -- **3.7** Click month name / year to fast-navigate between months -- **3.8** Red-days list in Settings (statutory + user overrides), paginated 4 years at a time -- **3.9** Flexible wishes ("när det passar bäst") with engine-driven red-day-dense proposals; `PLANNING_ENGINE_SPEC.md` §10 updated; backend entity/migration/repository/allocator; frontend season-preset toggle and `unplaced` outcome -- **3.10** Tests added for DevFetch, CalendarWeek, Dashboard, usePlannedDays -- **4.1** Three verification issues all fixed: typecheck pinned to local tsc, ESLint ignores `coverage/`/`dist/`, flaky `usePlanApply` test fixed with `waitFor` -- **Bonus:** New `GET /api/calendar/days` endpoint (see `ARCHITECTURE.md` §13.3) - -**State at handoff** -- Backend: **404 tests passing**, `ruff check` clean, `ruff format --check` clean -- Frontend: **704 tests / 56 files**, typecheck clean, lint 0 warnings -- Coverage: **96.52% lines / 91.08% functions / 86.78% branches** (configured thresholds are being ratcheted up concurrently by another agent; state the measured numbers here, not the new targets) - ---- - -## 2. Running it - -```bash -cd frontend && npm run dev # starts BOTH vite (5173) and uvicorn (8000) -``` - -If the servers die, restart individually: -```bash -cd backend && uv run uvicorn app.main:app --reload --port 8000 -cd frontend && npx vite -``` - -**Gotcha that cost an hour last session:** 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. If the API behaves impossibly, check `lsof -nP -iTCP:8000 -sTCP:LISTEN` for extra listeners. - ---- - -## 3. What's genuinely left - -Every numbered item from the previous handoff is done. Only these remain: - -### 3.1 Browser verification — HIGHEST PRIORITY - -**This is the single most important open item.** The entire feature set is implemented and both test suites pass, but nothing has been clicked yet. Every significant bug last session was found by clicking through the app while tests were green. - -Priority surfaces to click: -- §3.6: the new click-to-override mode toggle in Calendar -- §3.4: the röda dagar / klämdagar shading and R/K badges (colour accuracy) -- §3.8: the Settings red-days list pagination -- §3.9: flexible wishes end-to-end (propose, preview, apply) with season presets - -(A browser pass may be running as this handoff is written — state it as outstanding at time of writing if it hasn't completed.) - -### 3.2 Flexible-wish bounding-range picker (deferred, not rejected) - -The WishEditor ships v1 with season presets only (vår, sommar, höst, jul). A custom date-range picker for flexible wishes is known, deliberate, and deferred. The product owner chose season presets as the first ship to unblock the core feature. - -### 3.3 Frontend branch coverage - -Currently 86.78%, the weakest coverage metric. It drifted slightly down during the session (components added faster than tests). This is a regression-ratchet issue — not urgent if the metric is rising over time, but worth watching. - ---- - -## 4. Product-owner decisions made this session — don't relitigate - -- **Flexible wishes are a *propose*-time suggestion, confirmed in the Plan tab; never auto-applied.** The engine picks the window, the family sees it in the Plan view and approves, like every other plan. -- **Fixed wishes always win; flexible wishes fill remaining space.** A flexible wish cannot displace a fixed one (displacing user-chosen dates would be surprising). Priority still orders among several flexible wishes. -- **WishEditor ships v1 with season presets only:** vår (03-01→05-31), sommar (06-01→08-31), höst (09-01→10-31), jul (12-01→01-31 wrapping). Custom bounding-range picker is deferred. -- **An applied flexible occurrence may move on a later re-propose.** No `wish_id` on `planned_leave_day`; the propose→apply preview is the guardrail. -- **The `Age4Projection` stat on the Plan tab uses the proposed allocation.** It answers "how many days expire if this plan is taken," acknowledging that life may not work out that way. - ---- - -## 5. Known issues & traps - -### 5.1 Traps from previous handoff (still true) - -- **The `holidays` library returns combined names.** Easter Sunday comes back as `"Påskdagen; Söndag"`. Filtering with `"Söndag" in name` silently deletes Påskdagen, Pingstdagen, Nationaldagen and Annandag jul. Match exact strings. -- **`_FULL_SCHEMA` in `backend/app/db/connection.py` must stay in sync with Alembic migrations.** Test fixtures apply that DDL string directly; forgetting it means tests run against a stale schema. -- **`vi.mock('@/context')` does NOT intercept direct submodule imports** like `@/context/AuthContext`. Mock the specifier the component actually imports. -- **The dataclass → response mapping in `app/models/plan.py` is hand-written.** New engine outputs are invisible to the UI until mapped. Always add an **API-level test**, not just a service test, when the wire format changes. -- **`localStorage` is unreliable in the test sandbox.** Work around it with `vi.stubGlobal('localStorage', mock)`. -- **`git grep` only searches tracked files.** Use plain `grep` to catch untracked work. - -### 5.2 New trap from this session - -- **A literal `*/` inside a JSDoc block comment prematurely closes it**, breaking the whole file. Found in `WishEditor.tsx` during the flexible-wishes implementation. Avoid nested JSDoc strings with `*/` or escape them carefully. - ---- - -## 6. How to verify - -```bash -cd backend && uv run ruff check . && uv run ruff format --check . && uv run pytest -cd frontend && npm run typecheck && npm run lint && npx vitest run && npm run coverage -``` - -**Cost rule (from `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. - -**And verify in a browser.** Both suites may pass while the app is broken in the browser. See §3.1. - ---- - -## 7. Repo pointers - -- `PLANNING_ENGINE_SPEC.md` — the engine design, including §10 on flexible wishes (added this session) -- `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 -- `backend/scripts/fetch_swedish_calendar.py` — regenerates the 10-year calendar cache -- `CLAUDE.md` — the operating guide for the repo - ---- - -## 8. Clean state - -All changes committed as `e3d19f2`. Working tree clean. Old todo files (`todo.txt`, `todo_260912.txt`) were fully addressed and deleted. `todo-backend.txt` (June notes on smoketests, postgres, scraper scope) is also deleted; those items are pre-work scope or in separate tracks and do not block any current work. diff --git a/NextSteps260915.md b/NextSteps260915.md deleted file mode 100644 index a8df39c..0000000 --- a/NextSteps260915.md +++ /dev/null @@ -1,438 +0,0 @@ -# Next Steps — handoff, 2026-09-15 - -**Format note for whoever dispatches this:** each task below (§2 onward) is written as a -self-contained brief. Copy ONE brief verbatim into a subagent prompt — it does not need the rest -of this file, `NextSteps260914.md`, or `NextSteps260913.md`. Only the orchestrator needs §0/§1. -`NextSteps260913.md` and `NextSteps260914.md` are history — do not edit them. - ---- - -## 0. State as of now (all committed, working tree clean) - -Commits this session: `e3d19f2`, `42cebba`, `dd7f0ff`, `084ee91`, `760b312`. - -Gates, all currently green — **these are the baselines to paste into every verifier**: -- Backend: **439 tests passing**, `ruff check` clean, `ruff format --check` clean (128 files) -- Frontend: **710 tests passing / 56 files**, `npm run typecheck` clean, `npm run lint` 0 issues -- Coverage (frontend): 96.54% lines/statements, 91.08% functions, 86.84% branches. Thresholds are - a **regression ratchet — never lower them**: lines 95 / statements 95 / functions 89 / branches - 85. Backend CI gate `--cov-fail-under=75` (measured ~81), set in `.github/workflows/ci.yml`. - -Everything in `NextSteps260914.md`'s roadmap is DONE, including full browser verification. - ---- - -## 1. Shared preamble (orchestrator reads this once; do not forward to subagents) - -**Stack:** Python 3.14 / FastAPI / uv backend (no ORM, hand-rolled repositories, Alembic -migrations); TypeScript / React 18.3 / Vite 5 / Vitest frontend (no TanStack Query, no Redux, -hand-rolled i18n). Full conventions in `/Users/jhag/projects/Föräldraledigplannerare/CLAUDE.md`. - -**Run the app:** -```bash -cd frontend && npm run dev # starts BOTH vite (5173) and uvicorn (8000) -``` -Vite binds IPv6 `::1` — use `http://localhost:5173`, **not** `127.0.0.1`. If the API behaves -impossibly, check `lsof -nP -iTCP:8000 -sTCP:LISTEN` for a squatter (macOS resolves `localhost` to -`::1` first). - -**Seed demo data:** -```bash -cd backend && uv run python scripts/seed_demo.py --reset -``` -Login `demo@example.com` / `DemoPass123!`. Creates a couple workspace: Alex (has a login, a -near-drained pool) and Sam (no-login partner, created via `add_placeholder_parent`, large -surplus), child Nova, vacation rows for only some years (so `is_assumed` badges show), a worked -override and an off override, and four wishes including a flexible one and a deliberately -underfunded one (produces both shortfalls and surplus advice). - -**Sanity-check the engine without a browser:** `backend/scripts/demo_plan.py`. - -**Verification commands:** -```bash -cd backend && uv run ruff check . && uv run ruff format --check . && uv run pytest -cd frontend && npm run typecheck && npm run lint && npx vitest run && npm run coverage -``` - -**Cost rule (from `CLAUDE.md`):** a Haiku subagent runs these itself, immediately. A Sonnet-or-larger -subagent must NOT run full-suite verification itself — it finishes the code change, then spawns a -Haiku subagent with the exact commands, the directory to run them from, the numeric baseline above, -and an instruction to paste **raw verbatim output**, not a summary. Several agents have reported -gates as "all green" that were in fact failing — a report without pasted output is not accepted. -This rule does not apply to a single focused test run while mid-task. - -**Browser verification matters.** Every significant bug found this project — the calendar -overshoot, the flexible-wish double-booking, the doubled age-4 entitlement, two stale-data bugs — -was found by driving the running app while the entire suite was green. Suites confirm the code -does what it was written to do, not what it should do. `backend/tests/integration/test_user_journey.py` -guards the main journey but is not a substitute for clicking. - ---- - -## 2. Waves - -- **Wave 1 — investigation, no code guessed.** Task A only. It touches read paths across - frontend and backend but the brief explicitly forbids shipping a fix before the behaviour is - confirmed, so it cannot safely run in parallel with a wave that edits the same files for a - different reason. Not blocked on anything. -- **Wave 2 — independent file-owning tasks, can run fully in parallel.** Tasks B (frontend - `WishEditor.tsx` UI, blocked on a product decision — see brief), C (frontend test files only, - raises branch coverage), and D (backend `analytics_service.py` docs/decision framing, blocked on - a product decision). B, C, D touch disjoint files (B: `WishEditor.tsx` + its test + i18n; C: test - files across the frontend under existing components; D: `analytics_service.py` region + possibly - `Dashboard.tsx`) and none touches a file Task A investigates, so all three can be dispatched - together once Wave 1's finding is known (Task A might turn out to change what Task B/D need, but - neither B nor D depends on A's *outcome* to start — see each brief's decision-flag). - -If Task A concludes "this is a real bug and the fix is obvious and small," that fix is a **new** -task, not folded into A's brief — A is investigation-only per the instructions below. - ---- - -## Task A — Investigate the Settings partner section - -### Goal -Determine, with evidence (not assumption), whether the Inställningar (Settings) page's partner UI -is broken or working as designed. In the seeded couple workspace, partner "Sam" exists in the -workspace and is planned around (wishes, allocations, the surplus/shortfall picture all reference -Sam), but the Settings page currently shows only an empty "Add a partner" form with no visible -indication a partner is already linked. This was spotted in passing during a previous session and -was **not** dug into — it may be correct behaviour with unusual seed data, or it may be a real gap. -Done looks like: a clear, evidenced verdict — "this is a bug, here is exactly what's wrong and -where" or "this is correct, here is why the seed data made it look wrong" — with no code changed -unless the fix is trivial and you have explicit confirmation from whoever dispatched you that -shipping it is in scope. - -### Owned files -Read-only investigation. If (and only if) you get explicit go-ahead to fix something, the only -files in scope are: -- `frontend/src/pages/Settings.tsx` -- `frontend/src/context/WorkspaceContext.tsx` -- `backend/app/routers/workspace.py` - -Do not touch anything else without asking first. - -### Constraints and traps that apply to THIS task -- **`vi.mock('@/context')` does NOT intercept direct submodule imports** like - `@/context/AuthContext`. Mock the exact specifier the component imports — this caused 14 test - failures once. `Calendar.test.tsx` / `Settings.test.tsx` show the right idiom. -- **`localStorage` is unreliable in this test sandbox** (Node's experimental webstorage shadows - jsdom's). Use `vi.stubGlobal('localStorage', mock)`. -- **Every user-facing string must be `message_key` + `data`**, never a hardcoded Swedish string. - `backend/tests/integration/test_user_journey.py::TestNoSwedishOnTheWire` walks responses for - å/ä/ö and will fail violations. `frontend/src/i18n/sv.ts` is the source of truth; `en.ts` derives - its type from it, so a key missing from either is a compile error. -- **No `any`, `@ts-ignore` or `@ts-expect-error`** — `no-explicit-any` is an ESLint error and CI - runs `--max-warnings 0`. -- **Use `npm run typecheck`, never `npx tsc`** — npx resolves a newer TypeScript and emits a bogus - `TS5101 baseUrl is deprecated` error. -- **`git grep` only searches tracked files** — use plain `grep` when checking new or uncommitted - work. -- **Never delete, skip or weaken a test to make a suite pass.** - -### Facts already gathered (starting point — verify, don't re-derive from scratch) -- `frontend/src/pages/Settings.tsx` has an "Add partner" form (`handleAddPartner`, around line 230; - the JSX section starts around line 397 with `settings.partnerSectionTitle`) that POSTs via - `api.addPartner`. It does **not** appear to call `api.listWorkspaceMembers` anywhere, nor render - any existing-member list — grep confirms `members`/`Members` do not appear in `Settings.tsx`. -- `frontend/src/services/api.ts` around line 868–872 has `listWorkspaceMembers(workspaceId)` which - calls `GET /workspace/{id}/members` — this function exists and is used elsewhere (for picking - which parent a wish belongs to in wish editing) but is a candidate for what Settings is missing. -- `backend/app/routers/workspace.py` has `GET /api/workspace/{workspace_id}/members` (returns all - `PersonalDataResponse` rows, i.e. both the logged-in user and any placeholder partner) and - `POST /api/workspace/{workspace_id}/parents` (`add_placeholder_parent`) which is how the seeded - partner "Sam" was created — a `PersonalData` row with **no login**, per - `backend/scripts/seed_demo.py` (`PARTNER_NAME = "Sam"`, `add_placeholder_parent`). -- `frontend/src/context/WorkspaceContext.tsx` exposes `parentName` (the *current logged-in user's* - name) and `personalDataUuid`, but has no field for "other members" or "partner name" — check - whether this is intentional (Settings might be expected to fetch members itself) or a real gap. - -### What to actually check -1. Run the app with seeded demo data, log in as `demo@example.com`, open Inställningar, and look - at the partner section with your own eyes (or via `mcp__claude-in-chrome__*` browser tools). - Confirm what is and isn't shown. -2. Read all of `Settings.tsx` to see if there's member-fetching logic you missed (e.g. behind a - loading state, an error being swallowed, or a condition that's never true for this seed). -3. Check the network tab / add a temporary log to see if `GET /workspace/{id}/members` is even - called from Settings, and if so, what it returns for the demo workspace. -4. Check `Settings.test.tsx` for existing coverage of the partner section — does a test already - assert "no member list is shown," which would mean it's a deliberate v1 scope cut rather than a - bug that slipped through? -5. Decide: is the correct behaviour "show existing partner(s) with a way to manage/remove them" and - Settings simply never got that view built (a real gap), or is the current one-shot "add a - partner" form intentional because a workspace only ever gets one partner added once, after - which the UI doesn't need to display them here (they show up in the Plan/Calendar contexts - instead)? Look for the second parent's name surfacing anywhere else in Settings (e.g. a - per-parent vacation table) as evidence for the second reading. - -### Done criteria -No code changes are the default outcome. Report: -- The concrete behaviour observed (with a screenshot or copied DOM/text if using the browser - tools). -- Whether `GET /workspace/{id}/members` is called from Settings, and what it returns. -- A clear verdict: bug vs. by-design vs. "by-design but confusing enough to flag for a product - decision." -- If you conclude it's a bug and the fix is small and obvious (e.g. "just render the existing - members list that `listWorkspaceMembers` already returns"), you may implement it, but only after - making the verdict explicit in your own reasoning — do not silently patch it as a side effect of - investigating. If you do fix it, run: - ```bash - cd frontend && npm run typecheck && npm run lint && npx vitest run && npm run coverage - ``` - and confirm no regression against the frontend baseline (710 tests passing / 56 files, coverage - ≥ 96.54/91.08/86.84 for lines/functions/branches) before reporting done. Per the cost rule, if - you are a Sonnet-or-larger agent, delegate that run to a Haiku subagent and require verbatim - output. - -### Anything needing a product decision -If Task A finds the display is ambiguous-but-defensible (e.g. the partner does show up elsewhere, -just not in Settings), flag to the product owner: *should Settings show a "Partner: Sam" line with -a remove/replace action once a partner exists, instead of (or alongside) the add-partner form?* -Do not guess an answer and build it. - ---- - -## Task B — Custom bounding-range picker for flexible wishes - -### Goal -A flexible wish ("när det passar bäst") today can only be bounded by one of four season presets -(vår 03-01→05-31, sommar 06-01→08-31, höst 09-01→10-31, jul 12-01→01-31, which wraps the year -end). This was a deliberate v1 scope cut, not a rejection. Done looks like: a family can pick an -arbitrary date range (e.g. "sometime between March 15 and April 20") as the bounding range for a -flexible wish, in addition to keeping the four season presets as one-click shortcuts. The engine -side needs no changes — a flexible wish already reuses the existing `window_start_month/day` / -`window_end_month/day` fields as its search bound; this is a UI-only addition. - -### Owned files -- `frontend/src/components/planning/WishEditor.tsx` -- `frontend/src/components/planning/WishEditor.test.tsx` (or wherever its existing test file is — - confirm the exact name with `git ls-files | grep -i wisheditor` before starting) -- `frontend/src/i18n/sv.ts` and `frontend/src/i18n/en.ts` (new keys only, additive) - -Do not touch `backend/app/services/allocation_service.py`, `backend/app/entities/leave_wish.py`, -or `PLANNING_ENGINE_SPEC.md` — no engine or spec change is needed for this task. - -### Constraints and traps that apply to THIS task -- **A literal `*/` inside a JSDoc block comment prematurely closes it and breaks the whole file** — - this already bit this exact file (`WishEditor.tsx`) once. Avoid nested JSDoc strings containing - `*/`, or escape them carefully, in any comment you add or edit. -- **Every user-facing string must be `message_key` + `data`**, never a hardcoded Swedish string in - the frontend component — use `frontend/src/i18n/sv.ts` as the source of truth; `en.ts` derives - its type from it, so a key missing from either is a compile error. -- **No `any`, `@ts-ignore` or `@ts-expect-error`** — `no-explicit-any` is an ESLint error and CI - runs `--max-warnings 0`. -- **Use `npm run typecheck`, never `npx tsc`** — npx resolves a newer TypeScript and emits a bogus - `TS5101 baseUrl is deprecated` error. -- **Never delete, skip or weaken a test to make a suite pass.** -- **`localStorage` is unreliable in this test sandbox** — use `vi.stubGlobal('localStorage', mock)` - if the component or its tests touch it. -- The bounding-range fields already exist and are named `windowStartMonth` / `windowStartDay` / - `windowEndMonth` / `windowEndDay` in the wish object built by `WishEditor.tsx` (see - `addPreset`/`addSeasonPreset`, around lines 162–182) — a custom picker must populate exactly - these same fields, in the same month/day (not full-date) shape, since that's what the backend - entity and allocator expect. Do not invent a new field or a full ISO-date representation. -- Season presets fill the bounding range only, **not** the final placement (see the existing - `PLANNING_ENGINE_SPEC.md §10.1`/§10.7 comments already in the file around lines 58–65 and - 179–182) — a custom range picker must follow the same contract: it sets the search bound, the - engine still picks the actual placement inside it at propose time. - -### Done criteria -```bash -cd frontend && npm run typecheck && npm run lint && npx vitest run && npm run coverage -``` -Must show: typecheck clean, lint 0 issues, all tests passing (baseline 710 passing / 56 files, this -task adds tests so the count should only go up), and coverage not regressing below the current -ratchet (lines 95 / statements 95 / functions 89 / branches 85 — current measured 96.54 / 96.54 / -91.08 / 86.84). Per the cost rule, if you are a Sonnet-or-larger agent, delegate this run to a -Haiku subagent and require verbatim output, not a summary. - -Also do a manual browser pass (seed demo data per §1, log in, open a wish editor, add a flexible -wish, use the custom picker, propose a plan) — this project's history is that suites pass while the -feature is subtly broken in the browser (double-booking, overshoot, etc. were all browser-only -finds). - -### Anything needing a product decision -**This entire task is blocked on a product decision that has not been made:** how much freedom to -expose in the custom picker. Options include (not exhaustive, do not pick one yourself): -- Any two dates, unconstrained (family could pick a 300-day bounding range). -- A minimum/maximum span (e.g. must be between 1 and ~90 days) to keep the engine's - window-scanning inside sane limits. -- Whether the picker allows a range that wraps the calendar year end, the way the `jul` preset - does, or restricts custom ranges to a single calendar year for simplicity. -- Whether the four season presets stay as visible quick-picks alongside the custom picker, or the - custom picker replaces them once available. -Do not guess and build; get this decided (or explicitly ask the user) before writing UI, since it -changes the validation logic and the i18n copy. - ---- - -## Task C — Raise frontend branch coverage - -### Goal -Frontend branch coverage (86.84%) is the weakest of the four coverage metrics and has the least -headroom above its ratchet floor (85%). Done looks like: branch coverage measurably higher than -86.84% (ideally comfortably clear of 85, e.g. 88%+), achieved by adding real tests for genuinely -untested branches — not by deleting or weakening any existing test, and not by chasing the number -with low-value tests that don't exercise a real decision path. - -### Owned files -Test files only — do not modify any non-test source file to make coverage easier to hit. Before -starting, run `npm run coverage` and read the generated HTML/text report -(`frontend/coverage/`) to find the actual weakly-covered branches (this project's own history notes -that `Auth.tsx`, `Tips.tsx`, `DevFetch.tsx` were previously at or near 0% — check whether that is -still true or has since improved before assuming those are still the gaps). Pick specific files -based on the report, then add or extend the corresponding `*.test.tsx`/`*.test.ts` file for each. -Do not touch files owned by Task A (`Settings.tsx`) or Task B (`WishEditor.tsx`) — if the coverage -report points you at either of those, pick a different low-coverage file first and leave a note -about the overlap instead of racing another task's edit. - -### Constraints and traps that apply to THIS task -- **`vi.mock('@/context')` does NOT intercept direct submodule imports** like - `@/context/AuthContext`. Mock the exact specifier the component imports — this caused 14 test - failures once. `Calendar.test.tsx` / `Settings.test.tsx` show the right idiom. -- **`localStorage` is unreliable in this test sandbox** (Node's experimental webstorage shadows - jsdom's). Use `vi.stubGlobal('localStorage', mock)`. -- **No `any`, `@ts-ignore` or `@ts-expect-error`** — `no-explicit-any` is an ESLint error and CI - runs `--max-warnings 0`. -- **Use `npm run typecheck`, never `npx tsc`** — npx resolves a newer TypeScript and emits a bogus - `TS5101 baseUrl is deprecated` error. -- **Never delete, skip or weaken a test to make a suite pass.** This applies doubly here: the whole - point of this task is more real coverage, not a smaller denominator. -- **A literal `*/` inside a JSDoc block comment prematurely closes it** — be careful if any test - file you touch has JSDoc-style comments. -- **`git grep` only searches tracked files** — use plain `grep` when checking new or uncommitted - work by other in-flight tasks so you don't collide with them. - -### Done criteria -```bash -cd frontend && npm run typecheck && npm run lint && npx vitest run && npm run coverage -``` -Baseline to compare against: 710 tests / 56 files passing, coverage 96.54% lines/statements, -91.08% functions, 86.84% branches. After this task: test count should increase, all four coverage -numbers should be equal or higher (branches specifically should move up meaningfully, not by -0.01%), and none of the ratchet floors (lines 95 / statements 95 / functions 89 / branches 85) -should regress. Per the cost rule, if you are a Sonnet-or-larger agent, delegate this run to a -Haiku subagent and require verbatim output, not a summary. - -### Anything needing a product decision -None — this is pure test-quality work with no user-visible behaviour change. If while adding tests -you discover a branch that looks like a real bug (not just untested), stop and report it separately -rather than writing a test that locks in wrong behaviour. - ---- - -## Task D — Per-parent age-4 breakdown on the Dashboard - -### Goal -The Dashboard's age-4 stat ("Förfaller vid 4 års ålder") is workspace-wide — it sums both parents' -projected expiring days into one number and the label says "all parents". The Plan tab, by -contrast, already breaks this down per parent. Both numbers are currently correct (a prior bug that -doubled one parent's entitlement was fixed in commit `760b312`), so this is **not** a bug fix — it -is a product decision about whether the Dashboard should also show a per-parent breakdown for -clarity, the way the Plan tab does. Done looks like, IF the product owner confirms they want it: the -Dashboard shows each parent's own expiring-day count (not just the combined total) using the -per-parent projection the backend already computes, with the combined total kept or clearly -labelled as a sum, whichever the product owner picks. - -### Owned files -- `backend/app/services/analytics_service.py` (read-only reference; a per-parent breakdown is - likely already computed here — do not change its arithmetic without a specific reason) -- `frontend/src/pages/Dashboard.tsx` -- `frontend/src/pages/Dashboard.test.tsx` -- `frontend/src/i18n/sv.ts` / `frontend/src/i18n/en.ts` (new keys only, additive, if new labels are - needed) - -Do not touch `backend/app/models/plan.py`'s dataclass→response mapping unless the per-parent field -needed on the Dashboard genuinely isn't on the wire yet — check first (see traps below). - -### Constraints and traps that apply to THIS task -- **The 390 + 90 tier totals are the whole CHILD's entitlement across BOTH parents**, not one - parent's. One parent's no-snapshot default is `days_per_parent` (240 = 195 + 45). Getting this - wrong doubled a parent's entitlement and made two screens disagree — this was fixed in `760b312` - precisely in this area of the code (`analytics_service.py`), so be careful not to reintroduce it - while restructuring how the numbers are displayed. -- **The dataclass→response mapping in `backend/app/models/plan.py` is hand-written** and has - silently dropped new engine fields twice before. If you need a new field on the wire, add an - **API-level** test, not just a service test, to prove it actually reaches the response body. -- **Every user-facing string must be `message_key` + `data`**, never a hardcoded Swedish string. - `frontend/src/i18n/sv.ts` is the source of truth; `en.ts` derives its type from it, so a key - missing from either is a compile error. -- **No `any`, `@ts-ignore` or `@ts-expect-error`** — `no-explicit-any` is an ESLint error and CI - runs `--max-warnings 0`. -- **Use `npm run typecheck`, never `npx tsc`** — npx resolves a newer TypeScript and emits a bogus - `TS5101 baseUrl is deprecated` error. -- **Never delete, skip or weaken a test to make a suite pass.** -- Reference facts already confirmed in `backend/app/services/analytics_service.py`: it has - `expiring_at_b4` per parent (a single-parent projection, `max(projected_left - 96, 0)`) and a - combined view with `combined_days_used`, `combined_days_left`, `combined_expiring_at_b4` (sum - across all parents' projections) — the per-parent numbers this task needs already exist - server-side; check whether they already reach the Dashboard's API response or need adding to the - relevant response model. - -### Done criteria -Backend (only if you touch it): -```bash -cd backend && uv run ruff check . && uv run ruff format --check . && uv run pytest -``` -Baseline: 439 tests passing, ruff check clean, ruff format --check clean (128 files); count should -only increase. - -Frontend: -```bash -cd frontend && npm run typecheck && npm run lint && npx vitest run && npm run coverage -``` -Baseline: 710 tests / 56 files passing, coverage 96.54%/96.54%/91.08%/86.84% (lines/statements/ -functions/branches); none of the ratchet floors (95/95/89/85) may regress. - -Per the cost rule, if you are a Sonnet-or-larger agent, delegate both runs to a Haiku subagent and -require verbatim output, not a summary. Also do a manual browser pass with seeded demo data (see -§1) comparing the Dashboard and Plan tab numbers for Alex and Sam side by side — this is exactly -the kind of two-screens-disagree bug that has bitten this project before (`760b312`). - -### Anything needing a product decision -**This entire task is blocked on a product decision:** does the product owner actually want a -per-parent breakdown on the Dashboard, or is the current combined "all parents" framing intentional -(e.g. because the Dashboard is meant to be an at-a-glance household view and per-parent detail -belongs only in the Plan tab)? If yes, also decide: does the combined total stay visible alongside -the per-parent numbers, or does the per-parent breakdown replace it? Do not guess and build either -answer. - ---- - -## 3. Traps reference (for the orchestrator only — already duplicated into each brief above) - -Kept here once for the orchestrator's own awareness; do not point a subagent at this section -instead of the duplicated version in its brief — that defeats the point of self-contained briefs. - -- `holidays` library combined names (`"Påskdagen; Söndag"`) — exact-string match only. -- `_FULL_SCHEMA` in `backend/app/db/connection.py` must track Alembic migrations. -- `vi.mock('@/context')` doesn't intercept direct submodule imports. -- `backend/app/models/plan.py` dataclass→response mapping is hand-written and has dropped fields - twice. -- `localStorage` unreliable in the test sandbox — `vi.stubGlobal`. -- `git grep` only searches tracked files. -- Literal `*/` inside JSDoc breaks the file (bit `WishEditor.tsx` once). -- `npm run typecheck`, never `npx tsc` (npx resolves a newer TS, bogus `TS5101`). -- No `any` / `@ts-ignore` / `@ts-expect-error` — ESLint error, CI `--max-warnings 0`. -- Never delete/skip/weaken a test to make a suite pass. -- Keep the `Allocator` pure — no DB, no `date.today()`. -- Every user-facing string is `message_key` + `data`; `TestNoSwedishOnTheWire` enforces it. -- 390+90 tiers are the whole child's entitlement across both parents, not one parent's. - -## 4. Decisions already made — do not relitigate (for the orchestrator's awareness) - -- Planning horizon runs past B4; the FP pool is capped at `min(remaining, 96)` after — show the - squeeze, don't refuse. -- Valborgsmässoafton is NOT a default day off. -- No API calls at runtime in the planner; calendar cache covers 2026–2055, `strict=True` raises - `UncachedYearError` → 422 rather than silently degrading. -- 2049 and 2055 legitimately have 17 red days (Pingstdagen coincides with Sveriges nationaldag). -- Flexible wishes are propose-time suggestions, never auto-applied; fixed wishes always win. -- An applied flexible occurrence may move on a later re-propose (no `wish_id` on - `planned_leave_day`). -- WishEditor v1 ships season presets only — a custom range picker (Task B above) is deferred, not - rejected. -- Coverage thresholds are a regression ratchet, never lowered to make a gate pass. -- i18n is hand-rolled on purpose — no `react-i18next`. -- Verification cost rule: Haiku runs the suites itself; Sonnet+ delegates and requires verbatim - output. diff --git a/todo.md b/todo.md index 0eebfed..f364d8b 100644 --- a/todo.md +++ b/todo.md @@ -1,5 +1,42 @@ -- [x] In planning tab: It is very annoying that I need to click "save" on each individual thing. - Better if we auto-save this (with a debounce). - DONE — per-row Save button removed; edits auto-save 800ms after the last change, with a - per-row "Sparar…"/"Sparat" status. Also fixed the underlying Plan-page bug where every - mutation swapped the page for a spinner and collapsed the expanded row. +# These are TODOs and ideas from me, do not touch these unless explicitly told. + +- The whole UX and user flow feels very off. Here we need to rethink. More of this below. +- We need to make it more clear if a plan has been selected / is active, so that you clearly can either remove the entire plan, or do additions to the existing plan. Or maybe rethink the entire plan part all together. More of this below. +- Let's remove the "tips" tab, those should probably be instead on the side of the calendar view and in a section in the overview to make it more clear. But hidden if there's nothing to say. +- Not sure what the "Mark working/day off" button and entire logic is about in the Calendar view. Probably remove? +- In the calendar view, red days are not shown. I would expect e.g. today's week and month to be fully covered but it is not. Sundays should always be marked red and in addition to that, also the other red days. +- A setting that says if "half days" are actually used by the employer (e.g. myself does not have those, but many do, so should be a toggle) +- According to Gemini, Försäkringskassan will remove the days on "lägsta nivå" first, meaning a tip (and how the planner should work) is to use the "sjukpenningnivå" days first always. +- We should remove, in the settings, the planning goal. Everything will always be "most money" because the tips we have are all related to maximising the use of both parental leave days + vacation days. So this can be simplified across planning + settings. +- The days after the child turns 4 are also cut strictly for each parent to save max 48 days. So this means that it is probably good to transfer days between the parants (if possible) beforehand so that both have more than 48 days. Example of a bad situation: One parent have 60 days and the other have 10 days. This will result in the first parent losing 12 days and the second parent keeping the 10, resulting in a total of 58 days when a total of 70 could have been achieved. So this should be a super important tip if the balance between the parents are "off" and the child's 4th birthday is coming up. Maybe the first 2 yeras (child being 0 and 1 years old) are ok, but after that the app should start to show this! To do it even better is if both parents have fetched/entered data for the child, then the app can show exactly the situation. Otherwise it will just have to do a "btw". +- On the overview in the bottom it says "48 / 96 weeks" for example for the remaining until age 4. This is wrong: It is not 96 weeks it is days. Plus we should make this more clear that it is actually 48+48 days (per parent) that can be saved. So if we can modify the progress bar to show how much each parent have left, that would be good. +- It seems like in the "plan" tab that UUIDs are being displayed instead of the user's (parents) actual names. A translation from user id to name should happen here. For example, it says "3d6c6b2e-e91d-450c-99d6-bc199a91dd57 has a large surplus" which is confusing. + + +## User flow + +This flow assumes that it is the first time the user uses this app. So no prior data exists. + +1. A user logs in +2. The app shows that no child(ren) are registered and asks the user to fill that in. This can be done by manually adding the child, or by simply fetching data from Försäkringskassan, which then will give the app information about the amount of children and their names, to be auto-filled. +3. Once the data is there (manually or automatic), the overview page can be displayed. The most important thing to show here is if (and if so, how many) any days are at risk of being lost when the child turns 4 years old. At the very start this is most probably the case, that a lot of days are at risk of getting lost. +4. The user should be able to configure if the planning should happen for one or both parents directly, and this is then saved in the settings. (Another parent can later login and _also_ connect to the same child/plan +5. The user then heads over to the calendar tab, because now he/she wants to start planning. + +The planning can happen in two ways: + + 5.1. The user might click-and-select dates in the calendar view directly and say what kind of day this is (parental leave vs vacation etc) + + 5.2. By selecting one of the "quick buttons" that should show somewhere on the page where it makes for good UX/UI (maybe on the side of the calendar view?) which correspond to the "preset buttons" on the now Plan tab, so "Summer (6 weeks)", "Winter (4 weeks)", "Autumn break (1 week)" etc. + + If the user clicks one of the presets, the calendar view should imediately jump to the next upcoming time that preset can happen (i.e. if summer is pressed and we are in May, jump to July, if we are in September, jump to next year's July) and mark those weeks that the planner think is the best fit. The planner will also have selected the kind of time-off depending on what fits best. + +Somehow (and here I need help of UX expert agent), the periods should be displayed as one section even though they might include multiple types of time-off (PL + vacation), and the type of time-off should also be visible on a per-day basis. + +It should also be possible to click a time-off period and see that specific period (just like when you click on a multi-day event in your calendar on the phone) and there be able to change start/end date, the other things that can be changed and also importantly: The ability to "Repeat" (again, exactly like it is possible to do in a calendar app on your phone). Here the options should be "Don't repeat", "Every _ month" and "Every _ year" where 1 should be default but can be changed. That way a summer vacation can be repeated every year. + +Basically, this whole thing is like a normal calendar with events that can stretch for multiple days, with the difference that each individual day within a range can have "time-off types" and that the app as a whole needs to keep track of the amount of days of specific types in order to know how many days are left for the future. + +When an event is saved, that should trigger the planner (and whatever else needs to run) to run and re-evaluate things like tips etc. It can of course be that a user manually selected to have parental leave on a friday and then a monday, and then this is the time a new "tip" should be created saying something like "Hey, it is better if you take vacation here" or something like that. + +When a user have selected that e.g. a summer vacation should "repeat every 1 year", it might be better to shift the start & end date to better fit red days in another year. These kind of things should happen when the event is saved, when the planner runs and can calculate this.