782415122d
The calendar mixed two kinds of truth in one table. `planned_leave_day` was simultaneously the allocator's output cache, the user's override store (`is_pinned`, plus ad-hoc rows with no `wish_id`), and the resource ledger that analytics and the age-4 maths read. The planner therefore had to ask "am I allowed to touch this row?" on every sync, and got it wrong at the edges. Every user assertion is now one `day_assertion` row — a concrete `day_type` replaces a pinned day, a row with no `wish_id` replaces an ad-hoc day, and `day_type='none'` replaces `planned_day_exclusion`. `planned_leave_day` becomes purely derived with a single writer, and `PlanningService.pinned_keys()` is the single answer to "is this day pinned", used by the allocator's loader, by `sync_plan` and by the read router alike. On top of that, every calendar band is now a real event: an ad-hoc band opens the same full editor as a wish-backed one and saving it promotes it to a `LeaveWish`, so it can be named, repeated yearly, assigned to either or both parents, and handed to the planner. "Planering" is a `moveable` toggle plus a from/to month bound plus a separate lock/unlock action on the days themselves, replacing a three-rung ladder whose "planner decides" rung set a search window equal to the drawn dates and so could never move anything. Bugs found and fixed along the way: - The plan erased its own history. Fixed windows clip to today, so an elapsed auto-placed day fell into `stale_keys` and was deleted on the next sync — and a background sync runs on every workspace load. Past rows are now frozen and counted as spent. - Migration 010's downgrade silently destroyed every pinned day and exclusion (`is_pinned` returned as DEFAULT 0, the exclusion table came back empty, exit 0, no warning). It now reconstructs both from `day_assertion`, and 009 itemises what it cannot recover before dropping the table. - Deleting a period left its assertions behind, locking those dates forever with no UI path to clear them. Wish deletion now clears them by `wish_id`. - `DayAssertionService` and `LeaveWishService` committed multi-statement writes without a transaction; a crash mid-write diverged the tables or half-deleted a series. Both now run in a unit of work. - A sibling year's shortfall was never surfaced: the toast only inspected the edited occurrence's outcome, so a fully unfunded later year rendered as an empty band with no warning. - A flexible window whose bound had entirely elapsed was dropped from the proposal with no outcome at all. It now reports as `unplaced`. - Unlocking a period deleted its days instead of returning them to the planner. - The e2e global setup posted to a route deleted in this change, which would have failed the whole suite before its first test. Gates: backend 938 tests / 91% coverage, frontend 1351 / 97.4 | 92.2 | 94.37 | 97.4. Migrations 009 and 010 audited against a hand-built realistic database: the upgrade reconciles exactly and a constraint violation fails safe, leaving the schema at 008. Not complete. Step 4's toggle, month picker and lock/unlock action are verified by browser walk and unit tests, but the partial-lock readout was not exercised. `ideas.md` §7 adds a full plan for the vacation-forfeiture defect (the planner detects expiring days and then does nothing about them) — not implemented here. Remaining smaller items are listed in the session's restructure notes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T4wjgbVL4HAC5Rdaw75VbV
93 lines
4.9 KiB
Markdown
93 lines
4.9 KiB
Markdown
# Ideas
|
|
|
|
Opinionated proposals from agents while working on the App. Agents are allowed to append ideas that they find.
|
|
|
|
---
|
|
|
|
## 1. Multi-child is half-built
|
|
|
|
The tips engine now runs per child. The rest does not really. `TipsContext` was single-child until
|
|
a fix two commits ago; the dashboard shows one child at a time via a header selector; the age-4
|
|
maths is per `(parent, child)` but nothing shows two children's deadlines side by side.
|
|
|
|
A second child is the common case for this product's users, and their windows overlap in ways worth
|
|
seeing together — days spent on one child are days not spent on the other, and two fourth
|
|
birthdays mean two cliffs.
|
|
|
|
**Suggestion:** decide whether multi-child is a supported scenario. If yes, the timeline on
|
|
Översikt is the natural place to show both; if no, say so in the UI rather than half-supporting it.
|
|
|
|
## 2. The planner cannot explain itself --> in progress
|
|
|
|
Every funded day already carries a `reason` code (`vacation_wrap_red_day`, `fp_interior`,
|
|
`swap_first_half`, `dubbeldag`, …) and every window produces a `WindowOutcome` with a cost
|
|
breakdown and a strategy. **None of it reaches the user.** The calendar shows what was decided, not
|
|
why.
|
|
|
|
"Why is this a vacation day and not a parental-leave day?" is the question this product exists to
|
|
answer, and the data is already on the wire.
|
|
|
|
**Suggestion:** surface the reason in the day popover — one line, plain Swedish. Cheapest
|
|
high-value change on this list.
|
|
|
|
## 3. Flexible periods can move without warning
|
|
|
|
A flexible wish is re-placed on every sync, and sync now runs on every edit. A summer period can
|
|
therefore shift because you edited something in December. The change toast reports it, but a toast
|
|
is ephemeral and the user may not connect cause to effect.
|
|
|
|
**Suggestion:** mark a flexible period visually as "placed by the planner, may move", and offer a
|
|
one-click pin to fix it where it is. The machinery exists — `is_pinned` already protects days.
|
|
|
|
## 4. Dismissed tips could hide a growing problem
|
|
|
|
Dismissals expire when the tip's `data` changes, which is the right instinct. But `preserve_for_age_4`
|
|
is a warning whose numbers change slowly and monotonically — dismiss it in year two and it may not
|
|
resurface until the figure has moved materially, by which point less can be done.
|
|
|
|
**Suggestion:** exempt the age-4 warnings from dismissal entirely, or make them re-surface on a
|
|
calendar cadence rather than only on data change.
|
|
|
|
## 5. Vacation is modelled more thinly than parental leave
|
|
|
|
Parental-leave days have tiers, reserved portions, transferability, dubbeldagar and an age-4
|
|
ceiling. Vacation has `total_days` and `saved_days_in` per year, plus a statutory floor on what
|
|
must be taken. Real Swedish vacation has more shape than that — collectively agreed extra days,
|
|
employer rules about carry-over beyond the statutory five years, and the distinction between
|
|
earned and advance days.
|
|
|
|
Not obviously worth building. Worth knowing that vacation is the weaker half of the model, given
|
|
the app's premise is optimising the two *together*.
|
|
|
|
## 6. Smaller things
|
|
|
|
- **`ParentFacts.name` is dead.** Carried through the allocator, never read. Removing it churns
|
|
fixtures across 580+ tests, which is why it survived a cleanup sweep. Do it opportunistically.
|
|
- **`isCreatingPeriod` has no test.** It gates the preset rail's busy state and drag re-entry, and
|
|
is only incidentally exercised. A merge with `syncHook.isSyncing` looks right but has no safety
|
|
net, so it was left alone.
|
|
- **`ProposedDayApplyItem` and `makeProposedDayApplyItem`** are dead on the frontend — leftovers of
|
|
the removed direct-apply path.
|
|
- **E2E is flaky in parallel.** All specs share one demo workspace and race on auto-sync. CI pins
|
|
`workers: 1`, so this only bites locally. Fixing it properly means per-spec workspaces.
|
|
- **Promote the E2E job to blocking** once it has a run of green builds. It is deliberately
|
|
non-blocking today.
|
|
- **`total_days_per_child` is now computed**, which forecloses an env override for multiples. The
|
|
assumption is that Swedish multiples rules add days at the sjukpenning tier, so the derivation
|
|
still holds — worth confirming before anyone builds twins support.
|
|
|
|
---
|
|
|
|
## Deliberately not proposed
|
|
|
|
- **Replacing the hand-rolled fetch hooks** with a generic factory or a query library. The ~14
|
|
hooks look repetitive but differ in load-bearing ways (background-refresh flags, window
|
|
parameters, differing refetch semantics). A generic version would need enough escape hatches to
|
|
cost more than it saves.
|
|
- **Unifying the two band-building functions** (`CalendarWeek.computeBandSegments`,
|
|
`YearTimeline.buildBands`). Same word, different problems: one groups by wish identity with
|
|
gap-bridging and lane assignment at week granularity, the other by day-type majority at year
|
|
granularity.
|
|
- **Collapsing `/propose` and `/apply` into `/sync`.** They are the pure, independently testable
|
|
primitives underneath it. Keeping them is what makes the allocator exercisable without a database.
|