FormField owns the .field markup and wires id/aria-invalid/aria-describedby
onto its control; Alert owns role=alert banners. Auth, Join and the partner
invite panel drop their one-off error classes for the shared alert style.
VacationDaysInput and ParentProfileInput move onto the .field primitives.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The planner knew exactly how many semesterdagar a parent was about to
forfeit and never placed a single day to prevent it. It also had no
weekend-wrap logic at all: REASON_VACATION_WRAP_WEEKEND was stamped on
every tier-3 interior vacation day, including a lone Tuesday in February.
Two changes.
Spend better. A workday adjacent to a free stretch, or in the middle of a
gap of three or fewer fenced by free days, now takes vacation --
_vacation_bridge_reason, consulted for every parent type. This departs
from spec 6.1's "wrapping suppressed for hourly": the FP-preservation
argument is salary-type independent, only the pay argument was hourly-moot.
In a both-parents window, both now take vacation on such a date rather
than one paying with FP. After funding, reallocate_vacation_by_value swaps
vacation onto higher-ranked dates 1-for-1 with FP days, so which dates hold
vacation improves while how many never drops -- greedy first, re-timed
after, never withheld. An invariant test asserts the count can only rise.
Name the losses. A new pass proposes concrete dates for vacation that would
otherwise expire, current year only, ranked red-day bridge then weekend
bridge then interior, later dates first. They ride on
PlanProposal.rescue_days, kept off `days` so nothing downstream commits
them, and surface on the Dashboard with accept-one, accept-all (one
batched /api/plan/apply, not N) and dismiss. Dismissal reuses the
tip_dismissal table, keyed on (parent, year) identity rather than the
group's dates -- those are recomputed from the whole plan's residual on
every propose, so date-hashing made dismissal self-defeating. Dismiss does
not bump the plan version; it changes no data and must not trigger a write.
Measured on the seeded demo: 12 forfeiting days for one parent now yield 12
proposed dates, all Fridays or Mondays. Total vacation placed is unchanged
at 203 days, confirming the reallocation only re-times.
Three defects here were found by driving a browser, while 2273 unit tests
passed -- all three lived in the seam between layers that unit tests mock
away. Adds API-level tests over the real request sequence. Rate limiting is
now off in tests (one file consumed the whole registration budget) with the
suite's first real 429 assertion.
PROPOSE_VS_SYNC_BUG.md documents a pre-existing, unfixed issue found along
the way: Dashboard figures swing on the first sync with no user action.
Backend 946 tests, frontend 1341, coverage above all gates.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZaX6xbuMEMXuyKA1fq2Mm
Warnings such as "vacation days at risk of being lost" kept showing after
the planner had already resolved them. The planner itself was firing
correctly -- PlanSyncProvider syncs on every plan-input change -- so the
fault was in the warnings, in two independent places.
The vacation-expiry warning was computed before allocation. It read
`vacation_forfeiting`, which the loader derives from already-committed
usage, while `_assign_tier3` deliberately spends at-risk vacation days
first precisely to avoid that forfeiture. The figure therefore counted
days the plan was in the middle of saving. `_vacation_expiry_warnings`
now subtracts the vacation days this run actually placed for that parent
and year, clamped at zero -- sound because the bank is FIFO, so a day
spent in year Y retires one expiring day one-for-one. The full
carry-forward simulation is still not duplicated inside the ledger.
The Dashboard then never refreshed them. Its proposal came from a
one-shot effect guarded by `proposal === null`, with no planVersion
dependency, so the card kept rendering the pre-replan warning until
remount -- while `syncPlan` was already returning a fresh proposal that
both call sites discarded. PlanSyncProvider now exposes that proposal
behind a monotonic counter, and usePlanProposal prefers it once any sync
has completed, keeping `propose()` for the first load.
Also adds a manual recompute button to Settings, which only bumps
planVersion and reports what the resulting sync did, and fixes partner
add/remove never bumping planVersion at all -- that gap meant gaining or
losing a parent never re-ran the allocator, though it changes who days
can be assigned to. Rename stays cosmetic.
Verified in the browser: a 2031 warning of 15 days dropped to 12 after
three vacation days were planned, without a reload.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jPFUcCJ5AKXmPVC1anhXe
Free days (Sat/Sun/red days, subject to DayOverride) can never be
vacation, and can only be parental leave when the workspace opts in via
take_pl_on_red_days. The allocator already honoured this; the hand-pin
path bypassed it entirely.
- PlanningService gains optional workspace_repo/day_override_repo deps
and _reject_illegal_free_day, enforced in add_planned_day and update.
Reuses allocation_service.is_free_day rather than re-deriving it, and
no-ops when the deps are unset so existing callers are unaffected.
- update_planned_day had no ValidationError handler at all, so a
validation failure there 500'd instead of 422'ing. Fixed.
- Calendar filters the pin write loop by the day's isFree; the modal
explains free days stay off, and that pinning opts a day out of
automatic re-planning.
Vacation days at risk of forfeiture were computed and sent over the
wire but never rendered: an at-risk column with the age-out/over-cap
split, and a dashboard key figure.
PlanProposal.warnings was likewise parsed but rendered nowhere, for all
three kinds. Adds PlanWarningCard in a Risks section above the tips,
with parent names resolved and dates formatted.
POST /api/plan/sync now fires from the workspace layer on every
plan-input change rather than only from Calendar, so a Settings edit or
a pinned-day save re-plans day types instead of merely refetching.
Overlapping bumps coalesce into one trailing run; a rejection cannot
wedge the in-flight guard. eviction_preview was silently dropped by the
frontend despite always being on the wire, and is now surfaced through
context.
Verified: backend 890 passed (91.49% coverage), frontend 1297 passed,
ruff/eslint/tsc clean. Browser-checked in Swedish and English.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVQx4NUvPbJhn6UcvRd2QY
Three strands of work that grew together across the same settings screen and
API client, so they land as one change.
Per-year preschool planning days ("studiedagar"): new table, repository
(SQLite + Postgres), service, router and migration 007, plus the hooks and
Settings UI to plan and record them per child and year. Allocation now
reserves the recorded figure for a year where one exists and only falls back
to the child-level estimate otherwise, marking that fallback as assumed.
Partner invitations: workspaces issue and list invitations, registration
accepts an invite token to join the inviter's workspace instead of creating a
personal one, and a /join/:token page plus the settings invite panel drive it.
Vacation forfeiture: tips and the dashboard now distinguish days a parent
genuinely loses from days that merely age, so the warning stops firing on
balances that were never at risk.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dashboard stated the loss and stopped there. With Jacob at 48/48 savable
and 24 days over the cap, and Amelie at 3/48 with 45 days of headroom, those
24 days are not lost at all — transferring them before the 4th birthday saves
every one. The app never said so.
`_rule_age4_pooling_loss` already computed exactly that advice, bounded by the
FK-reported `transferable_days_left`, in finished Swedish and English. It never
ran: `age4_pooling_tip_min_child_age_years` held it back until the child turned
2. The calendar IS the plan, so gate on the plan instead — the noise case is a
newborn with nothing planned, not a young child whose parents have planned in
detail. `age4_pooling_tip_min_planned_days` (20) replaces the age gate.
Both variants drop to `info`. A projection four years out should not read as a
verdict on the user's planning; the hero carries the alarm, this card carries
the advice. The copy now leads with "plans change, this is only a projection"
and closes by pointing at keeping balances up to date, since parents will come
back and record what they actually took.
- Suppress `preserve_for_age_4` whenever `age4_pooling_loss` fires for the same
child. The pooling tip states the same loss plus the remedy and the correct
per-parent cap, so emitting both gave two contradictory "you're losing N
days" figures.
- `preserve_for_age_4` now sends `parent_cap` and its copy interpolates it. It
claimed "only 96 days can be saved" — false for a two-parent family, where
the cap is 48 each, as the Age4Bars card two rows below already showed.
- The hero grows a "How to save them" line derived from the tip itself, so the
two can never disagree on the number. Dismissing the tip retires both.
- "Försäkringskassan" in that line links to FK's BankID login carrying a `goto`
to Min föräldrapenning — the e-service showing "Dagar ni kan föra över" —
so the transfer is one click away. `translate()` returns a plain string and
word order differs between locales, so the anchor's position travels inside
the template as `{fk}` and the rendered string is split around it.
Verified in the browser: hero, link target, tip severity and the absence of
`preserve_for_age_4`, in both languages and at 390px. Note the demo seed cannot
reach this shape — it gives the surplus parent no FK snapshot at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXPBfr1v7F71t6QcbHCaaT
The dashboard now leads with a typographic headline, three key
figures, per-parent age-4 bars and one row per year until the fourth
birthday. Each year row shows the timeline bands beside each parent's
parental-leave days left at year end and vacation days left, carried
over or expiring, and opens into a per-parent table with that year's
wish outcomes. Tips become a plain list and absorb the planner's
surplus advice; "Detaljer" keeps the metrics and a balance snapshot
for every parent. The separate plan-proposal fold and the
type-count table are gone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBRLomdLK9jF7wU7a8AA6H
New token system in global.css (near-white surfaces, one ink-blue
accent, no sub-12px type), self-hosted Schibsted Grotesk, rewritten
primitives (buttons, fields, modal shell, segmented, disclosure,
skeleton), a borderless 56px header with an SV | EN language toggle,
an Icon component replacing glyphs, page skeletons that keep the
layout during loading, and leave-type colours moved out of TS into
CSS variables. Feature blocks keep their structure for later phases.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Onboarding asks explicitly for one or two parents and shows one card per
parent for balances; the balance form is per parent with inline
validation; the partner can be filled from the FK deduction or added
from it. Settings and Dashboard reuse the same form.
CSS: bare buttons carry no colour any more; buttons, cards, choice
cards, fields, alerts, badges, eyebrow and modal chrome are defined once
in primitives.css. Media queries live only in responsive.css. A style
guard test blocks hex, !important and stray media queries. Bilingual
fixes: calendar selects size by content, header no longer wraps. Vitest
is capped at four workers.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The interface had drifted into two of everything. The calendar and its
popovers were built around a blue accent the rest of the app had long since
replaced with green, five different sizes served the same class of small
label, and page sections were a stack of identical bordered cards that gave
no sense of what mattered most. Sections now separate by space and a hairline,
the overview leads with its headline figure at a size that says so, and the
palette, type scale and spacing live in one place as variables rather than
scattered literals.
Two things were not styled at all. The sign-in screen was an unstyled form in
the top-left corner, and the tip cards had no rules whatsoever despite their
markup already carrying a severity, so every tip rendered identically with a
plain green square for its dismiss control.
The calendar was also unusable on a narrow screen: the preset rail never
collapsed and covered the last three columns of the grid.
A partner added during onboarding appeared as "Parent 2" on the overview
until you navigated away and back: the members list had been fetched before
that partner existed and nothing refetched when the wizard finished. The
fallback was doing its job; it was covering for stale data.
A period drawn on the calendar is measured in days, but a shortfall was
reported in the weeks the engine had rounded it to, so seven days came back
as three weeks. It now reports what was actually asked for.
Registering used to land you on a bare form asking for a child, with no hint
what came next. A short wizard now walks through the child, whether you are
planning alone or together, and where the day balances come from: fetched from
Försäkringskassan, entered by hand, or skipped — and if skipped, the figures
carry a badge saying they are estimates until real ones arrive.
Fetching is the first option rather than a hidden one, so the scraper is on by
default and the script that used to force it on no longer needs to.
Planning and Tips lose their tabs. Authoring moved to the calendar and the
figures moved to the overview, so both pages were only a second way to reach
what is already on screen; their routes now redirect rather than dead-end. The
dead translations went with them.
Dates also follow the chosen language now instead of always formatting as
Swedish.
The page opened with generic tiles and a bar reading forty-eight of ninety-six
weeks, which was neither weeks nor connected to the age-four ceiling it appeared
to describe. It is replaced by one headline and a bar per parent, in days,
against each parent's own savable limit as the server reports it.
The headline has two framings on the same arithmetic. Before anything is
planned it says what there is to plan and what will be lost without a plan,
because on the first day almost the whole entitlement is technically at risk
and saying so bluntly reads as an accusation. Once a plan exists it warns about
the days genuinely at stake.
A timeline of the years to the fourth birthday answers what the next four years
look like, which a month-at-a-time calendar cannot. Shortfalls, surplus advice
and balances sit below the fold, read-only: applying is automatic now, so a
second apply button would be a way to get two different answers.
Rejection messages move to one shared place that resolves a parent's name, so
an identifier can no longer surface in their place.
The Dashboard summed both parents' expiring days into one figure while
the Plan tab already showed them separately. Each parent's own count is
now shown alongside the combined total, which stays and is relabelled as
a sum.
The per-parent numbers were already on the wire as
parentProjections[].expiringAtB4 -- no backend change, and no arithmetic
here. Both figures are rendered exactly as the backend computed them,
since re-deriving them is what previously doubled a parent's entitlement
(87b3eb5).
A single-parent workspace shows only the combined figure, where a
breakdown would just repeat it.
nameResolver moves out of PlanResults.tsx into src/utils/parentNames.ts
so both screens resolve uuid -> name the same way. PlanResults.test.tsx
passes untouched, which is the evidence the extraction preserved
behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
Works through every numbered item in NextSteps260913.md section 3, plus the
three known verification issues in section 4.1.
Surface what the backend already computed:
- 3.1 render surplus_advice in PlanResults, mirroring the shortfall remedies
- 3.2 surface VacationBalance.is_assumed so assumed statutory years are visible
- 3.3 age-4 expiry given the proposed plan, via a new Age4Projection reusing
the existing _age4_projections arithmetic (Dashboard stat verified, not rebuilt)
Calendar cluster (one pass over the holiday data):
- new GET /api/calendar/days exposing roda dagar, klamdagar and day overrides,
paginated four years at a time
- 3.4 shade roda dagar/klamdagar, with R/K badges so colour is not the only signal
- 3.6 click-to-override behind an explicit mode toggle, leaving drag multi-select intact
- 3.7 click month/year to advance
- 3.8 per-year red-days list in Settings, statutory days plus overrides
i18n:
- 3.5 five backend-generated Swedish surfaces converted to message_key + data,
following the tips_service pattern; old string fields replaced, not kept
Flexible wishes (3.9):
- PLANNING_ENGINE_SPEC section 10: window_mode, candidate search, cost function,
deterministic tie-break, two-pass ordering, test matrix rows 15-24
- backend: entity, migration 006, _FULL_SCHEMA sync, repository, allocator
- frontend: season-preset toggle in WishEditor, unplaced outcome rendering
Fixes (4.1):
- pin typecheck to the local tsc rather than npx resolving a newer TypeScript
- ignore generated coverage/ and dist/ in the ESLint flat config
- fix the flaky usePlanApply test properly, with waitFor rather than a retry
Tests: backend 361 -> 404, frontend 540 -> 704. Coverage 89.47% -> 96.52% lines.
Not yet verified in a browser.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
Implemented 4 page stubs with hooks and components:
- Dashboard: displays overview metrics, planned days breakdown, progress chart, break periods
- Calendar: month-based calendar grid with event badges and navigation
- Tips: list of optimization tips with dismiss functionality
- Settings: workspace configuration with child info, parent profile, vacation days, break periods
Added utility exports:
- src/context/index.ts: re-exports useAuth, useWorkspace hooks
- src/hooks/index.ts: re-exports all domain hooks
All pages:
- Use Layout wrapper with header configuration
- Load data via hooks with loading/error states
- Render presentational components (no business logic)
- Pass callbacks for user actions
- Type-safe with full @types coverage
Frontend test coverage: 167 tests, all passing
TypeScript: zero errors, lint: zero errors
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>