User policy decision (2026-06-05, exact): 'Hard filter. If it is denied
this week twice, it should be considered denied for good.'
The planner had no cross-week memory of denials: a denial on
meal_plan_item.approval_status was never consulted by the planner,
and NeverSuggest (the per-family permanent blocklist) was empty for
the user. The 'Roasted Sweet Potato and Chickpea Bowl' the user
denied on 2026-05-15 was still in the planner's pool 3 weeks
later.
Implements C + Z (explicit two-button model + soft-decay +
hard-filter escalation):
- Approve: untouched.
- Deny this week (1st in 90d): denial_expires_at = now() + 90d.
- Deny this week (2nd in 90d, server-side auto-escalation):
denial_expires_at = NULL + a NeverSuggest row written.
- Never again (explicit): same as the 2nd-time auto-escalation.
Both soft and permanent denials are hard filters in the planner
(per user). A denied recipe never reappears until either the 90d
window expires or the user un-blocks via the NeverSuggest API.
Changes:
- Migration 0016: meal_plan_item.denial_expires_at (partial index)
and meal_plan_vote.denial_scope.
- 3 backend helpers (_apply_denial, _ensure_never_suggest_recipe,
_has_prior_active_soft_denial) — single source of truth for the
deny path.
- POST /api/meals/items/{id}/deny?scope=this_week|never_again
(default this_week). Returns promoted_to_permanent.
- POST /api/meals/vote/{id} extended: vote=approve|deny|never_again.
Returns denial_scope + promoted_to_permanent.
- GET /api/meals/vote/{id} HTML page renders 3 buttons; supports
one-click ?scope=... for email direct-action links.
- Email template (step_email): 3 direct-action links per recipe
plus a secondary 'open vote page' link.
- Planner: _load_blocklists returns 3 sets; soft_denied_recipes
is hard-filtered (union with blocked_recipes at the call site).
- Frontend: MealCard renders 3 buttons (Approve / Deny this week
/ Never again) for pending items. handleDeny is scope-aware;
toast reflects promoted_to_permanent. window.confirm on
'Never again' prevents accidental permanent blocks.
Verification:
- npm run build green.
- 21/21 planner tests pass (1 pre-existing test_filter_blocks_by_cost
failure is NOT introduced by Sprint 8 — verified via git stash).
- Review/sprint8-verification.md: 11-step browser smoke + 4 API
curls + email-render procedure + rollback.
Files:
- backend/alembic/versions/0016_denial_decay_and_scope.py (new)
- backend/app/models/__init__.py:221-242, 250-269
- backend/app/schemas/__init__.py:204-219, 248-269
- backend/app/api/meals.py:30-138 (helpers), 240-330 (HTML page),
380-455 (submit_vote), 486-552 (deny_meal_item)
- backend/app/services/orchestrator/steps.py:283-300
- backend/app/services/planner/generate.py:59-99, 150-194
- frontend/src/api/index.ts:48-58
- frontend/src/pages/Dashboard.tsx:38-50, 385-410
- Review/{sprint8-verification,ui-nielsen-audit,handoff-ui-audit}.md
- fix-ui-audit.md
- docs/HANDOFF.md
- .agent/{plan,context}.md
Deploy (user runs on deployment host):
cd ~/MealPlanner && git pull
docker compose exec backend alembic upgrade head
docker compose -f docker-compose.yml up -d --build backend frontend
22 KiB
UI/UX Audit & Fix — Agent Handoff
You are taking over a 6-sprint UI/UX audit and fix cycle. All code changes are 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-04. Last updated: 2026-06-05 (Sprint 8 in progress).
⚠️ Active sprint: Sprint 8 — "Deny" semantics (C + Z, hard-filter escalation)
Status: in progress. User approved on 2026-06-05. Code not yet committed.
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+ aNeverSuggestrow 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.1–T2.10, Review/handoff-ui-audit.md (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.
⚠️ Active sprint: Sprint 7 — Fix webui "empty meal plan" (date-semantics mismatch)
Status: in progress. User approved on 2026-06-05. Code not yet written.
Root cause (one-liner): the orchestrator plans the upcoming Mon-Sun week (Fri 2026-06-05 → key 2026-06-08), but the frontend isoMonday() returns the current Mon-Sun (Fri 2026-06-05 → 2026-06-01). Email + DB + webui disagree by 7 days. User sees an empty page.
Scope (7 boxes): see .agent/plan.md "Active sprint" section. Code changes are small (1 backend function, 1 frontend util rename, 1 new WeekRangeNav component, 2 call-site updates) plus 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.1–S7.6, docs/HANDOFF.md Sprint 7 section. All will be filled in before the user pulls.
Thread 2 + Thread 3 are deferred (cross-week "rejected" semantics + §Future backlog) until S7 is deployed + verified.
TL;DR
Ten commits land all 14 audit findings + 6 of the §Future items:
| 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) |
All work is on main ahead of origin/main (pre-existing WIP also present). All six sprints compile. Sprint 1 is live. Sprints 2, 3, 4, 5, 6 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, and Review/sprint6-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). After verification, any remaining items move to the §Future backlog in fix-ui-audit.md (F1 onboarding tour, F8 Spoonacular enrichment, F9 Ollama matcher — F3, F4, F5, F6, F7 now done across Sprints 4-6).
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) —qtyfield; alsoing.ingredient?.namefallback. - B3
$N/A per serving(MealDetail.tsx:191) — conditional oncost != null. - B4
/recommendedblank page —*NotFound catch-all +/recommendedalias to/recipes/recommended. Newpages/NotFound.tsx.EmptyStateextended with optionaltoprop. - 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()inlib/utils.tsstrips 14 boilerplate patterns, raw text → "Notes from source" disclosure. - B8 Pantry aisle free-text → canonical select —
PANTRY_AISLESenum,Selectcomponents, ingredient name marked*. Backend migration0015_normalize_pantry_aisles.pynormalizesingredient.aisle+grocery_item.aislein a single transaction; dry-run atbackend/scripts/dry_run_aisle_migration.sql; persistent backup atbackend/scripts/persist_aisle_backup.sql. - B9 ShoppingList snake_case aisle labels —
AISLE_LABELmap +aisleDisplay(). - B10 Mobile pantry table —
role="region"+ right-edge white-to-transparent gradient hint. - B11 Recipes filters —
applied/pendingstate 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.tsfor JSX) gainsshowToast.undo(message, onUndo, ms=5000). Dashboard undo re-firesgenerateItemto refill the slot (recipe may differ — documented in plan §R4). Pantry undo re-firespantry.addwith 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">,Badgeextended withicon+aria-labelprops, approval-status Badge usesaria-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
-
S2.3 / B8 backend migration NOT YET RUN on deployment host. The user must run
alembic upgrade headaftergit 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 canDROPafter confidence is established. -
S3.1 / B12 Dashboard Undo is "rebuild, not restore".
meals.generateItemre-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). -
frontend/src/lib/toast.tsxis force-added because of thelib/gitignore bug. Future agents should be aware: any new file infrontend/src/lib/needsgit add -f. Consider fixing the.gitignore(use/lib/or specific Python paths instead oflib/) as a small follow-up. -
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. -
scripts/dry_run_aisle_migration.sqlis read-only but reads from prod. If the user runs it viadocker 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 (— done in Sprint 5/,g p,g s,n m)F3. Bulk add on Pantry/Shopping List (H7)— done in Sprint 6 (ShoppingList only)F4. Plan-the-whole-week button (H7)— done in Sprint 6F5. Persistent week selector in URL— done in Sprint 5F6. Badge a11y for color-only signals (generalized)— done in Sprint 4F7. Global— done in Sprint 4react-queryonErrortoast handler- 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 Planempty-state CTA atDashboard.tsx:415to 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.mddeploy 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 fixesfeat(ui): …for new behaviorrefactor(frontend): …for restructuring without behavior changedocs(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 — Sprints 1, 2, 3, 4, 5, 6 deployed-or-awaiting-deploy; Sprint 7 (webui empty-meal-plan fix) committed 09c7525 awaiting user deploy; Sprint 8 (Deny semantics C + Z with hard-filter escalation) in progress. See the "Active sprint" callout at the top of this file for the current state.