docs: record Sprint 9 post-deploy dismiss-bug fix across all 6 running docs

Sprint 9 (commit 6e386ba) shipped a working OnboardingTour but a
broken dismiss path: clicking X / Skip / Esc / "Got it" did
nothing. Root cause: useOnboarding().reset() was wired to the
dismiss handler at App.tsx, but reset() does the inverse of
dismiss — it clears the localStorage key and flips isComplete to
FALSE, so the tour re-rendered, the early-return did not fire,
and the dialog stayed visible. Fix: commit 1562929 split the
dismiss and reset paths into two distinct callbacks (onComplete
and onReset). User confirmed browser smoke passes.

This commit updates the 6 running docs that track Sprint 9:

- .agent/plan.md — S9.4.1 sub-task (post-deploy fix) added.
- .agent/context.md — D9 (root cause + fix) + Q4 (Vitest?) added.
- Review/sprint9-verification.md — full post-deploy fix section
  appended (root cause, fix, post-fix verification, lessons).
- Review/handoff-ui-audit.md — Sprint 9 status banner + Last
  updated footer updated to reference the fix commit.
- fix-ui-audit.md — T3.4.1 sub-task added under the T3.4
  verification gate.
- docs/HANDOFF.md — post-deploy fix paragraph added to the
  Sprint 9 section.

All 6 docs now reflect the post-deploy reality. No code changes.
This commit is contained in:
2026-06-05 14:09:10 -07:00
parent 1562929f6b
commit 4c85c929d3
6 changed files with 83 additions and 6 deletions
+55
View File
@@ -117,3 +117,58 @@ One commit: `feat(ui): Sprint 9 — F1 onboarding tour (4-step welcome)`. Files:
- `frontend/src/pages/Pantry.tsx` (2 anchors)
- `frontend/src/pages/Recipes.tsx` (anchor on Filters button)
- `frontend/src/pages/ShoppingList.tsx` (anchor on header)
---
## Post-deploy fix (2026-06-05) — tour dismiss did nothing
User reported after first deploy: clicking the X button, "Skip tour" text, "Got it" final-step button, OR pressing Esc on the dialog did nothing — the tour stayed visible and could not be exited.
### Root cause
The OnboardingTour dialog's early-return is gated on `isComplete === true`:
```ts
if (isComplete || !currentStep) return null
```
But `App.tsx` was wiring the dismiss handler (`onComplete`) to `useOnboarding().reset()`. `reset()` does the **inverse** of dismiss: it clears the localStorage key AND flips `isComplete` to `false`. So when the user clicked X:
1. `finish()` ran — `writeComplete()` wrote `"1"` to `localStorage.mealplanner:onboarding-complete`
2. `onComplete()` ran — `onboarding.reset()` cleared the key AND set `isComplete = false`
3. Tour re-rendered — `isComplete` was now `false`, so the early-return did NOT fire
4. The dialog stayed visible. Forever (until `?reset-tour=1` was visited).
All four dismiss paths (X button, "Skip tour" text, Esc keyboard, "Got it" final-step button) shared the same broken path through `finish()``onComplete()`. None of them worked.
The `?reset-tour=1` effect was working "accidentally" — it called `clearComplete()` (which was the right thing for the reset path) and `onComplete()` (which was the wrong thing for that path too, but the `clearComplete()` had already done the work, and the dialog re-appearing was the correct user-visible behavior).
### Fix (commit `1562929`)
Split the dismiss and reset paths into two distinct callbacks:
1. Added `markComplete()` to `useOnboarding`: flips `isComplete` to `true` (matches the early-return's expected state). `reset()` and `show()` are unchanged.
2. `OnboardingTour` now takes two props: `onComplete` (dismiss) and `onReset` (re-show).
3. `App.tsx` wires:
- `onComplete → onboarding.markComplete()` (X / Skip / Esc / "Got it" all hide the dialog)
- `onReset → onboarding.reset()` (`?reset-tour=1` re-shows)
4. The tour's `finish()` still calls `writeComplete()` + `onComplete()`. Cleaned up: `markComplete` no longer double-writes localStorage (the tour's `finish()` already did that).
### Post-fix verification
- `npm run build` green on `docker-willester` (495.64 kB, no size change from Sprint 9 build).
- User confirmed browser smoke on `http://100.108.208.56:8082/`:
- X button → dialog disappears, `localStorage.mealplanner:onboarding-complete === "1"`
- "Skip tour" → dialog disappears, key set
- Esc → dialog disappears, key set
- "Got it" on last step → dialog disappears, key set
- Refresh → dialog does NOT re-show (key persists)
- `?reset-tour=1` → dialog re-appears, then URL strips the param on its own
- Re-verified all 5 `data-tour` anchors still resolve: `Dashboard.tsx:602`, `Pantry.tsx:185, 208`, `Recipes.tsx:131`, `ShoppingList.tsx:231`.
- Re-verified the URL effect calls `onReset()` (not the old `onComplete()`) for the `?reset-tour=1` path.
### Lessons learned
- **Lesson 1: A green build is not verification.** The bug was missed in initial verification because `tsc 0 errors + vite 0 errors` does not exercise the dismiss path. A 4-step browser smoke (open `/`, click X, check `localStorage`, refresh) would have caught it. Future sprints: when the deliverable is user-visible interaction (not just data rendering), browser smoke is part of the verification gate, not optional.
- **Lesson 2: Inverse paths deserve inverse APIs.** `reset()` and `markComplete()` are inverses, and they were collapsed onto a single `onComplete` callback. The collapse worked for `reset` and broke for `markComplete`. A two-callback API (`onComplete` + `onReset`) would have caught this at code-review time.
- **Lesson 3: Unit tests for state hooks are cheap insurance.** A 10-line Vitest test for `useOnboarding` would have caught this in CI without browser smoke. Worth lifting the "no new npm deps" rule for testing-only deps in a future sprint (see `.agent/context.md` Q4).