Files
Meal-Planner/Review/handoff-ui-audit.md
T
admin af4ec793c7 docs: Sprint 14 — Vitest for useOnboarding (Q4) across all 6 running docs
Sprint 14 (commit 28f3212) adds Vitest + happy-dom +
@testing-library/react + @testing-library/jest-dom as
devDeps and locks the useOnboarding state-transition
contract with 7 unit tests in 25 ms. Lifts the 'no new
npm deps' rule for testing-only. Runtime bundle unchanged
(503.82 kB). No pre-existing WIP files touched.

Sprint 9's bug 1562929 (onComplete wired to .reset, the
inverse op) shipped a post-deploy fix the same day.
Sprint 14 prevents recurrence at npm test time.

This commit updates the 6 running docs that track sprints:

- .agent/plan.md — header changed to 'Active sprint: Sprint
  14'; Sprint 14 section (S14.1-S14.4 + Done when + Out of
  scope) added after the Sprint 13 section.
- .agent/context.md — Sprint 14 decisions (D1-D7), open Q1,
  and file:line references added.
- Review/sprint14-verification.md — NEW: deploy + 7-case
  test table + verification commands + Case 7 S9-bug-catch
  proof + 5-risk table + open Q1 (component-level tests).
- Review/ui-nielsen-audit.md — Sprint 14 status block
  (T7.1-T7.3) added after the Sprint 13 block.
- fix-ui-audit.md — Sprint 14 section (T7.1-T7.5) added
  after the Sprint 13 section.
- Review/handoff-ui-audit.md — Batch J line in deploy
  list, Sprint 14 section after Sprint 13, TL;DR Sprint
  14 line, Last-updated footer updated. (Did NOT replace
  the Sprint 7/8/TL;DR/environment-quirks/active-risks
  content that was at the bottom of the file — that
  material is preserved unchanged.)
- docs/HANDOFF.md — Sprint 14 section added after Sprint
  13, Last-updated footer updated.

All 6 docs now reflect Sprint 14. §Future backlog
remaining: F9-full (local Ollama model pull on the host).
Q4 (Vitest) is now closed. Sprint 14 is the smallest
sprint in the cycle (devDeps + 1 test file + 2 config
files + 2 scripts) and the first that adds a test
infrastructure layer.
2026-06-05 17:30:00 -07:00

38 KiB
Raw Blame History

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).
    • Batch G: Sprint 11 (one git pull, docker compose up -d --build frontend — frontend-only, no migration, no backend rebuild).
    • Batch H: Sprint 12 (one git pull, docker compose up -d --build backend frontend — backend has the new recipe_search.py router, frontend has the new toggle).
    • Batch I: Sprint 13 (one git pull, docker compose up -d --build backend frontend — backend has the new llm_plan.py router, frontend has the new prompt modal).
    • Batch J: Sprint 14 (one git pull, npm install in frontend/, npm test to confirm 7/7, then docker compose up -d --build frontend — no backend change, no migration; the 5 new devDeps land on npm install).
  4. Open issues in .agent/plan.md (the "Phase R1-R3" section is a prior plan; the Sprint 14 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 §Future backlog is now empty: F1 (onboarding) shipped as Sprint 9; the dead "Generate Meal Plan" CTA shipped as Sprint 11; F8 (Spoonacular) shipped as Sprint 12; F9-lite shipped as Sprint 13. All four are deployment-pending. F9-full (local Ollama model pull) is the only remaining §Future item — opt-in based on cloud-billing feedback.
  • Sprint 14 (Vitest for useOnboarding, Q4) code complete on 2026-06-05. 7/7 tests green in ~25 ms, npm run build green, no runtime dep change. Awaiting commit + push. Closes the open Q4 from Sprint 9.
  • 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. F9-full (local Ollama model pull on the host) remains a future sprint. Sprint 9 (F1 onboarding) + post-deploy fix (1562929), Sprint 10 (Deny Forever on Recipes), Sprint 11 (wire the dead "Generate Meal Plan" CTA), Sprint 12 (F8 Spoonacular search), and Sprint 13 (F9-lite Ollama Cloud plan synthesis) are all committed 2026-06-05, awaiting user deploy.

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: 14 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 " 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 11 — Wire the dead "Generate Meal Plan" CTA (user-driven)

Status: COMMITTED on 2026-06-05. Build green. Frontend-only. Awaiting user to git pull + docker compose up -d --build frontend (no migration, no backend rebuild).

Root cause (one-liner): the Dashboard's empty state has rendered a "Generate Meal Plan" button since Sprint 1 with onClick: () => {} — clicking it does nothing. The user has no path from a fresh install to a populated plan.

Scope (3 boxes): 1 new handleGenerateFirstPlan handler in Dashboard.tsx (~50 lines, calls existing meals.create + meals.fillEmptySlots), 1 new EmptyState.action.disabled prop (optional, backward-compatible), 1 toast format reused from Sprint 6 F4. No new dependencies. No backend changes. The two endpoints already exist from Sprint 6+.

Generation flow: 1) POST /api/meals with { week_start_date, status: 'draft', items: [] } to create a fresh plan. 2) POST /api/meals/{id}/fill-empty-slots with { meal_types: ['breakfast', 'lunch', 'dinner'] } to fill it from the library. 3) Invalidate ['mealPlan', weekStart]. The page re-renders with the populated plan.

