Clicking a planned day raises the calendar toast with a plain-language
reason (sv + en) taken from the live in-memory proposal; the backend
field-parity walk now covers ProposedDay.
Conflicts in Calendar.tsx / Calendar.test.tsx against the day-assertions
restructure and the two earlier merges; both intents kept:
- usePlannedDayExclusions stays replaced by useDayAssertions; the branch's
usePlanProposal watch, reasonProposal state, reasonByDay map and the
handleBandClick reason toast are added alongside main's band logic.
- runSync keeps the security branch's background refresh and also records
the sync's proposal for the reason toast. main's applyWishSyncResult
(every wish save on this page) records result.sync.proposal the same way,
so an edit's fresh reasons reach the toast — the stale-response guards
in usePlanSync/usePlanProposal only gate hook state, never the returned
response the reason rides in on.
- One main test ("no success toast when sync-with-wish throws") now
dismisses the click's own reason toast first; its assertion is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4wjgbVL4HAC5Rdaw75VbV
Backend — Föräldraledigplannerare
FastAPI + Python 3.14, packages managed with uv. Layered architecture
(entities → repositories → services → routers) with dependency injection.
Postgres is a partial stub, not a working swap today: only
repositories_impl/postgres/vacation_days_repository.py exists; every other
app/dependencies.py repository factory raises ValueError for any
database_type other than "sqlite". SQLite is the only usable backend
until the remaining Postgres repositories land.
Setup
uv sync # install deps (incl. dev group)
# --reload-exclude keeps the dev SQLite file (and its -wal/-shm/-journal
# siblings) out of the watched tree — a write-heavy request otherwise
# self-triggers a reload mid-request and drops the connection, returning a
# 503 *after* the write already committed (see frontend/package.json's
# dev:backend/login:backend for the same flags).
uv run uvicorn app.main:app --reload --reload-exclude '*.db' --reload-exclude '*.db-shm' --reload-exclude '*.db-wal' --reload-exclude '*.db-journal' # run dev server on :8000
uv run pytest # run tests with coverage
uv run ruff check . # lint
uv run pytest alone does not enforce a coverage floor (pyproject.toml's
addopts only requests a report). CI additionally passes
--cov-fail-under=80 (see the root CLAUDE.md) — a locally green run can
still fail in CI if coverage dropped.
Configuration
Env vars (prefix APP_, nested via __; see .env.example). Settings
lives in app/config.py — that file's module docstring mentions a
HMAC_PEPPER setting; no such setting exists, so do not look for it here
or set it expecting an effect.
APP_ENV—development(default) |test|production. In production,Settingsrefuses to start unlessAPP_JWT_SECRETis a random value of at least 32 characters (the shipped dev default is rejected). Production also disables/docs,/redoc, and/openapi.json.APP_DATABASE_TYPE—sqlite(default) |postgres(not usable yet — see above).APP_SQLITE_PATH— default./foraldraledig.db.APP_CORS_ORIGINS— JSON list; default["http://localhost:5173"].APP_JWT_SECRET— required to be a strong, random secret in production.APP_JWT_ALGORITHM— defaultHS256.APP_JWT_ACCESS_TOKEN_EXPIRE_MINUTES— default15.APP_JWT_REFRESH_TOKEN_EXPIRE_DAYS— default7.APP_LOG_LEVEL— defaultINFO.APP_SCRAPER_ENABLED— defaults totrue. Legal/ToS review of FK scraping + BankID automation completed 2026-06; the scraper is cleared for use. Set tofalsein any environment without Playwright's Chromium installed (uv run playwright install chromium).APP_SCRAPER_HEADLESS— defaulttrue.APP_SCRAPER_SESSION_TTL_SECONDS— default180; auto-cancels a stalled scraper session.APP_SCRAPER_DEBUG_ENABLED— defaultfalse. When enabled, writes personal data to disk (an HTML dump + full-page screenshot of the logged-in FK page — name, balances, payout amounts). RequiresAPP_SCRAPER_DEBUG_DIRto also be set. Local debugging only; never enable in a shared or production environment.APP_SCRAPER_DEBUG_DIR— where debug artifacts are written when the above is enabled.APP_SCRAPER_DEBUG_DUMP_ON_SUCCESS— defaultfalse. Also writes the same debug artifacts (HTML + full-page screenshot) after a successful extraction, not just on error — a capture aid for confirming FK's real DOM (e.g. the family panel structureapp/scrapers/fk_scraper.pycurrently assumes). Same personal-data warning asAPP_SCRAPER_DEBUG_ENABLEDapplies; only takes effect when bothAPP_SCRAPER_DEBUG_ENABLEDandAPP_SCRAPER_DEBUG_DIRare also set.APP_SCRAPER_RESULT_RETENTION_SECONDS— default300; how long a finished scraper session's result stays in memory before being purged.APP_RULES__*— the Swedish parental-leave rule constants (LeaveRules, nested underrules), overridable per field, e.g.APP_RULES__MAX_SAVABLE_DAYS_AT_AGE_4=96. Defaults: 390 sjukpenning days, 90 lägstanivå days, 90 reserved days/parent, 60 max dubbeldagar over the first 15 months, 96 max savable at age 4 (48 per parent when the child has two or more parents — seeresolve_age4_savable_cap), age-12 hard expiry, 6 preschool planning days/year, 25 statutory vacation days/year, 20 minimum vacation days/year that must be taken. These are common-case figures only — children born before 2014, multiples, and single custody differ and are not modeled.
Layout
app/
config.py # pydantic-settings (app config + frozen LeaveRules)
dependencies.py # DI factories (pick repo impl by settings.database_type)
entities/ # plain dataclasses (domain)
models/ # pydantic API schemas
repositories/ # abstract interfaces
repositories_impl/ # concrete impls per backend (sqlite/, postgres/)
services/ # business logic
routers/ # FastAPI routes
db/connection.py # connection + (dev) schema bootstrap
utils/ # errors, logger
Demo data (manual/browser testing)
scripts/seed_demo.py seeds a realistic, ready-to-explore workspace through the
normal service/repository layers (no raw SQL) — a couple workspace with a
partner, a child born ~6 months ago, vacation rows for only some years (so the
is_assumed badge is visible), day overrides, and a spread of leave wishes
(fixed, flexible, and one deliberately underfunded to show a shortfall +
remedies + surplus advice). Login credentials are fixed and printed at the end.
uv run python scripts/seed_demo.py # create once; re-running is a no-op
uv run python scripts/seed_demo.py --reset # wipe the demo workspace and rebuild
Then open http://localhost:5173 and log in with the printed demo@example.com
credentials. Safe to re-run: without --reset it detects the existing demo
user and changes nothing; --reset only ever deletes the one demo workspace.
Reference vertical slice
VacationDays is implemented end-to-end (entity → schema → repo interface →
SQLite impl → service → router → tests). Copy this slice when adding a new
entity.
The inline schema in
db/connection.pyexists only so dev/tests are self-contained. Production schema is owned by Alembic migrations (a senior task).