diff --git a/new-features/alignment-plan.md b/new-features/alignment-plan.md deleted file mode 100644 index daebbde..0000000 --- a/new-features/alignment-plan.md +++ /dev/null @@ -1,116 +0,0 @@ -# Align the year-row timelines in the Overview tab - -## Context - -In the Dashboard Overview tab each year is a `
` whose -`` is its **own** grid: `grid-template-columns: 3.5rem minmax(0, 1fr) auto var(--icon-sm)` -(`frontend/src/styles/components.css:225`). Because every row is an independent grid, the `auto` -figures column is sized per row by that row's own text — "· expiring 5", a wider `saved` number, -a longer parent name, a 3-digit vs 2-digit PL value. The `minmax(0, 1fr)` track column absorbs the -difference, so the timeline bars end at a different x per year (see `timeline-offset.png`: 2029's -track is clearly shortest, 2025 the longest). The figures columns are ragged for the same reason. - -Goal: every year's timeline track starts and ends at the same x, and the figure columns line up -down the list — without hardcoding a pixel width that breaks in Swedish (sv strings are longer than -en) or when a number gains a digit. - -## Approach — one shared grid via `subgrid` - -Promote the column definition from each summary to the `.year-rows` list and have the rows opt into -it with `grid-template-columns: subgrid`. Tracks are then sized once, by the widest content in the -whole list, and every row inherits identical column widths. Content still sizes the columns, so it -stays bilingual-safe. - -Split the figures cell into its three real columns (name | PL left | vacation group) at the list -level too, so the numbers align vertically across years as well. - -**CSS only — no TSX changes.** The existing markup already has the right elements -(`.year-rows` → `.year-row` (`details`) → `.year-row__summary` / `.year-row__body`, -`.year-row__figures` → `.year-row__parent { display: contents }`). - -### Files - -- `frontend/src/styles/components.css` — `.year-rows` (162), `.year-row__summary` (225), - `.year-row__summary::after` (238), `.year-row__year` (244), `.year-row__track` (252), - `.year-row__figures` (289), `.year-row__body` (322) -- `frontend/src/styles/responsive.css` — mobile block at 93–120 (must reset subgrid) - -### Shape - -`.year-rows` becomes the grid owner: - -```css -.year-rows { - display: grid; - /* year | track | name | PL left | vacation group | chevron */ - grid-template-columns: 3.5rem minmax(0, 1fr) auto auto auto var(--icon-sm); -} -``` - -Each row spans it and re-exposes it: - -```css -.year-row { /* the
*/ - grid-column: 1 / -1; - display: grid; - grid-template-columns: subgrid; -} -.year-row__summary { - grid-column: 1 / -1; - display: grid; - grid-template-columns: subgrid; - /* keep: align-items, column-gap, row-gap, padding, font-weight */ -} -.year-row__body { grid-column: 1 / -1; } -``` - -Column placement inside the summary shifts to match the six tracks: year `1`, track `2`, -`.year-row__figures` `grid-column: 3 / 6` + `grid-template-columns: subgrid` (keep -`display: grid`, `align-items: baseline`, `white-space: nowrap`; drop its own -`auto auto auto`), chevron `::after` `grid-column: 6`. `.year-row__parent { display: contents }` -stays — each parent's three spans land in the three shared columns. - -Notes: -- `--sp-4` column-gap must be declared on `.year-rows` as well, since a subgrid inherits the - parent's gaps unless it sets its own; keep the summary's `column-gap` so the values agree. -- `.year-rows > .disclosure:last-child { border-bottom: none }` and the `.disclosure` hairlines - in `primitives.css` are unaffected (borders on the `details` still paint). -- In the solo case `.year-row__name` isn't rendered; that column collapses to `0` for every row — - still uniform. -- Left-over `grid-row: 1` declarations on year/track/figures stay valid. - -### Fallback - -Wrap the promotion in a support check so older engines keep today's (merely ragged) layout rather -than collapsing: - -```css -@supports (grid-template-columns: subgrid) { /* the rules above */ } -``` - -Keep the current `.year-row__summary` rule as the base; the `@supports` block overrides it. - -### Mobile - -`responsive.css` already re-lays the summary as `3rem minmax(0, 1fr) var(--icon-sm)` with the -figures wrapped to row 2 as a flex column. Inside that media query, reset the new rules: -`.year-rows { display: flex; flex-direction: column }` and -`.year-row { display: block }` — so the mobile block's existing declarations apply unchanged. -Place these resets after the `@supports` block in source order, or repeat them inside their own -`@supports` guard, so specificity/order lets mobile win. - -## Verification - -1. `cd frontend && npm run dev`, open the Dashboard Overview tab with the demo workspace - (`demo@example.com`) — delegate the browser pass to a Sonnet subagent (never drive Chrome from - the coordinator). -2. Check: every year's track left and right edge align to the pixel, including 2029 (shortest - figures) vs 2025 (no expiring) vs a row with `expiring`; the "PL left" and "Vacation left" - labels form straight columns. -3. Repeat in **both** Swedish and English (longer sv strings are the real stress test), and in a - solo workspace (no parent names) as well as a couple workspace. -4. Narrow to the mobile breakpoint and confirm figures still wrap under the track, one parent per - line (use the same-origin iframe trick — `resize_window` is a no-op in this setup). -5. Gates (delegate to a Haiku subagent, raw output): from `frontend/` `npm run typecheck`, - `npm run lint`, `npm run coverage`. CSS-only change — `YearRows.test.tsx` and `Dashboard.test.tsx` - should be untouched and green. diff --git a/new-features/fix-deprecations-backend-plan.md b/new-features/fix-deprecations-backend-plan.md deleted file mode 100644 index d6c8dfb..0000000 --- a/new-features/fix-deprecations-backend-plan.md +++ /dev/null @@ -1,69 +0,0 @@ -# Fix backend deprecation warnings - -## Context - -`uv run pytest` on `backend/` emits 26 `DeprecationWarning`s. They are noise in CI output and two of the four sources are real forward-compat debt in our own code — the Starlette 422 constant is already renamed upstream, and Alembic's `version_path_separator` is superseded. Goal: our code emits zero deprecation warnings. - -Confirmed inventory (Python 3.14.0, fastapi 0.136.3, starlette 1.2.1, alembic 1.18.4, slowapi 0.1.9): - -| Source | Count | Ours? | -|---|---|---| -| `HTTP_422_UNPROCESSABLE_ENTITY` → use `HTTP_422_UNPROCESSABLE_CONTENT` | 8 | yes — 20 call sites | -| `alembic/config.py:612` — `No path_separator found in configuration` | 12 | yes — `backend/alembic.ini` | -| `slowapi/extension.py:717` — `asyncio.iscoroutinefunction` | 6 | no — upstream, out of scope | - -Decision made: fix our code only. No `filterwarnings` config is added; the slowapi warning stays visible as an upstream reminder. - -**Baseline:** 4 tests already fail on `main` (`test_migration_004_backfill.py::test_flexible_yearly_wish_keeps_the_same_bounding_range_every_year`, `test_leave_wish_repository.py::…test_flexible_series_keeps_one_bounding_range_per_year_unshifted`, two in `test_allocation_service.py`). These come from the uncommitted in-flight work in `app/entities/leave_wish.py` / `app/services/allocation_service.py` and are **unrelated** to this change. Expected result after the change: still exactly those 4, no more. - -## Change 1 — rename the 422 constant (20 sites, 15 files) - -Mechanical rename, same numeric value (422), so no wire-format or frontend impact: - -``` -status.HTTP_422_UNPROCESSABLE_ENTITY → status.HTTP_422_UNPROCESSABLE_CONTENT -``` - -All sites are in `backend/app/routers/`. Representative files: `workspace.py` (3 sites, lines 51/94/223 — inside conditional expressions, not plain raises), `settings.py` (3), `fetch_data.py` (2), `leave_wishes.py` (2), `plan.py` (2), `preschool_planning_days.py` (2), plus one each in `auth.py`, `calendar.py`, `day_overrides.py`, `planned_day_exclusions.py`, `planned_days.py`, `vacation_days.py`. - -Notes: -- `status` is already imported from `fastapi` in every one of these files — no import changes. -- `status.HTTP_422_UNPROCESSABLE_CONTENT` is verified present in the installed starlette (evaluates to `422`). -- No test file references the constant by name (`git grep UNPROCESSABLE -- tests` is empty), so tests need no update. -- The warnings attributed to `fastapi/routing.py:328` are our handler frames raising through the dependency call — they disappear with this rename too. - -## Change 2 — Alembic `path_separator` - -`backend/alembic.ini` currently has: - -```ini -prepend_sys_path = . -version_path_separator = os -``` - -Replace `version_path_separator = os` with `path_separator = os`. In Alembic ≥1.16 `path_separator` governs both `prepend_sys_path` and `version_locations`, and its absence is what triggers the legacy-splitting warning. Leave `prepend_sys_path = .` and the annotated `sqlalchemy.url` fallback untouched. - -## Out of scope - -- slowapi's `asyncio.iscoroutinefunction` warning — upstream, no released fix. -- `typing.Dict` in `app/utils/holidays.py` and `datetime.timezone.utc` in `app/services/auth_service.py` — older spellings, but not deprecated and not warning sources. -- The 4 pre-existing test failures. - -## Verification - -Delegate to a **Haiku** subagent (per the cost rule); require raw verbatim output, from `backend/`: - -```bash -uv run ruff check . -uv run ruff format --check . -uv run pytest -uv run alembic upgrade head # exercises the alembic.ini change directly -``` - -Acceptance criteria: -1. The warnings summary contains **only** the 6 `slowapi/extension.py:717` lines — no `UNPROCESSABLE_ENTITY`, no `path_separator`, count down from 26 to 6. -2. Failure list is exactly the 4 baseline failures above; pass count unchanged at 860. -3. Coverage still ≥ the `--cov-fail-under=80` gate (no gate changes). -4. `alembic upgrade head` completes without the `path_separator` warning. - -Then `graphify update .`. diff --git a/new-features/fix-number-drop-redundant-preschool-field-plan.md b/new-features/fix-number-drop-redundant-preschool-field-plan.md deleted file mode 100644 index 57432f5..0000000 --- a/new-features/fix-number-drop-redundant-preschool-field-plan.md +++ /dev/null @@ -1,154 +0,0 @@ -# Fix number-input clearing UX + drop the redundant preschool estimate field - -## Context - -Two problems in Settings, both reported by the user: - -1. **Clearing a number input traps you at `0`.** The per-year "Planned days" input - (`Settings.tsx:186-192`) keeps its draft as a `number`, and `Number('') === 0`, so - blanking the box writes a literal `0` to the server and snaps the display to `0`. - Typing `5` then produces `05`. The same hazard exists in `VacationDaysTable` and - `PeriodPopover`. `VacationDaysInput` has the mirror bug: it silently rejects a - non-numeric value, so the field can't be blanked to retype at all. - -2. **Two preschool controls look duplicated.** "Preschool planning days per year" - (the child-level estimate, `Child.preschoolPlanningDaysPerYear`) sits directly above - the "Planning days per year" heading for the per-year rows. They are not duplicates — - the estimate seeds each year row and is the allocator's fallback for years with no - saved row (`backend/app/services/allocation_service.py:545-573`, `:2985-3001`) — but - the UI gives no hint of that, so it reads as dead weight. - -**Decisions taken:** remove the estimate *input* from the UI while keeping the backend -field (default `6`) as the seed/fallback; extract one shared number input and roll it out -to every numeric field that still has the `Number('') === 0` hazard. - -**Out of scope (flagged, not fixed):** `backend/app/services/tips_service.py:579` passes -the *global* `settings.rules.preschool_planning_days_per_year` into -`preschool_buffer_needed` instead of the child's own value, so a customised per-child -value is ignored by that tip. Separate bug, separate change. - -## 1. New shared component: `frontend/src/components/inputs/NumberField.tsx` - -Lift the working pattern already used by the "Days taken" input (`Settings.tsx:199-217`). - -```tsx -export interface NumberFieldProps { - value: number - onChange: (value: number) => void - label?: string - min?: number - max?: number - id?: string - className?: string - inputClassName?: string - disabled?: boolean - placeholder?: string - 'data-testid'?: string - 'aria-invalid'?: boolean - 'aria-describedby'?: string -} -``` - -Behaviour contract: - -- Holds an internal **string** draft. `''` is a legal transient state. -- `onChange` fires only when the draft parses to a finite number — **never on empty**, so - the previously saved value survives while the box is blank. -- No clamping while typing (clamping mid-keystroke is what makes small `max` values - unusable). Clamp to `[min, max]` on blur only, and commit the clamped value. -- On blur with an empty or unparseable draft: revert the draft to the current `value` - prop; do not call `onChange`. -- Sync the draft from the `value` prop only while the input is **not focused** (a ref - flag set on focus/blur), so a debounced server round-trip can't overwrite live typing. -- Renders `type="number" inputMode="numeric"` with `min`/`max` attributes and the existing - `field` / `field__control` BEM classes; `label` renders as `field__label` when given. - No hardcoded `id`/`data-testid` — both are props (the bug that makes `VacationDaysInput` - non-reuse-safe today). - -Co-located test `NumberField.test.tsx` covering: clear-then-retype yields `5` not `05`; -empty draft fires no `onChange`; blur-empty restores the prior value; blur clamps to -`min`/`max`; external `value` change while focused does not clobber the draft. - -## 2. Remove the child-level estimate input - -`frontend/src/pages/Settings.tsx` - -- Delete the `VacationDaysInput` block (`:562-571`) and its import (`:21`). -- Delete the `preschoolPlanningDays` state (`:252`), its prefill in the `childDetails` - effect (`:285`), and `preschoolPlanningDaysPerYear` from the `updateChildDetails` call in - `handleSaveFamily` (`:366`). The field is optional in `ChildUpdate` - (`backend/app/models/child.py:40`), so omitting it leaves the stored value untouched. -- Pass `defaultPlannedDays={childDetails?.preschoolPlanningDaysPerYear ?? 6}` to - `PreschoolPlanningYearRow` (`:583`) instead of the removed state. -- Move the `settings.preschoolHelp` paragraph (`:563`) into the per-year block, under the - heading, and drop the now-redundant `settings.preschoolPerYearHelp` line or merge the two - into one sentence. -- Retitle the section so it names the subject: `settings.preschoolPerYearTitle` → - `"Preschool planning days"` / `"Förskolans planeringsdagar"`. - -`frontend/src/i18n/{en,sv}/settings.ts` — remove `settings.preschoolPlanningDaysLabel` -(`:83` in both), reword `settings.preschoolHelp` (`:82`) so it reads as the intro to the -year rows, and update `settings.preschoolPerYearTitle` (`:126`). - -Delete `frontend/src/components/inputs/VacationDaysInput.tsx` and -`VacationDaysInput.test.tsx` — `Settings.tsx:564` was the only usage. - -## 3. Roll `NumberField` out - -| File | Inputs | -|---|---| -| `frontend/src/pages/Settings.tsx:181-195` | per-year **Planned days** (the reported bug) | -| `frontend/src/pages/Settings.tsx:199-217` | per-year **Days taken** — keep its `null` = "not recorded" sentinel; it needs `value: number \| null` handling, so either give `NumberField` an optional `nullable` mode or leave this one as-is and note why | -| `frontend/src/components/vacation-days/VacationDaysTable.tsx:174-183, 188-197` | `totalDays`, `savedDaysIn` | -| `frontend/src/components/calendar/PeriodPopover.tsx:458-470` | `lastYear` (min 2000 / max 2200 — blanking currently shows `0`) | - -`frontend/src/components/modals/ParentBalanceForm.tsx` already handles empty correctly via -`parseField` (`:134`, `value={x ?? ''}`) — **leave it alone**, or migrate it only if -`NumberField` gains the nullable mode above. Do not regress its `null` semantics. - -Existing `data-testid`s (`preschool-planned-{year}`, `vacation-total-days-{year}`, -`period-last-year-input`, …) must be preserved so current tests keep targeting the same -elements. - -## 4. Tests to update - -- `frontend/src/pages/Settings.test.tsx:416-437` — drop the `vacation-days-field` prefill - and the "change to 10 → `updateChildDetails({ preschoolPlanningDaysPerYear: 10 })`" - assertions; assert instead that the family save no longer sends that key and that - year rows still prefill from `childDetails.preschoolPlanningDaysPerYear`. -- `frontend/src/pages/Settings.test.tsx:459-500` — add a clear-then-retype case on - `preschool-planned-{year}` asserting the committed value is `5`, and that clearing alone - does not call `setPlanned`. -- `frontend/src/hooks/useChildSettings.test.ts:100-113` — unchanged (the hook still - supports the field); no edit expected, verify it still passes. -- Backend tests unchanged: no model, service, entity or migration touched. - -Mock only with the typed factories in `frontend/tests/fixtures/` and `vi.mocked(fn)` — -no `as any`, no casts (CLAUDE.md anti-patterns). - -## Verification - -Delegate the full suites to a **Haiku** subagent, one at a time (never in parallel), with -raw verbatim output required: - -```bash -# from frontend/ -npm run typecheck -npm run lint # zero warnings -npm run coverage # gates: lines 96 | functions 91 | branches 92 | statements 96 -``` - -Backend is untouched, so a backend run is only needed if something unexpected changes there. - -Then a **Sonnet** subagent drives Chrome (never the coordinator) against `npm run dev`: - -1. Settings → Family panel: confirm the standalone "Preschool planning days per year" - input is gone and the year rows still show the seeded value with the "assumed" badge. -2. In a **Planned days** box: select-all, delete → box goes empty (not `0`); type `5` → - shows `5`, not `05`; blur → autosaves `5`. -3. Blank a box and click away without typing → the previous value returns, nothing saved. -4. Repeat 2-3 on a `VacationDaysTable` row and on the period `lastYear` input. -5. Check the reworded section in **both Swedish and English** — the labels must not clip - (per the bilingual-CSS rule). - -Finish with `graphify update .`. diff --git a/new-features/free-days-and-auto-planning-plan.md b/new-features/free-days-and-auto-planning-plan.md deleted file mode 100644 index 5686d59..0000000 --- a/new-features/free-days-and-auto-planning-plan.md +++ /dev/null @@ -1,102 +0,0 @@ -# Free days, vacation forfeiture, and auto-replanning - -## Context - -Three feedback points from `todo.md` (human-owned file — do not edit it; item 1 there, the yearly-repeat bug, is owned by another worker and is out of scope here): - -1. **A full-week selection must not burn days on Sat/Sun/red days.** You can never take *vacation* on a free day; you *can* take PL on one (more money, fewer days banked) — that must be an opt-in setting defaulting to off. -2. **Vacation days at risk of being forfeited should push the planner toward spending vacation** instead of PL days. -3. **Every event that changes the days off must trigger a re-plan** of the day *types*, not just a refetch. - -Exploration shows the backend already implements most of 1 and 2. The real gaps are: the hand-pin path bypasses the free-day invariant entirely, recalculation only fires from the Calendar page for some events, and vacation forfeiture is invisible in the UI. - -### What already exists (reuse, do not rebuild) - -| Piece | Where | State | -|---|---|---| -| `take_pl_on_red_days` workspace column + migration `003` + `PUT /api/workspace/:id` | `backend/app/entities/workspace.py`, `app/services/workspace_service.py` | done, defaults `False` | -| Settings toggle + sv/en strings | `frontend/src/pages/Settings.tsx:496`, `src/i18n/{sv,en}/settings.ts:108-110` | done | -| Allocator free-day handling (free day ⇒ `'free'`, or FP via `REASON_FP_RED_DAY` only when the flag is on; vacation **never**) | `allocation_service.py` `_fund_solo:1271`, `_fund_both:~1518` | done, tested at `tests/unit/test_services/test_allocation_service.py:2616+` | -| `is_free_day(d, holidays_map, overrides)` — the single definition | `allocation_service.py:470` | reuse everywhere | -| `vacation_forfeiting` bias in `_assign_tier3` | `allocation_service.py:1171-1225`, fed from `AnalyticsService._vacation_year_series` at `:2935` | done | -| `VacationYearBalance.expiring / expiring_age / expiring_over_cap` on the wire | `analytics_service.py:47`, `frontend/src/types/entities.ts:251` | computed, **never rendered** | -| `POST /api/plan/sync` (full re-plan + persist + diff) | `planning_service.py:662`, `usePlanSync`, `Calendar.tsx` `runSync:365` | done | -| `WorkspaceContext.planVersion` / `bumpPlanVersion()` cross-instance invalidation | `frontend/src/context/WorkspaceContext.tsx:25-36,179` | done; bumped only by `usePlannedDays`, `useDayOverrides`, `usePlannedDayExclusions` | - ---- - -## Task 1 — Free days must never consume vacation (hand-pin path) - -Selecting a range that spans Sat/Sun/red days is fine and stays fine. The planner already skips those days correctly. The hole is the **"advanced" day popover**: `Calendar.tsx` `handleSavePinnedDay:571` writes one `planned_leave_day` row per selected date, free days included, and `PlanningService.add_planned_day:241` accepts `day_type='vacation'` on a Saturday without complaint. - -**Backend (authoritative guard).** In `backend/app/services/planning_service.py`: -- Add optional `workspace_repo` + `day_override_repo` constructor deps, following the existing optional-dependency pattern of `_engine` / `_leave_wish_repo` (docstring the reason; keep `scripts/seed_demo.py` and test fixtures working unchanged when unset). -- New private helper `_reject_illegal_free_day(workspace_id, date, day_type)` used by both `add_planned_day` and `update`: build the holiday map + `DayOverrideService.get_effective_map(workspace_id)`, call `is_free_day` (import from `allocation_service`, do not re-derive), then raise `ValidationError` when - - `day_type == 'vacation'` and the day is free (never legal), or - - `day_type in ('sjukpenning','lagsta')` and the day is free and `workspace.take_pl_on_red_days` is `False`. -- Skip the check entirely when the deps are unset (`None`), so behaviour is byte-identical for callers that never wire them; `app/dependencies.py` wires the real ones. -- Error goes through `app/utils/errors.py`, message keyed so the frontend can translate. - -**Frontend.** In `Calendar.tsx` `handleSavePinnedDay`, filter the write loop by the day's `isFree` from `calendarDaysByDate` (already loaded, `CalendarDayEntry.isFree`): -- `vacation` → skip free days always. -- `sjukpenning`/`lagsta` → skip free days unless `workspace.takePlOnRedDays`. -- If every selected date was skipped, show the existing modal error rather than silently doing nothing. -- `PlannedDayModal` shows a one-line note when the selection contains free days that will be left as days off, with a pointer to the setting. New sv/en keys in `src/i18n/{sv,en}/calendar.ts`. - -**Tests.** Backend: `add_planned_day`/`update` reject vacation on a Saturday and on a red day; reject PL on a free day with the flag off; accept PL on a free day with it on. Frontend: pinning a Mon–Sun range with `vacation` creates 5 rows, not 7; with `takePlOnRedDays: true` and `sjukpenning`, creates 7. - ---- - -## Task 2 — Surface vacation days at risk - -The numbers are already on the wire and unused. - -- `frontend/src/components/vacation-days/VacationDaysTable.tsx` — add an "at risk" column rendering `VacationYearBalance.expiring`, with a danger style when `> 0` and a tooltip/help line splitting `expiringAge` (too old to carry) from `expiringOverCap` (above the carry-forward ceiling). Follow `Age4Bars.tsx` / `KeyFigures.tsx` for the existing danger-ink pattern; size text by content per the bilingual CSS rule. -- `frontend/src/components/dashboard/KeyFigures.tsx` — a tile mirroring the existing `dashboard-figure-expiring` for vacation, so the reason the plan shifted toward vacation days is visible. -- `PlanWarning` kind `'vacation_expiring'` already exists; confirm it renders where `days_lost_at_age_4` does and add the missing sv/en strings if not. -- Tests: table shows the column and the danger state; zero-expiring renders the neutral state. - -No backend change — `_vacation_year_series` and the `_assign_tier3` bias stay as they are. - ---- - -## Task 3 — Re-plan on every plan-input change (global) - -Today `runSync` is Calendar-only and fires once per workspace on mount plus after wish/preset/exclusion edits. Pinned-day saves and deletes, and every Settings change, do not re-plan. - -**Move the trigger into the workspace layer** so it is route-independent: - -- New `frontend/src/context/PlanSyncProvider.tsx` (mounted inside `WorkspaceProvider`, or folded into it — implementer's call, keep `WorkspaceContext`'s surface stable): a `useEffect` on `[workspaceId, planVersion]` that calls `api.syncPlan(workspaceId)` **silently**, guarded by an in-flight ref so overlapping bumps coalesce into one trailing run, and by a "skip the very first render if Calendar is about to sync anyway" check is *not* needed — a duplicate silent sync is idempotent and cheap, but the in-flight guard must still prevent a stampede. - - Cycle safety: `bumpPlanVersion` is never called from a fetch effect (`usePlannedDays.ts:84-93` documents this), and `syncPlan` does not bump. Verified no loop. - - On an `eviction_preview` coming back (nothing written, needs a user choice), the silent path must **not** discard it — surface it through context so Calendar can prompt on next visit, or at minimum leave the plan untouched and log through `src/services/errors.ts`. Never swallow it into an empty catch. -- **Add the missing bumps** so every plan input participates. `useVacationDays.upsert`, `usePreschoolPlanningDays` (after its 800 ms debounce settles), `useChildSettings.update`, `api.updateParentSettings`' caller in `Settings.tsx`, and `handleToggleTakePlOnRedDays` all call `bumpPlanVersion()` on success — they currently do not. `usePlannedDays`, `useDayOverrides`, `usePlannedDayExclusions` already do. -- **Calendar keeps its explicit `runSync` calls** for the paths that need a toast + undo (wish create/edit/delete, presets, exclusions). Those write via `useLeaveWishes`, which does *not* bump, so there is no double sync. Replace the `didAutoSyncRef` mount-once effect with reliance on the provider, or keep it — it becomes redundant but harmless; prefer removing it to keep one trigger. -- `handleSavePinnedDay` / `handleDeletePlannedDay` need no new call: they write through `usePlannedDays`, which bumps, which now drives the global sync. This is what makes item 2's forfeiture bias actually retype existing unpinned days after a calendar edit. - -**Known limit to state plainly in the UI, not silently:** a *pinned* day is excluded from re-planning by design (`sync_plan` passes `only_pinned_existing=True`). The forfeiture bias can therefore never retype a hand-pinned day. The popover note from Task 1 is the right place to say pinning opts that day out of optimization. - -**Tests.** Frontend: a `bumpPlanVersion()` from a non-Calendar hook triggers exactly one `api.syncPlan`; two rapid bumps coalesce to one trailing call; a failing sync does not wedge the in-flight ref. - ---- - -## Verification - -Delegate all full-suite runs to a **Haiku** subagent (per AGENTS.md), serialized — one suite at a time, never parallel: - -```bash -# backend/ -uv run ruff check . && uv run ruff format --check . && uv run pytest -# frontend/ -npm run typecheck && npm run lint && npm run coverage -``` -Require raw verbatim output with exact pass/fail counts and coverage percentages; gates are backend `--cov-fail-under=80` and frontend lines 96 / functions 91 / branches 92 / statements 96. Never lower a gate. - -New migration is **not** needed — no schema change. - -End-to-end, delegated to a **Sonnet** subagent driving Chrome (never the coordinator), on `npm run dev`, checked in both Swedish and English: -1. Select Mon–Sun in the calendar, "advanced" → vacation. Assert 5 planned days, Sat/Sun untouched. -2. Settings → tick "Ta ut föräldrapenning på röda dagar" → return to calendar, repeat with `sjukpenning`. Assert 7 days. -3. Settings → edit a vacation-days row → assert a background `POST /api/plan/sync` fires (network panel) without visiting the Calendar page. -4. Enter a vacation balance that forfeits days → assert the at-risk column shows it and the plan's day mix shifts toward vacation. - -Then `graphify update .`. diff --git a/todo.md b/todo.md index 73bf248..231a3ea 100644 --- a/todo.md +++ b/todo.md @@ -1,42 +1,12 @@ ## NOT ALLOWED TO TOUCH THIS; ONLY HUMANS CAN EDIT THIS FILE -- Bug in the planning when it repeats yearly: It is taking the exact same dates, but the way you usually take time off is week based, so 5th-11th of october 2026 is a monday through sunday, but for 2027 it looks like I'll take time off from tuesday to the following week's monday, which is not what I will do. Instead for 2027 I will probably take 4th to 10th instead. - -> DONE - - -Following three have a plan (free-days-and-auto-planning-plan.md): - - Please just verify that if I say that I take a full week time-off, including saturday and sunday, that I will NOT actually take any vacation or PL during those days. Or rather: This should be a setting: Take PL during red days yes/no. And it should default to NO. It is impossible to take vacation during red days (and saturdays), but you can actually take PL to get more money, but you will of course loose days. - - - If we have vacation days that are in risk of being lost, then the planner should run to update the kind of days used in the plan to use more vacation days (thus saving PL days). - - - On every event that updates the days off (i.e. the calendar is making some changes), that should trigger the planner to recalculate the best kinds of time-off for all days. - - -> In progress, super long time..... - - -- Problem with the yearly timelines in general tab - -> alignment-plan.md -> DONE - ## Below here is taken from another markdown file, so should probaby be filtered and removed + ## High prio -- If we have vacation days that are in risk of being lost, then the planner should run to update the kind of days used in the plan to use more vacation days (thus saving PL days). --> have a plan I think - -- On every event that updates the days off (i.e. the calendar is making some changes), that should trigger the planner to recalculate the best kinds of time-off for all days. --> have a plan I think - -- Use fabel to go over the frontend to analyse what can be extracted into proper components and reused that today might be implemented multiple times: "Component Extraction" - -## medium prio -- Update all deprecated things in the backend code. --> fix-deprecations-backend-plan.md -> DONE - ## lowest prio -- check if the security audit findings have been done --> yet to have a plan +- Also noted, not fixed: WindowOutcome in frontend/src/services/api.ts is missing requested\_strategy, strategy\_degraded, +dubbeldagar\_legally\_available, and RawPlanAge4Projection is missing savable\_cap\_at\_b4 — all sent by the backend today. +There's no frontend counterpart to the parity test, so they fell behind silently. -- Also noted, not fixed: WindowOutcome in frontend/src/services/api.ts is missing requested_strategy, strategy_degraded, - dubbeldagar_legally_available, and RawPlanAge4Projection is missing savable_cap_at_b4 — all sent by the backend today. - There's no frontend counterpart to the parity test, so they fell behind silently. \ No newline at end of file