Race handling: if meals.create returns 400 with detail: "Meal plan for this week already exists" (another tab created one first), the handler falls through to getPlanned(weekStart) to get the existing plan's id, then calls fillEmptySlots against it. No error toast in this case.

Tracking docs: Review/sprint11-verification.md (deploy + 4-step browser smoke + race test + 2 API curls), Review/ui-nielsen-audit.md Sprint 11 status block, fix-ui-audit.md T5.1T5.5, this file, docs/HANDOFF.md Sprint 11 section.

Sprint 12 — F8 Spoonacular search (§Future H10) (user-driven)

Status: COMMITTED on 2026-06-05. Build green. Backend + frontend. Awaiting user to git pull + docker compose up -d --build backend frontend (no migration).

Root cause (one-liner): the user can browse ~150 local recipes on /recipes but has no path to find new ones without leaving the app.

Scope (6 boxes): NEW backend/app/api/recipe_search.py (~270 lines, 2 endpoints + module-level quota counter), 1 schema addition (RecipeSearchHit + RecipeImportRequest), 1 Settings field (SPOONACULAR_API_KEY), 1 router registration, 5 new mealPlannerApi.recipes methods, 1 toggle button + 1 web-search panel + 1 import mutation in Recipes.tsx, 2 optional RecipeIngredient fields. No new dependencies. No migration. No pre-existing WIP files touched.

Free-tier quota: Spoonacular = 150 points/day. complexSearch = 1 pt + 0.01/result. The new search uses summary-only (1.1 pts/query). The import uses 1 pt for the info call + ingredient upserts (no extra points). 140-pt daily budget leaves a 10-pt safety margin. Counter resets on process restart.

Pre-existing tsc errors exposed: adding 5 methods to the recipes object literal exposed 5 latent errors in Pantry/MealDetail/Recommended.tsx (calls to non-existent methods + 2 missing RecipeIngredient fields). Resolved per user decision: 3 stub methods + 2 optional type fields. ~7 lines of fixes; no WIP touched. Documented in Review/sprint12-verification.md D-fix section + fix-ui-audit.md T6.4.

Tracking docs: Review/sprint12-verification.md (deploy + 4-step browser smoke + 2 API curls + quota test + a11y check + 5-risk table), Review/ui-nielsen-audit.md Sprint 12 status block, fix-ui-audit.md T6.1T6.6, this file, docs/HANDOFF.md Sprint 12 section.

Sprint 13 — F9-lite (Ollama Cloud plan synthesis) (§Future H10) (user-driven)

Status: COMMITTED on 2026-06-05. Build green. Backend + frontend. Awaiting user to git pull + docker compose up -d --build backend frontend (no migration).

Root cause (one-liner): the Sprint 11 "Generate Meal Plan" CTA was library-only. Sprint 13 splits it into a 2-step modal: "Use the recipe library" (Sprint 11 unchanged) or "Ask the LLM" (new). The LLM path lets the user describe what they want for the week ("Italian-inspired, vegetarian") and uses kimi-k2.6:cloud on ollama.com to pick meals from the local library.

