Files
Meal-Planner/Review/handoff-ui-audit.md
T
MealPlanner 09c7525a12 fix(ui): align 'this week' to upcoming Monday (Sprint 7)
User report 2026-06-05: 'webui Meal Planner page is empty' on Friday
morning after the Friday email went out. Root cause: the orchestrator
keyed plans by the most-recent-Friday while the frontend's isoMonday()
returned the most-recent-Monday — a 7-day mismatch on Fridays.

Fixes (one semantic across the stack):
- runner._current_week_start() returns the upcoming Monday (today if
  Mon, else the next Mon). The Friday email subject
  ('Meal plan for week of <date>') automatically picks up the new
  value via run.week_start_date.
- frontend isoMonday -> upcomingMonday (same logic; renamed for
  intent). isoMonday kept as a deprecated alias.
- New WeekRangeNav component (Dashboard + ShoppingList share it).
  Renders [<]  Jun 8 - Jun 14  [>] with clickable chevrons and a
  clickable range label that jumps to the upcoming week. Replaces
  the Sprint 5 inline segmented control on both pages.
- New formatWeekRange(mondayIso) helper (UTC-stable; uses
  timeZone: 'UTC' so the rendered date matches the stored ISO date
  regardless of viewer TZ; closes a latent bug in formatIsoDate too).
- New SQL fix script that retargets the user's 3-pending-items plan
  from 2026-06-05 (Friday-keyed) to 2026-06-08 (upcoming Monday).
  Idempotent + transaction-wrapped. Optional block for 2026-05-29.

No backend migration. No new dependencies. Deploy is git pull +
run the SQL fix + docker compose up -d --build backend frontend.
See Review/sprint7-verification.md for the full deploy + smoke flow.

Files:
- backend/app/services/orchestrator/runner.py:20-35
- backend/scripts/fix_2026_06_05_to_2026_06_08.sql (new)
- frontend/src/lib/utils.ts:43-130
- frontend/src/components/WeekRangeNav.tsx (new)
- frontend/src/pages/Dashboard.tsx (3 call sites + 1 segmented control)
- frontend/src/pages/ShoppingList.tsx (5 call sites + 2 segmented controls)
- Review/{sprint7-verification,ui-nielsen-audit,handoff-ui-audit}.md
- fix-ui-audit.md
- docs/HANDOFF.md
- .agent/{plan,context}.md
2026-06-05 07:46:55 -07:00

282 lines
21 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 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 7 in progress).**
---
## ⚠️ 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.1S7.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`) — `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** — Sprints 1, 2, 3, 4, 5, 6 deployed-or-awaiting-deploy; **Sprint 7 (webui empty-meal-plan fix) in progress**. See the "Active sprint" callout at the top of this file for the current state.