# Bug: Dashboard figures swing on the first plan sync **Status:** open, not fixed. Found 2026-09-20 during browser verification of the vacation-rescue feature. **Pre-existing** — not introduced by that work. ## Symptom The vacation figures on the Dashboard change substantially the first time `POST /api/plan/sync` runs, with **zero user interaction** — an ordinary page reload is enough. Measured on a freshly seeded demo workspace (`backend/scripts/seed_demo.py`), 2026-09-20: | Figure | Before sync | After sync | |---|---|---| | Total "vacation at risk of being lost" | 178 | 24 | | Sam 2026 — vacation left | 23 | 17 | | Sam 2026 — expiring | 18 | 12 | | Sam 2027 — expiring | 20 | 2 | | Sam 2028 — expiring | 20 | 3 | ## Cause Two different numbers are both correct, for different questions, and the UI shows them in sequence without saying which is which: 1. `usePlanProposal` (`frontend/src/hooks/usePlanProposal.ts`) initially renders the result of `POST /api/plan/propose` — a **read-only** projection that writes nothing. 2. `PlanSyncProvider` (`frontend/src/context/PlanSyncProvider.tsx`) then runs `POST /api/plan/sync`, which **persists** the proposed days via `PlanningService.sync_plan` (`backend/app/services/planning_service.py`). 3. Once those days exist in `planned_leave_day`, `AllocationInputLoader` counts them as committed usage when it builds `vacation_forfeiting`, so the next forecast is much smaller. 4. `usePlanProposal` permanently prefers `PlanSyncProvider.latestProposal` once any sync has completed, so the post-sync figure is what sticks. So the pre-sync figure answers *"what would be lost if nothing were planned?"* and the post-sync figure answers *"what will be lost given the plan we just wrote?"*. Only the second is what the user means by "days I'm about to lose". ## Why it matters It reads as a bug in whatever the user just did. During verification, the vacation-rescue **dismiss** action was initially blamed for it — dismiss was the first Dashboard control to call `bumpPlanVersion()`, so it triggered the sync and appeared to move the numbers. Dismiss was **exonerated** by capturing figures immediately before and after the click: with dismiss no longer bumping the plan version, the figures are byte-identical across a dismiss (178 / 23 / 18 before and after). The swing only ever came from the sync. ## Suggested direction (not decided) Options, roughly in increasing order of effort: 1. Don't render vacation figures until the first sync has completed — show the existing loading skeleton instead. Smallest change; makes the transient state invisible rather than explained. 2. Have the Dashboard sync before first paint rather than propose-then-sync, so only one number is ever shown. 3. Reconsider whether `sync_plan` should write on a plain page load at all. It is a persisting write triggered by navigation, which is what makes a read of the Dashboard change stored data. Option 3 is the real question and is the largest; 1 is a stopgap that would stop the user seeing a figure that is about to change by 7x. ## Reproduction 1. Seed a fresh demo workspace and log in as `demo@example.com`. 2. Open the Dashboard and read the "vacation at risk" total **immediately**, before the background sync completes (it is quick — capture it fast, or throttle the network). 3. Reload the page and read it again. ## Related, probably not a real defect Chrome's network panel reports `POST /api/plan/rescue-days/dismiss` as **503** while uvicorn's own access log shows **204 No Content** for the same call, with one log line per click and no retries reaching the server. Server behaviour is verifiably correct. Looks like an artifact of the browser extension's request tracking, possibly around React 18 dev-mode effect double-invocation. Recorded only so it isn't re-investigated from scratch.