Scope (5 boxes): NEW backend/app/api/llm_plan.py (~280 lines, 1 endpoint + 4 helpers + tolerance for malformed LLM responses), 2 schema additions (LLMPlanRequest + LLMPlanResponse), 1 router registration, 1 mealPlannerApi.llm.plan method, prompt modal in Dashboard.tsx (radio + textarea + click-outside-to-dismiss). No new dependencies. No migration. No pre-existing WIP files touched. Reuses OLLAMA_BASE_URL / OLLAMA_API_KEY / OLLAMA_MODEL from config.py:36-38.

LLM tolerance: a 60s timeout, parse-failure (markdown code fences, trailing commentary), or empty response all return 0 picks; the library fill takes over. The user sees a success toast with picked_count: 0 — same as if the LLM had returned 0 picks — never a crash.

Tracking docs: Review/sprint13-verification.md (deploy + 3-step browser smoke + 4 API curls + a11y check + 6-risk table), Review/ui-nielsen-audit.md Sprint 13 status block, fix-ui-audit.md T7.1T7.5, this file, docs/HANDOFF.md Sprint 13 section.

Sprint 14 — Vitest for useOnboarding (Q4) (user-driven)

Status: CODE COMPLETE on 2026-06-05. 7/7 tests green in ~25 ms. npm run build green. Awaiting user commit + push. No backend change, no migration, no runtime dep change (5 new devDeps only).

Root cause (one-liner): Sprint 9 (F1 Onboarding Tour) shipped a hand-rolled ~420-line component; the bug 1562929 shipped a post-deploy fix the same day (onComplete was wired to useOnboarding().reset() — the inverse op). Q4 (open question from Sprint 9) was "add Vitest to lock useOnboarding state transitions." Sprint 14 lifts the "no new npm deps" rule for testing-only and locks the bug class at npm test time.

Scope (5 boxes): 4 new devDeps (vitest@^1.6.0, happy-dom@^14.7.0, @testing-library/react@^14.2.0, @testing-library/jest-dom@^6.4.0) + 1 tsc dep (@types/node@^20) + 2 new scripts (test, test:watch) + 2 new config files (vitest.config.ts, vitest-setup.ts) + 1 new test file (OnboardingTour.test.tsx, 7 cases). No backend change. No migration. No runtime dep change. No pre-existing WIP files touched.

Case 7 is the load-bearing test: Sprint 9's bug was at the App.tsx call site, not inside useOnboarding. Cases 1-6 lock the hook contract; Case 7 is a static check on App.tsx that uses node:fs/promises to read the source as a string, runs two regex matches to capture the arrow bodies of onComplete={...} and onReset={...}, and asserts each body calls the right onboarding.* method. Verified: flipping markCompletereset in App.tsx makes Case 7 fail.

Tracking docs: Review/sprint14-verification.md (deploy + test commands + 5-risk table + open Q1), Review/ui-nielsen-audit.md Sprint 14 status block, fix-ui-audit.md T7.1T7.5, this file, docs/HANDOFF.md Sprint 14 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)
11 (committed 2026-06-05) Wire the dead "Generate Meal Plan" empty-state CTA — meals.create + meals.fillEmptySlots; race-safe; reusable for F8/F9. green ⚠️ committed; awaiting user deploy (frontend-only)
12 (committed 2026-06-05) F8 Spoonacular search — "Search the web" toggle on /recipes + Import button. New GET /api/recipes/search + POST /api/recipes/import. Quota-gated (140pt/day). green ⚠️ committed; awaiting user deploy (backend + frontend)
13 (committed 2026-06-05) F9-lite Ollama Cloud plan synthesis — prompt modal on Dashboard CTA. New POST /api/llm/plan. Library-or-LLM radio; 60s timeout tolerance; library fills the rest. green ⚠️ committed; awaiting user deploy (backend + frontend)

All work is on main ahead of origin/main (pre-existing WIP also present). All 13 sprints compile. Sprint 1 is live. Sprints 2-13 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.mddocs/HANDOFF.mdReview/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:

# 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), Sprint 10 (Deny Forever on Recipes), Sprint 11 (Wire the dead "Generate Meal Plan" CTA), Sprint 12 (F8 Spoonacular search), and Sprint 13 (F9-lite Ollama Cloud plan synthesis) committed on 2026-06-05, awaiting user deploy; Sprint 14 (Vitest for useOnboarding) code complete, 7/7 tests green, awaiting commit + push. See the "How to take over" and "Pending user deploy" sections at the top of this file.