Files
Meal-Planner/Review/handoff-ui-audit.md
T
admin 4c85c929d3 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.
2026-06-05 14:09:10 -07:00

366 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# UI/UX Audit & Fix — Agent Handoff
You are taking over an 8-sprint UI/UX audit and fix cycle. **All 8 sprints' code is committed and build green.** The user's deployment host (Tailscale `100.108.224.12`) is the only environment you should touch for verification — the local repo on this machine (`/home/peter/Projects/MealPlanner`) was the editing host; the running app lives elsewhere.
**Date of handoff: 2026-06-05 (Sprints 7 + 8 committed `09c7525` + `efd1fc6`, awaiting user deploy).**
---
## How to take over (fresh-agent quickstart)
If you are a new agent continuing this work, do this **in order**:
1. **Read** `docs/ORIENTATION.md` (project orientation) → `docs/HANDOFF.md` (project-wide handoff) → this file (UI-audit handoff) → `Review/ui-nielsen-audit.md` (the audit itself).
2. **Skim** the per-sprint verification docs in `Review/sprint{1..10}-verification.md`. They are the source of truth for the deploy + smoke flow.
3. **Check the user's deployment status** — the user deploys in batches. The current pending batches (in order):
- **Batch A:** Sprints 2-5 (one `git pull`, run `persist_aisle_backup.sql`, `alembic upgrade head`, `docker compose up -d --build backend frontend`). The 0015 cast fix is in `d78bd18`; Sprint 2's deploy was blocked on it.
- **Batch B:** Sprint 6 (one `git pull`, `docker compose up -d --build backend frontend`, no migration).
- **Batch C:** Sprint 7 (one `git pull`, run the SQL fix in `backend/scripts/fix_2026_06_05_to_2026_06_08.sql`, `docker compose up -d --build backend frontend`).
- **Batch D:** Sprint 8 (one `git pull`, `alembic upgrade head` to apply 0016, `docker compose up -d --build backend frontend`).
- **Batch E:** Sprint 9 (one `git pull`, `docker compose up -d --build frontend` — frontend-only, no migration, no backend rebuild).
- **Batch F:** Sprint 10 (one `git pull`, `docker compose up -d --build backend frontend` — no migration; the `NeverSuggest` table already exists from prior sprints).
4. **Open issues** in `.agent/plan.md` (the "Phase R1-R3" section is a prior plan; the **Sprint 10 active-sprint** section is the current state) and in `.agent/context.md` (decisions + open Qs for the current sprint).
5. **Do not** touch the pre-existing WIP files: `backend/app/api/recipes.py`, `backend/app/schemas/recipe.py`, `nginx/nginx.conf` (untouched since before this work; user's to manage).
6. **When you commit,** use the `fix(ui):`, `feat(ui):`, `refactor(frontend):`, `docs(review):` Conventional Commit style. Force-add new files in `frontend/src/lib/` (the `.gitignore` line 17 `lib/` is a pre-existing bug that catches it).
**TL;DR of where things stand:**
- Sprints 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8: code committed and build green. Sprint 1 deployed. Sprints 2-8 awaiting user deploy.
- The only remaining §Future items are F1 onboarding tour, F8 Spoonacular enrichment (proposal), F9 Ollama LLM matcher (proposal), and the dead `Generate Meal Plan` CTA at `Dashboard.tsx:415`. Documented in `.agent/plan.md` and `fix-ui-audit.md`; awaiting user direction.
- Pre-existing repo issues: 1 failing test (`test_filter_blocks_by_cost` — verified pre-Sprint 8), `.gitignore` `lib/` bug, no CI. Documented.
---
## ⚠️ Pending user deploy — Sprints 7 + 8 (committed, awaiting pull)
### Sprint 8 — "Deny" semantics (C + Z, hard-filter escalation)
**Status: COMMITTED `efd1fc6` on 2026-06-05. Build green. 21/21 planner tests pass.** Awaiting user to `git pull` + `alembic upgrade head` + rebuild.
**User policy decision (2026-06-05, exact words):** "Hard filter. If it is denied this week twice, it should be considered denied for good."
**Root cause (one-liner):** the planner has no cross-week memory of denials. Denials live on the `meal_plan_item` row, are never consulted by the planner, and the `NeverSuggest` blocklist is empty for the user's family. The user's "Roasted Sweet Potato and Chickpea Bowl" was denied on 2026-05-15 but the recipe was still in the pool for the next 90+ days.
**Policy (Sprint 8):**
- "Approve" → `item.approval_status = approved`. n/a.
- "Deny this week" (1st in 90d) → `denied` + `denial_expires_at = now() + 90d`. Recipe becomes eligible again after 90d.
- "Deny this week" (2nd in 90d — **server-side auto-escalation**) → `denied` + `denial_expires_at = NULL` + a `NeverSuggest` row written. Permanent.
- "Never again" (explicit) → same as the 2nd-time auto-escalation. Permanent.
**Scope (12 boxes):** see `.agent/plan.md` "Active sprint" section. Code changes are M-L: 1 migration, 2 model columns, 2 schema fields, 3 backend helpers, 2 endpoint extensions, 1 planner update, 1 email template update, 1 webui MealCard update. **No new dependencies. Migration 0016 required.**
**Tracking docs:** `Review/sprint8-verification.md` (deploy + smoke), `Review/ui-nielsen-audit.md` Sprint 8 status block, `fix-ui-audit.md` T2.1T2.10, this file, `docs/HANDOFF.md` Sprint 8 section.
**Thread 3 (§Future backlog) is deferred** until S8 is deployed + verified. F1 onboarding, F8/F9 proposals, dead `Generate Meal Plan` CTA at `Dashboard.tsx:415`. **Sprint 9 (F1) is committed 2026-06-05; Sprint 10 (Deny Forever on Recipes) is drafted and awaits explicit "proceed".**
### Sprint 9 — F1 Onboarding Tour (H10)
**Status: COMMITTED + POST-DEPLOY FIX on 2026-06-05. Build green. Frontend-only.** Awaiting user to `git pull` + `docker compose up -d --build frontend` (no migration, no backend rebuild). The original commit (`6e386ba`) had a broken dismiss path (X / Skip / Esc / "Got it" did nothing — `onComplete` was wired to `useOnboarding().reset()` which is the inverse operation). Post-deploy fix is `1562929`: split into two callbacks `onComplete` (dismiss → `markComplete()`) and `onReset` (re-show → `reset()`). User confirmed browser smoke passes after the fix.
**Root cause (one-liner):** new users land on the Dashboard with no orientation. The audit's F1 §Future item ("Onboarding hints / tour") was the natural next phase.
**Scope (4 boxes):** 1 new `OnboardingTour.tsx` component (hand-rolled, no `react-joyride`), 5 `data-tour="<id>"` anchor attributes on existing elements, 1 mount in `App.tsx`, 1 localStorage key (`mealplanner:onboarding-complete`). **No new dependencies. No backend changes.**
**Tour behavior:**
- Auto-shows on first visit to `/` (the only root route). Subsequent visits do not show.
- 4 steps: Dashboard / Pantry / Recipes / Shopping List. Each anchors to a `[data-tour="<id>"]` element on the relevant page.
- Keyboard: `1``4` jump to step, `←/→` step back/forward, `Esc` dismiss.
- Off-route fallback: if the user is on a different page than the current step's anchor, the tooltip renders as a centered card with an "Open <page>" CTA.
- `?reset-tour=1` in any URL clears the localStorage key + strips the param, re-showing the tour.
- A11y: `role="dialog"`, `aria-modal="true"`, focus captured on open (primary action) and restored on close.
**Tracking docs:** `Review/sprint9-verification.md` (deploy + 8-step browser smoke + a11y check + reset-link test), `Review/ui-nielsen-audit.md` Sprint 9 status block, `fix-ui-audit.md` T3.1T3.4, this file, `docs/HANDOFF.md` Sprint 9 section.
### Sprint 10 — "Deny Forever" on Recipes (user-driven)
**Status: COMMITTED on 2026-06-05. Build green. Backend + frontend, no migration.** Awaiting user to `git pull` + `docker compose up -d --build backend frontend` (the `NeverSuggest` table already exists from prior sprints).
**Root cause (one-liner):** the user can already block a recipe from a meal plan (Sprint 8), but a recipe they've never seen planned can only be blocked by the admin via the `NeverSuggest` admin API. Sprint 10 surfaces the same `NeverSuggest` infrastructure on the Recipes surface so the user can pre-emptively mark a recipe as "allergy" or "dislike" while browsing.
**Scope (7 boxes):** 2 new public backend endpoints (`POST` + `DELETE /api/never-suggest`), 1 schema field (`recipe_name`), 1 new `NeverSuggestButton.tsx` component (~290 lines), 1 API client (`neverSuggest.list/add/remove`), 1 overlay on `RecipeCard`, 1 button group in `RecipeDetail` top bar. **No new dependencies. No migration. Admin path unchanged.**
**Two reasons (matching the server's `NeverSuggestReason` enum):**
- `Allergy` (red) — requires `window.confirm`. Permanent, irreversible to the planner.
- `Dislike` (neutral) — no confirm. The 6s undo toast is the escape hatch.
**Undo semantics:** Sprint 3 B12 `showToast.undo()` pattern. Click Undo → `DELETE /api/never-suggest/{id}` + 4 query invalidations so the recipe reappears immediately.
**Tracking docs:** `Review/sprint10-verification.md` (deploy + 9-step browser smoke + 5 API curls + undo test + a11y check), `Review/ui-nielsen-audit.md` Sprint 10 status block, `fix-ui-audit.md` T4.1T4.9, this file, `docs/HANDOFF.md` Sprint 10 section.
### Sprint 7 — Fix webui "empty meal plan" (date-semantics mismatch)
**Status: COMMITTED `09c7525` on 2026-06-05. Build green.** Awaiting user to `git pull` + run the SQL fix + rebuild.
**Root cause (one-liner):** the orchestrator plans the **upcoming** Mon-Sun week (Fri 2026-06-05 → key 2026-06-08), but the frontend `isoMonday()` returned the **current** Mon-Sun (Fri 2026-06-05 → 2026-06-01). Email + DB + webui disagree by 7 days. User sees an empty page.
**Scope (6 boxes):** 1 backend function, 1 frontend util rename, 1 new `WeekRangeNav` component, 2 call-site updates, 1 SQL fix script. **No migration.**
**Tracking docs:** `Review/sprint7-verification.md` (deploy + smoke), `Review/ui-nielsen-audit.md` Sprint 7 status block, `fix-ui-audit.md` S7.1S7.6, this file, `docs/HANDOFF.md` Sprint 7 section.
---
## TL;DR
Twelve commits land all 14 audit findings + 6 §Future items + 2 user-driven sprints:
| Sprint | Commit | Scope | Build | Deploy |
|---|---|---|---|---|
| 1 | `f3e4a44` | 5 P0 blockers | ✅ green | ✅ deployed 2026-06-02 by user |
| 2 | `ccc70aa` + `f5fb755` | 6 P1s + bonus S3.3 | ✅ green | ⚠️ blocked — see S2 + 0015 fix below |
| 3 | `e90a9d6` | 3 P2s + a11y sweep | ✅ green | ⚠️ blocked — depends on S2 deploy |
| 4 | `d71b67a` | F7 global error handler + F6 plan-status aria-label | ✅ green | ⚠️ not yet deployed (frontend-only) |
| 5 | `d78bd18` | F5 URL week selector + **CRITICAL 0015 cast fix** | ✅ green | ⚠️ not yet deployed |
| 5 | `f740f40` | F2 keyboard shortcuts + ShortcutHelpBanner | ✅ green | ⚠️ not yet deployed |
| 6 | `8ad4ef6` | F3 bulk pantry add + F4 plan-the-week (ShoppingList + Dashboard) | ✅ green | ⚠️ not yet deployed (backend + frontend, no migration) |
| 7 | `09c7525` | webui "empty meal plan" date-semantics fix + new `WeekRangeNav` + SQL data fix | ✅ green | ⚠️ committed; awaiting user deploy |
| 8 | `efd1fc6` | "Deny" semantics (C + Z, hard-filter escalation) | ✅ green | ⚠️ committed; awaiting user deploy |
| 9 | (committed 2026-06-05) | F1 Onboarding Tour (H10) — hand-rolled, no new deps, 4-step welcome tour with `?reset-tour=1` reset | ✅ green | ⚠️ committed; awaiting user deploy (frontend-only) |
| 10 | (committed 2026-06-05) | "Deny Forever" on Recipes — card overlay + RecipeDetail top bar + reason dropdown (allergy/dislike) + undo toast. New `POST`/`DELETE /api/never-suggest` (public) + `recipe_name` join. | ✅ green | ⚠️ committed; awaiting user deploy (backend + frontend, no migration) |
All work is on `main` ahead of `origin/main` (pre-existing WIP also present). All 10 sprints compile. **Sprint 1 is live. Sprints 2-10 are not yet live on `100.108.208.56:8082/`.**
**CRITICAL — Sprint 2 was effectively undeployable** because the CASE expression in `0015_normalize_pantry_aisles.py` failed with `text = boolean` on the `varchar(100) aisle` column. The bug is fixed in `d78bd18` (Sprint 5). Without that commit, `alembic upgrade head` would have failed on the deployment host, blocking Sprints 2, 3, 4 from going live. **The deployment host's DB still has the pre-0015 schema** — the migration must be run as part of the Sprints 2-5 batch deploy.
**Next action:** the user runs the deploy commands in `Review/sprint2-verification.md`, `Review/sprint3-verification.md`, `Review/sprint4-verification.md`, `Review/sprint5-verification.md`, `Review/sprint6-verification.md`, `Review/sprint7-verification.md`, and `Review/sprint8-verification.md` on the deployment host, then smoke-checks per the checklists. Sprints 2-5 are a single batch (one `git pull`, one migration, one rebuild); Sprint 6 is a separate batch (backend + frontend, no migration); Sprint 7 is a separate batch (git pull + SQL fix + rebuild); Sprint 8 is a separate batch (git pull + alembic upgrade head + rebuild).
---
## Where the work lives
```
/home/peter/Projects/MealPlanner/
├── Review/
│ ├── ui-nielsen-audit.md # The audit (276 → ~340 lines). Status blocks per sprint at the top.
│ ├── sprint2-verification.md # Deploy + smoke checks for Sprint 2 (BACKEND MIGRATION INCLUDED).
│ ├── sprint3-verification.md # Deploy + smoke checks for Sprint 3 (frontend only).
│ ├── sprint4-verification.md # Deploy + smoke checks for Sprint 4 (F7+F6, frontend only).
│ ├── sprint5-verification.md # Deploy + smoke checks for Sprint 5 (F5+F2+0015 fix; backend+frontend).
│ ├── sprint6-verification.md # Deploy + smoke checks for Sprint 6 (F3+F4; backend+frontend, no migration).
│ ├── docs-claude.md # Prior docs adversarial review (not yours).
│ ├── docs-gpt5.5.md # Prior docs adversarial review.
│ ├── repo-gpt5.5.md # Prior repo adversarial review.
│ ├── reviewconcensus.md # Prior review consensus.
│ └── synthesis.md # Prior review synthesis.
├── fix-ui-audit.md # The plan. Implementation notes per task + commit refs. Sprints 4-5 added.
├── docs/HANDOFF.md # The project-wide handoff (don't duplicate — read for context).
└── docs/ORIENTATION.md # Project orientation.
```
**Read first**, in this order: `docs/ORIENTATION.md``docs/HANDOFF.md``Review/ui-nielsen-audit.md` (the audit) → `fix-ui-audit.md` (the plan) → the four `sprintN-verification.md` files.
---
## Audit findings — all closed
The audit has 14 findings + 3 a11y items. They map 1:1 to the plan's task list. All have code committed. Some have known caveats — read the audit's "Sprint 1/2/3 status" blocks at the top of `Review/ui-nielsen-audit.md` for the per-finding deploy status.
**P0 blockers (Sprint 1, `f3e4a44`):**
- B1 Recipe ingredients fused unit+name (`RecipeDetail.tsx:161`) — `.trim()` removed.
- B2 Meal ingredients missing quantities (`MealDetail.tsx:248-252`) — `qty` field; also `ing.ingredient?.name` fallback.
- B3 `$N/A per serving` (`MealDetail.tsx:191`) — conditional on `cost != null`.
- B4 `/recommended` blank page — `*` NotFound catch-all + `/recommended` alias to `/recipes/recommended`. New `pages/NotFound.tsx`. `EmptyState` extended with optional `to` prop.
- B5 Mobile empty meal slots hidden (`Dashboard.tsx:164,219`) — `hidden md:*` removed; `min-h-11` (44 px) on the Generate button.
**P1 (Sprint 2, `ccc70aa` + `f5fb755`):**
- B6 Meal-card title 1-line truncate — `line-clamp-2`; image shrinks 56→40 on `<md`.
- B7 MealDetail hero overlap + spoonacular SEO copy — hero reworked, `cleanDescription()` in `lib/utils.ts` strips 14 boilerplate patterns, raw text → "Notes from source" disclosure.
- B8 Pantry aisle free-text → canonical select — `PANTRY_AISLES` enum, `Select` components, ingredient name marked `*`. **Backend migration `0015_normalize_pantry_aisles.py` normalizes `ingredient.aisle` + `grocery_item.aisle` in a single transaction; dry-run at `backend/scripts/dry_run_aisle_migration.sql`; persistent backup at `backend/scripts/persist_aisle_backup.sql`.**
- B9 ShoppingList snake_case aisle labels — `AISLE_LABEL` map + `aisleDisplay()`.
- B10 Mobile pantry table — `role="region"` + right-edge white-to-transparent gradient hint.
- B11 Recipes filters — `applied`/`pending` state split, Apply/Reset buttons, active-count chip on Filters button.
**P2 + a11y (Sprint 3, `e90a9d6`):**
- B12 Undo-toast replaces `confirm()``lib/toast.tsx` (renamed from `.ts` for JSX) gains `showToast.undo(message, onUndo, ms=5000)`. Dashboard undo re-fires `generateItem` to refill the slot (recipe may differ — documented in plan §R4). Pantry undo re-fires `pantry.add` with original fields (fully reversible).
- B13 Mobile nav wrap — `whitespace-nowrap` + `px-2 sm:px-3`.
- S3.3 Mobile shopping-list stat grid (3-col on all viewports) — done in Sprint 2.
- S3.4 ErrorBoundary already mounted at `App.tsx:42` — verified, no code change.
- S3.5 A11y sweep — `<nav aria-label="Primary">`, `aria-current="page"`, `<main id="main-content">`, `Badge` extended with `icon` + `aria-label` props, approval-status Badge uses `aria-label="Approval status: …"`.
---
## Environment quirks you MUST know
### 1. The deployment host is not this machine
This repo lives on a development host (Tailscale `100.108.146.47`). The live deployment is on the user's home server at Tailscale `100.108.224.12`, served at `http://100.108.208.56:8082/`. The deployment host's Docker stack is the source of truth for the running app.
When you `docker compose up -d --build` *here*, you rebuild images on this host's Docker daemon, not the deployment host's. The frontend I built in Sprint 1 was verified by the user *manually* deploying from this repo on the deployment host.
**Don't try to push to the deployment host.** The user's workflow is: commit locally, then `git pull` on the deployment host and rebuild there.
### 2. The db is in a container; no host psql
On the deployment host (`docker-ubuntu`), `psql` is not installed. All DB commands must run via `docker compose exec db psql -U mealplanner -d mealplanner -f /dev/stdin < script.sql`. The `Sprint 2` verification doc has the exact incantations.
**On the local dev host** (this one), `psql` IS available because the same `db` service runs locally for the user's dev work. But the DB used in dev is *different* from the one the user is migrating (Tailscale host). The dry-run I ran on this host showed 21,196 rows would change; the deployment-host DB will have a different count.
### 3. `.gitignore` blocks `frontend/src/lib/`
There's a pre-existing repo bug: `.gitignore` line 17 is `lib/` (the Python `lib/` ignore), and it catches `frontend/src/lib/`. New files in `frontend/src/lib/` need `git add -f`. Existing tracked files (`utils.ts`) work; the new `toast.tsx` was force-added in commit `e90a9d6`. Be aware when adding new files there.
### 4. The `.env` file is at the project root
It contains `SESSION_PASSWORD=test-family-password`, `APP_BASE_URL=http://100.108.224.12:8082` (Tailscale host), real SendGrid keys, real Ollama creds. Don't commit it (`.env` is in `.gitignore`). Don't `cat` it casually.
### 5. ESLint is not configured
`npm run lint` errors with "ESLint couldn't find a configuration file." That's a pre-existing repo gap, not introduced by this work. The `build` script (`tsc && vite build`) is the typecheck.
### 6. Pre-existing WIP is in `git status`
When you `git status`, you'll see modifications to `backend/app/api/meals.py`, `backend/app/api/recipes.py`, `backend/app/schemas/recipe.py`, `frontend/src/api/index.ts`, `nginx/nginx.conf`. These are NOT mine — they were already in the working tree from prior commits. Don't commit them as part of UI audit work. If you need to amend them, do it on a separate commit with a separate message.
---
## What "verification" actually looks like
This work has *no automated test suite in CI* (per the audit, only `npm run build` enforces typecheck). The verification gate is **screenshot + manual smoke** on the deployment host.
For each sprint, a `sprintN-verification.md` lists:
- A **deploy command** block (the user runs on the deployment host).
- A **smoke-check** list of manual interactions to perform in the browser.
- A list of "Things to look for" — the observable behavior change for each finding.
The user's flow is: read the smoke check, run the deploy, click around, mark each item ✅ or report an issue. The verification log is a record, not a script.
**If you need to re-verify locally**, the original screenshots are at `/tmp/opencode/mp-review/screenshots/` (20 PNGs, 1366 px and 390 px viewports). Post-fix screenshots are in `/tmp/opencode/mp-review/screenshots/fix-sprint1/`. The playwright harness script is at `/tmp/opencode/mp-review/shoot-sprint1.js`. Useful templates for re-screenshotting Sprints 2 and 3 are in there — adapt the URL list.
---
## Active risks & open questions
1. **S2.3 / B8 backend migration NOT YET RUN on deployment host.** The user must run `alembic upgrade head` after `git pull`. Dry-run shows 21k rows change on the dev DB; the prod DB may differ. The persistent backup (`persist_aisle_backup.sql`) creates two permanent tables the operator can `DROP` after confidence is established.
2. **S3.1 / B12 Dashboard Undo is "rebuild, not restore".** `meals.generateItem` re-fills the slot with a *new* recipe; the original recipe is gone. The plan documents this trade-off (§R4). If the user pushes back, the alternative is a backend endpoint to restore from a snapshot (out of scope, would need a separate task).
3. **`frontend/src/lib/toast.tsx` is force-added because of the `lib/` gitignore bug.** Future agents should be aware: any new file in `frontend/src/lib/` needs `git add -f`. Consider fixing the `.gitignore` (use `/lib/` or specific Python paths instead of `lib/`) as a small follow-up.
4. **S2.3 may need a follow-up constraint** — the migration docstring says no `CHECK (aisle IN (...))` constraint is added. The next migration (`0016`) could add it, but that's an optional tightening. The frontend already constrains the form to canonical values, so DB constraint is belt-and-suspenders.
5. **`scripts/dry_run_aisle_migration.sql` is read-only but reads from prod.** If the user runs it via `docker exec ... psql -f /dev/stdin < script.sql`, no writes happen. But they should know it's a *query*, not a script. The comment at the top of the file says so.
---
## What's NOT in scope (per audit §Future)
F2, F3, F4, F5, F6, and F7 are done across Sprints 4-6. The audit intentionally deferred these. If the user asks, they become a new plan:
- F1. Onboarding hints / tour (H10)
- ~~F2. Keyboard shortcuts (`/`, `g p`, `g s`, `n m`)~~ — done in Sprint 5
- ~~F3. Bulk add on Pantry/Shopping List (H7)~~ — done in Sprint 6 (ShoppingList only)
- ~~F4. Plan-the-whole-week button (H7)~~ — done in Sprint 6
- ~~F5. Persistent week selector in URL~~ — done in Sprint 5
- ~~F6. Badge a11y for color-only signals (generalized)~~ — done in Sprint 4
- ~~F7. Global `react-query` `onError` toast handler~~ — done in Sprint 4
- F8. Backend Spoonacular enrichment (separate proposal in `docs/proposals/`)
- F9. Ollama LLM matcher (separate proposal in `docs/proposals/`)
**Follow-up tickets surfaced during Sprints 4-6:**
- Wire the no-op `Generate Meal Plan` empty-state CTA at `Dashboard.tsx:415` to a real user-facing "create a new plan" path (orchestrator/admin flow). Distinct from F4 (which is "plan exists, slots are empty"). Currently the user sees the CTA on `/` only if their family has NO plan at all.
- Pantry bulk-add (F3 was scoped to ShoppingList in Sprint 6; adding multi-select to a 4-column table on mobile is a larger surface).
- "Sprints 2-5 batch" deployment: Sprints 2, 3, 4, 5 are deployable as a single batch now that the 0015 cast fix is in. Sprint 6 is a separate batch (no migration). See the per-sprint `sprintN-verification.md` deploy sections.
---
## File-level diff summary (audit work only)
| Sprint | File | What |
|---|---|---|
| 1 | `frontend/src/pages/RecipeDetail.tsx` | B1: drop `.trim()` |
| 1 | `frontend/src/pages/MealDetail.tsx` | B2: `qty` + name fallback; B3: cost conditional |
| 1 | `frontend/src/App.tsx` | B4: `/recommended` alias + `*` NotFound |
| 1 | `frontend/src/pages/Dashboard.tsx` | B5: remove `hidden md:*` on empty slots |
| 1 | `frontend/src/pages/NotFound.tsx` | **NEW** — uses EmptyState |
| 1 | `frontend/src/components/ui/EmptyState.tsx` | optional `to` prop |
| 1 | `frontend/src/types/index.ts` | `RecipeIngredient.notes` + `ingredient?` |
| 2 | `frontend/src/pages/Dashboard.tsx` | B6: line-clamp-2 + smaller image |
| 2 | `frontend/src/pages/MealDetail.tsx` | B7: hero rework + cleanDescription + disclosure |
| 2 | `frontend/src/lib/utils.ts` | B7: cleanDescription() |
| 2 | `frontend/src/pages/Pantry.tsx` | B8: select for aisle/unit + required marker; B10: scroll hint |
| 2 | `frontend/src/types/index.ts` | B8: PANTRY_AISLES + PantryAisle type |
| 2 | `backend/alembic/versions/0015_normalize_pantry_aisles.py` | **NEW** — migration |
| 2 | `backend/scripts/dry_run_aisle_migration.sql` | **NEW** — read-only preview |
| 2 | `backend/scripts/persist_aisle_backup.sql` | **NEW** — persistent backup |
| 2 | `frontend/src/pages/ShoppingList.tsx` | B9 + S3.3: aisle label + 3-col grid |
| 2 | `frontend/src/pages/Recipes.tsx` | B11: applied/pending filter + Apply/Reset + active count |
| 3 | `frontend/src/lib/toast.ts``.tsx` | renamed + undo helper |
| 3 | `frontend/src/pages/Dashboard.tsx` | B12: undo on delete; S3.5: aria-label on status badge |
| 3 | `frontend/src/pages/Pantry.tsx` | B12: reversible undo on remove |
| 3 | `frontend/src/App.tsx` | B13 + S3.5: nav nowrap, aria-current, main id |
| 3 | `frontend/src/components/ui/Badge.tsx` | S3.5: icon + aria-label props |
| 4 | `frontend/src/lib/toast.tsx` | F7: extractErrorMessage + showApiError |
| 4 | `frontend/src/App.tsx` | F7: QueryCache/MutationCache onError + queries defaultOptions |
| 4 | `frontend/src/pages/Dashboard.tsx` | F7: removed 6 local onError; F6: plan-status aria-label |
| 4 | `frontend/src/pages/Pantry.tsx` | F7: removed 3 local onError; undo now uses showApiError |
| 4 | `frontend/src/pages/MealDetail.tsx` | F7: removed submitMutation.onError |
| 5 | `backend/app/api/meals.py` | F5: added ?week_start= query param |
| 5 | `backend/app/api/shopping_list.py` | F5: added ?week_start= query param |
| 5 | `backend/alembic/versions/0015_normalize_pantry_aisles.py` | **CRITICAL cast fix** — was blocking Sprint 2 deploy |
| 5 | `frontend/src/lib/utils.ts` | F5: isoMonday/parseIsoDate/shiftIsoDate/formatIsoDate helpers |
| 5 | `frontend/src/api/index.ts` | F5: getPlanned/get take weekStart? |
| 5 | `frontend/src/pages/Dashboard.tsx` | F5: useSearchParams sync, week nav, queryKey includes weekStart |
| 5 | `frontend/src/pages/ShoppingList.tsx` | F5: same URL sync as Dashboard |
| 5 | `frontend/src/hooks/useKeyboardShortcuts.ts` | **NEW** — F2 global handler with vim-style sequences |
| 5 | `frontend/src/hooks/useFocusSearch.ts` | **NEW** — F2 focus-search CustomEvent bus |
| 5 | `frontend/src/components/ShortcutHelpBanner.tsx` | **NEW** — F2 help dialog |
| 5 | `frontend/src/App.tsx` | F2: mount GlobalShortcuts + ShortcutHelpBanner |
| 5 | `frontend/src/pages/Pantry.tsx` | F2: useFocusSearchOnShortcut on search input |
| 5 | `frontend/src/pages/Recipes.tsx` | F2: useFocusSearchOnShortcut on search input |
| 6 | `backend/app/api/pantry.py` | F3: POST /api/pantry/bulk with per-item upsert |
| 6 | `backend/app/api/meals.py` | F4: POST /api/meals/{id}/fill-empty-slots with partial-success result |
| 6 | `backend/app/schemas/__init__.py` | F3+F4: HomePantryBulkCreate, HomePantryBulkResult, FillEmptySlotsRequest, FillEmptySlotsResult, FilledSlot, FailedSlot |
| 6 | `frontend/src/api/index.ts` | F3: pantry.addBulk; F4: meals.fillEmptySlots |
| 6 | `frontend/src/pages/ShoppingList.tsx` | F3: 'Add N to pantry' button + addCheckedToPantry handler |
| 6 | `frontend/src/pages/Dashboard.tsx` | F4: 'Plan the week' button + Dinners only / All meals dropdown |
---
## Quick-start for the next agent
If you are continuing this work:
```bash
# 1. Get oriented
cd /home/peter/Projects/MealPlanner
cat docs/ORIENTATION.md
cat docs/HANDOFF.md
cat Review/ui-nielsen-audit.md
cat fix-ui-audit.md
# 2. See what's still in flight
git log --oneline -10
git status
git log origin/main..HEAD --oneline
# 3. If the user wants to deploy pending work (Sprints 2-5 as a single batch):
# - Sprint 2: docker compose exec db psql -U mealplanner -d mealplanner \
# -f /dev/stdin < backend/scripts/persist_aisle_backup.sql
# - docker compose exec backend alembic upgrade head
# - Sprints 3-5: docker compose -f docker-compose.yml up -d --build backend frontend
# See Review/sprint{2,3,4,5}-verification.md for the full checklist.
# 4. If you need to take new screenshots after changes
# /tmp/opencode/mp-review/shoot-sprint1.js is a working playwright harness.
# Adapt the URL list. Output: /tmp/opencode/mp-review/screenshots/.
# 5. Build check before any commit
cd frontend && npm run build
```
**When you commit**, follow the existing style:
- `fix(ui): …` for bug fixes
- `feat(ui): …` for new behavior
- `refactor(frontend): …` for restructuring without behavior change
- `docs(review): …` for doc-only commits
- Conventional commits, imperative mood, body explaining the why
**When you push**, only push UI-audit commits. Pre-existing WIP is the user's to manage.
---
## Final words
Trust the build output. Trust the smoke checklist. Don't trust the deployment host's UI until the user confirms. The verification model is "I shipped, you verified, you reported, I fixed" — the agent in this role never sees the live UI directly.
**Last updated: 2026-06-05** — Sprint 1 deployed; Sprints 2-6 awaiting user deploy; **Sprint 7 (`09c7525`), Sprint 8 (`efd1fc6`), Sprint 9 (F1 Onboarding Tour) + post-deploy fix (`1562929`), and Sprint 10 (Deny Forever on Recipes) committed on 2026-06-05, awaiting user deploy**. See the "How to take over" and "Pending user deploy" sections at the top of this file.