From dac1364c295c1fca5d75a4e582382e5ece638c77 Mon Sep 17 00:00:00 2001 From: Peter Woolery Date: Fri, 5 Jun 2026 15:36:26 -0700 Subject: [PATCH] =?UTF-8?q?docs:=20Sprint=2011=20=E2=80=94=20wire=20the=20?= =?UTF-8?q?dead=20"Generate=20Meal=20Plan"=20CTA=20across=20all=206=20runn?= =?UTF-8?q?ing=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sprint 11 (commit 41154e9) wires the previously-dead "Generate Meal Plan" empty-state CTA on the Dashboard to two existing endpoints (POST /api/meals + POST /api/meals/{id}/fill- empty-slots). No backend changes; no new dependencies. The handler lives on the client for now; future F8 (Spoonacular) + F9 (Ollama) will swap the fillEmptySlots call for an LLM call without changing the DOM. F8 + F9 remain in the §Future backlog. This commit updates the 6 running docs that track the sprint: - .agent/plan.md — Sprint 11 section (S11.1-S11.3) added. - .agent/context.md — Sprint 11 (D1-D6, Q1-Q3) added; file:line references; key takeaways. - Review/sprint11-verification.md — new file: 4-step browser smoke + race test + 2 API curls + a11y check + risks + future work section. - Review/ui-nielsen-audit.md — Sprint 11 status block (T5.1-T5.3) at the top, after the Sprint 10 block. - fix-ui-audit.md — Sprint 11 section (T5.1-T5.5) added after the Sprint 10 section. - Review/handoff-ui-audit.md — Batch G added to the deploy instructions; Sprint 11 section added after Sprint 10; TL;DR table row 11 added; Last-updated footer updated. - docs/HANDOFF.md — Sprint 11 section added after the Sprint 10 section, with a path-forward paragraph for F8/F9. All 6 docs now reflect Sprint 11. §Future backlog remaining: F8 (Spoonacular) + F9 (Ollama) proposals, both full backend work. --- .agent/context.md | 54 ++++++++++++++++ .agent/plan.md | 54 ++++++++++++++++ Review/handoff-ui-audit.md | 28 ++++++-- Review/sprint11-verification.md | 109 ++++++++++++++++++++++++++++++++ Review/ui-nielsen-audit.md | 6 ++ docs/HANDOFF.md | 21 +++++- fix-ui-audit.md | 41 ++++++++++++ 7 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 Review/sprint11-verification.md diff --git a/.agent/context.md b/.agent/context.md index 85ba684..82faba8 100644 --- a/.agent/context.md +++ b/.agent/context.md @@ -288,3 +288,57 @@ User direction 2026-06-05: "Proceed with the next phase in the redesign. Also ad - `frontend/src/pages/RecipeDetail.tsx:73-78` — top bar (Deny forever button group) - `Review/sprint10-verification.md` — new file (deploy + 9-step browser smoke + 5 API curls + a11y check) +--- + +# Context — Sprint 11 (Wire the dead "Generate Meal Plan" CTA) + +## Why Sprint 11 exists + +User direction 2026-06-05: "Proceed." Selected from the question menu as the smallest remaining §Future item. F1 (Sprint 9) shipped, F8 (Spoonacular) + F9 (Ollama) are full backend proposals, and the dead `Generate Meal Plan` CTA at `Dashboard.tsx:503` was the last remaining piece. The button renders with `onClick: () => {}` — clicking it does nothing. The backend already has the two endpoints needed (`POST /api/meals` to create a plan + `POST /api/meals/{id}/fill-empty-slots` to fill it from the recipe library), so the wiring is a 25-line client-side glue function. No backend changes. No new dependencies. F8/F9 remain future sprints that will swap the recipe-library-based fill for an LLM/Spoonacular-based generation. + +## Decisions (locked in for Sprint 11) + +- **D1. Wire to existing endpoints, no new backend route.** `POST /api/meals` (creates an empty plan) + `POST /api/meals/{id}/fill-empty-slots` (fills with library recipes). The `fillEmptySlots` partial-success report pattern is already in production for the existing `Plan Week` menu at `Dashboard.tsx:366-392`. Reusing the same toast messaging keeps the UX consistent. +- **D2. Client-side orchestration, not a new server endpoint.** A combined `POST /api/meals/generate` endpoint would be cleaner long-term (atomic, single source of truth for "this is how a meal plan is generated"), but it would duplicate `fillEmptySlots` logic and lock in a generation strategy before F8/F9 are decided. Keeping the orchestration on the client means F8/F9 only need to swap the `fillEmptySlots` call for a future `LLMGenerate` call. +- **D3. Handle the "already exists" race.** Two tabs clicking "Generate Meal Plan" at the same moment: the second `meals.create` returns 400 with `detail: "Meal plan for this week already exists"`. Fall through to `getPlanned(weekStart)` to get the existing plan id, then call `fillEmptySlots` against it. Same end result, no error toast. +- **D4. Reuse the partial-success toast format from `handlePlanWeek`.** `"Planned N of M meals"` on full success, `"Planned N of M — K failed (e.g. )"` on partial, `"No empty meals to fill"` on 0/0. The user already knows this toast shape. +- **D5. Track `generatingFirstPlan` state.** Swap the button label to `"Generating…"` and disable it while in-flight, matching the existing `planningWeek` state pattern at `Dashboard.tsx:363`. +- **D6. Path forward to F8/F9: the `EmptyState.action.onClick` is the single seam.** Future F8 (Spoonacular) or F9 (Ollama) work only needs to swap the function called by `onClick`. No DOM, copy, or component structure changes needed. + +## Open questions to surface to the user, not to assume + +- **Q1. Should the empty state show a meal-type picker ("Breakfast / Lunch / Dinner" toggles) before generating, or always generate all three?** Default: always generate all three (matching the existing `Plan Week` menu default). Surfacing a picker adds 3 checkboxes and a "Generate N meals" button; small but a separate UI decision. If you want it, it's a 5-line addition to `handleGenerateFirstPlan`. +- **Q2. Should the CTA be hidden entirely if the recipe library is empty?** Default: show it, and let it fail gracefully. The backend's `fillEmptySlots` returns `failed=[]` for every slot with reason "No recipes available" when the library is empty. The UI toast surfaces this. A library-empty case is rare in practice (admin seeds the library), and hiding the button would leave the user with no path forward. +- **Q3. Should the path forward to F8/F9 add a `source: 'library' | 'spoonacular' | 'ollama'` field to the meal plan to record which strategy was used?** Default: no. The current `MealPlan` table has no such field. Adding it is a Sprint 12+ change if F8/F9 ship. + +## Sprint 11 verification gate + +- `cd frontend && npm run build` → green (tsc 0 errors, vite 0 errors) +- Browser smoke (4 steps) on `http://100.108.208.56:8082/` per `Review/sprint11-verification.md` +- Race test: two tabs clicking "Generate Meal Plan" simultaneously — both succeed +- No regression in Sprints 1-10 + +## Sprint 11 — does NOT touch + +- The OnboardingTour (Sprint 9) — unchanged. The tour's first step is the Dashboard's Weekly Overview card (`Dashboard.tsx:602`); the empty state with the CTA renders above the card and is a different element. No tour interaction needed. +- The NeverSuggestButton (Sprint 10) — unchanged. +- The 3-button Sprint 8 voting row — unchanged. +- The WeekRangeNav (Sprint 7) — unchanged. +- The bulk pantry add (Sprint 6 F3) — unchanged. +- The existing `handlePlanWeek` (Sprint 6 F4) — unchanged. That fills empty slots in an *existing* plan. Sprint 11 is the create-then-fill path. +- The keyboard shortcuts (Sprint 5 F2) — unchanged. +- The error toast / `showApiError` flow (Sprint 4 F7) — used for the error path; unchanged. +- Pre-existing WIP: `backend/app/api/recipes.py`, `backend/app/schemas/recipe.py`, `nginx/nginx.conf` — untouched. +- Backend code: no changes. The two endpoints already exist and are well-tested. + +## Key file:line references (Sprint 11) + +- `frontend/src/pages/Dashboard.tsx:366-392` — existing `handlePlanWeek` (model for the new handler) +- `frontend/src/pages/Dashboard.tsx:499-504` — `EmptyState` with the dead CTA (target) +- `frontend/src/pages/Dashboard.tsx:393-396` — `useQuery` for `['mealPlan', weekStart]` (invalidation target) +- `frontend/src/api/index.ts:38-65` — `meals` API client (already has `create` + `fillEmptySlots`) +- `backend/app/api/meals.py:159-195` — `POST /api/meals` (create) +- `backend/app/api/meals.py:693+` — `POST /api/meals/{id}/fill-empty-slots` +- `backend/app/schemas/__init__.py:227-247` — `MealPlanBase` + `MealPlanCreate` schemas +- `Review/sprint11-verification.md` — new file (deploy + 4-step browser smoke + race test) + diff --git a/.agent/plan.md b/.agent/plan.md index b53c572..7697263 100644 --- a/.agent/plan.md +++ b/.agent/plan.md @@ -285,4 +285,58 @@ User reported post-deploy: "The tour window looks great, but Clicking the X nor - A "Manage blocked recipes" page. - Bulk unblock. - Touch-device gesture for the card overlay (the focus state already surfaces the button on tap). + +--- + +## Sprint 11 — Wire the dead "Generate Meal Plan" CTA (Dashboard.tsx:499-504) + +**User direction (2026-06-05):** "Proceed." Selected from the question menu as the smallest §Future item. F1 (Sprint 9) is shipped, F8 (Spoonacular) + F9 (Ollama) are full backend proposals, and the dead `Generate Meal Plan` CTA at `Dashboard.tsx:503` is the final remaining item. The button is rendered with `onClick: () => {}` — clicking it does nothing. Wired to existing endpoints, no backend changes, no new dependencies. + +**Root cause:** the user lands on the Dashboard with no meal plan and sees a "Generate Meal Plan" button. Clicking it does nothing. The backend already has the two endpoints needed (`POST /api/meals` to create a plan + `POST /api/meals/{id}/fill-empty-slots` to fill it from the recipe library), and the `fillEmptySlots` partial-success report pattern is already in production for the existing `Plan Week` menu (`handlePlanWeek` at `Dashboard.tsx:366-392`). The wiring is a 25-line client-side glue function that calls both in sequence. + +### S11.1 — `handleGenerateFirstPlan()` in `Dashboard.tsx` + +- [ ] Add a new handler next to `handlePlanWeek` (line 366) that: + 1. Reads `weekStart` (already in scope). + 2. `POST /api/meals` with `{ week_start_date: weekStart, status: 'draft' }` to create an empty plan. + 3. On success, `POST /api/meals/{newId}/fill-empty-slots` with `{ meal_types: ['breakfast', 'lunch', 'dinner'] }`. + 4. Invalidate `['mealPlan', weekStart]`. + 5. Toast: reuse the same partial-success pattern as `handlePlanWeek` (`"Planned N of M meals — K failed"`). +- [ ] Wire `onClick` of the `EmptyState.action` (line 503) to call `handleGenerateFirstPlan()`. +- [ ] Track a `generatingFirstPlan` state for the loading spinner; swap the button label to `"Generating…"` while in-flight. +- [ ] Handle the `"Meal plan for this week already exists"` 400 from `meals.create` (race condition with another tab) by calling `fillEmptySlots` directly with the existing plan's id — refetch the plan from `getPlanned(weekStart)` to get the id. + +### S11.2 — Verify + +- [ ] `npm run build` green (tsc 0 errors, vite 0 errors). +- [ ] Browser smoke (4 steps) on `http://100.108.208.56:8082/`: + 1. Log in as a family with no meal plan for the current week. Land on `/`. + 2. Confirm `EmptyState` shows "Generate Meal Plan" button. + 3. Click the button. Confirm: button label flips to "Generating…", toast appears with "Planned N of M meals", empty state disappears, plan grid renders. + 4. Refresh the page. Confirm the plan persists. +- [ ] Race test: open two tabs, both click "Generate Meal Plan" at the same moment. Second tab should still succeed (handled by the `meals.create` 400 → fall-through to `fillEmptySlots` path). +- [ ] No regression in Sprints 1-10. + +### S11.3 — Docs (all 6 running docs updated) + +- [ ] `Review/ui-nielsen-audit.md` — Sprint 11 status block at the top. +- [ ] `fix-ui-audit.md` — Sprint 11 plan section (T5.1-T5.3). +- [ ] `Review/handoff-ui-audit.md` — Sprint 11 entry in the "How to take over" section + TL;DR row. +- [ ] `docs/HANDOFF.md` — Sprint 11 section. +- [ ] `.agent/plan.md` — this section. +- [ ] `.agent/context.md` — Sprint 11 decisions + file:line references. +- [ ] `Review/sprint11-verification.md` — written (4-step browser smoke + race test). + +### Done when (Sprint 11) + +- All boxes above ticked. +- `npm run build` green. +- `Review/sprint11-verification.md` exists. +- All 6 doc files have a Sprint 11 status block. + +### Out of scope (Sprint 11) + +- LLM-powered generation (F8 Spoonacular, F9 Ollama) — separate backend proposals, future sprints. Sprint 11 only wires the existing recipe-library-based fill. +- A "what would you like for dinner?" prompt before generation — the existing flow generates from the library with no user input. +- A "regenerate" button after the plan exists — the existing `Plan Week` menu at `Dashboard.tsx:366-392` already handles this case. - F8 Spoonacular + F9 Ollama + dead `Generate Meal Plan` CTA — separate. diff --git a/Review/handoff-ui-audit.md b/Review/handoff-ui-audit.md index 2817486..c8bb3ab 100644 --- a/Review/handoff-ui-audit.md +++ b/Review/handoff-ui-audit.md @@ -19,14 +19,15 @@ If you are a new agent continuing this work, do this **in order**: - **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). + - **Batch G:** Sprint 11 (one `git pull`, `docker compose up -d --build frontend` — frontend-only, no migration, no backend rebuild). +4. **Open issues** in `.agent/plan.md` (the "Phase R1-R3" section is a prior plan; the **Sprint 11 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. +- The only remaining §Future items are F8 Spoonacular enrichment (proposal) and F9 Ollama LLM matcher (proposal). F1 (onboarding) shipped as Sprint 9; the dead `Generate Meal Plan` CTA shipped as Sprint 11. Both are deployment-pending. - Pre-existing repo issues: 1 failing test (`test_filter_blocks_by_cost` — verified pre-Sprint 8), `.gitignore` `lib/` bug, no CI. Documented. --- @@ -52,7 +53,7 @@ If you are a new agent continuing this work, do this **in order**: **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, 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".** +**Thread 3 (§Future backlog) is deferred** until S8 is deployed + verified. F8/F9 proposals remain. **Sprint 9 (F1 onboarding) + post-deploy fix (`1562929`), Sprint 10 (Deny Forever on Recipes), and Sprint 11 (wire the dead "Generate Meal Plan" CTA) are all committed 2026-06-05, awaiting user deploy.** ### Sprint 9 — F1 Onboarding Tour (H10) @@ -88,6 +89,22 @@ If you are a new agent continuing this work, do this **in order**: **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.1–T4.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.1–T5.5, this file, `docs/HANDOFF.md` Sprint 11 section. + +**Path forward to F8/F9:** the `EmptyState.action.onClick` is the single seam. Future F8 (Spoonacular) or F9 (Ollama) work only needs to swap the `fillEmptySlots` call in `handleGenerateFirstPlan` for an LLM call. No DOM, copy, or component structure changes needed. + ### 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. @@ -117,8 +134,9 @@ Twelve commits land all 14 audit findings + 6 §Future items + 2 user-driven spr | 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) | -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/`.** +All work is on `main` ahead of `origin/main` (pre-existing WIP also present). All 11 sprints compile. **Sprint 1 is live. Sprints 2-11 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. @@ -362,4 +380,4 @@ cd frontend && npm run build 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. +**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), and Sprint 11 (Wire the dead "Generate Meal Plan" CTA) 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. diff --git a/Review/sprint11-verification.md b/Review/sprint11-verification.md new file mode 100644 index 0000000..58a40e6 --- /dev/null +++ b/Review/sprint11-verification.md @@ -0,0 +1,109 @@ +# Sprint 11 — Wire the dead "Generate Meal Plan" CTA — verification + +**Status (2026-06-05):** ✅ Code complete. `npm run build` green. Awaiting user deploy. + +## Summary + +Sprint 11 wires the previously-dead `Generate Meal Plan` button on the Dashboard's empty state (`Dashboard.tsx:553-560` post-fix) to two existing backend endpoints: + +1. `POST /api/meals` — creates a fresh meal plan for the current week +2. `POST /api/meals/{id}/fill-empty-slots` — fills it with recipes from the library + +No backend changes. No new dependencies. ~50 lines of TypeScript + a 1-line addition to `EmptyState`'s `action` type to support an optional `disabled` flag. + +## Files changed + +- `frontend/src/pages/Dashboard.tsx` + - New `generatingFirstPlan` state (line ~365) + - New `handleGenerateFirstPlan` handler (lines ~395-449) + - `EmptyState.action` wired to `handleGenerateFirstPlan` (lines ~553-560) +- `frontend/src/components/ui/EmptyState.tsx` + - `action.disabled?: boolean` (optional, backward-compatible) + +## Build verification + +```text +vite v5.4.21 building for production... +transforming... +✓ 1897 modules transformed. +rendering chunks... +computing gzip size... +dist/index.html 0.54 kB │ gzip: 0.31 kB +dist/assets/index-DRrz7haU.css 41.90 kB │ gzip: 7.24 kB +dist/assets/index-DoYpJI6B.js 496.48 kB │ gzip: 152.49 kB +✓ built in 2.60s +``` + +- `tsc` 0 errors, `vite` 0 errors. +- Bundle: 495.64 → 496.48 kB (+0.84 kB, the new handler). + +## Browser smoke (4 steps) + +Run on `http://100.108.208.56:8082/`. Prerequisite: a family with no meal plan for the current week (delete via the admin UI or `psql ... DELETE FROM meal_plans WHERE family_profile_id = ...;`). + +1. **Land on `/` with no plan.** Confirm the `EmptyState` shows "No meal plan yet" + a "Generate Meal Plan" button (label = "Generate Meal Plan", button enabled). +2. **Click the button.** Within ~200ms the label flips to "Generating…" and the button becomes disabled (greyed out, `cursor: not-allowed`). +3. **Wait for the response (~500ms-2s).** Confirm: + - The empty state disappears, replaced by the meal-plan grid. + - The plan has 1-21 items (depends on the recipe library size and the `fillEmptySlots` algorithm). + - A toast appears in the top-right: either `Planned N meals` (green/success) or `Planned N of M meals — K failed (e.g. )` (red/error) or `Plan created — no recipes to add yet` (green/success, if the library is empty). +4. **Refresh the page.** Confirm the plan persists. The empty state does NOT re-appear. + +## Race test (manual, optional) + +Open two browser tabs side-by-side. Both land on `/` with no plan. Both show the "Generate Meal Plan" button. + +1. Click both buttons at the same time (or within ~50ms of each other). +2. Confirm both tabs end up with a plan on the page. +3. Open the browser DevTools Network tab and confirm one tab sent `POST /api/meals` (201 Created) and the other sent `POST /api/meals` (400 with detail "Meal plan for this week already exists") followed by `GET /api/meals?week_start=...` (200) and `POST /api/meals/{id}/fill-empty-slots` (200). +4. No error toast should appear in either tab. + +The race is handled by the `try/catch` around `meals.create` — the second tab falls through to `getPlanned(weekStart)` to get the existing plan's id, then calls `fillEmptySlots` against it. + +## API verification (optional, bypasses the UI) + +If you want to verify the two endpoints directly before testing in the browser: + +```bash +# 1) Create an empty plan for the upcoming Monday +curl -X POST http://100.108.208.56:8082/api/meals \ + -H 'Content-Type: application/json' \ + -d '{"week_start_date":"2026-06-08","status":"draft","items":[]}' +# → 201 Created, response has `id` + +# 2) Fill its empty slots from the library +curl -X POST http://100.108.208.56:8082/api/meals//fill-empty-slots \ + -H 'Content-Type: application/json' \ + -d '{"meal_types":["breakfast","lunch","dinner"]}' +# → 200 OK, response has `filled: [...]` + `failed: [...]` +``` + +The expected response shape for step 2 is `{ filled: FilledSlot[], failed: FailedSlot[] }` per `backend/app/api/meals.py:693+`. + +## A11y check + +- The button is a real `