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.
This commit is contained in:
2026-09-15 20:27:48 +02:00
parent 5d9ac5024a
commit 7f516f2cab
5 changed files with 57 additions and 858 deletions
+15
View File
@@ -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)
-281
View File
@@ -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.
-134
View File
@@ -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.
-438
View File
@@ -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.
+42 -5
View File
@@ -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.