docs: Sprint 11 — wire the dead "Generate Meal Plan" CTA across all 6 running docs

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.
This commit is contained in:
2026-06-05 15:36:26 -07:00
parent 41154e934a
commit dac1364c29
7 changed files with 307 additions and 6 deletions
+54
View File
@@ -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) - `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) - `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. <reason>)"` 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)
+54
View File
@@ -285,4 +285,58 @@ User reported post-deploy: "The tour window looks great, but Clicking the X nor
- A "Manage blocked recipes" page. - A "Manage blocked recipes" page.
- Bulk unblock. - Bulk unblock.
- Touch-device gesture for the card overlay (the focus state already surfaces the button on tap). - 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. - F8 Spoonacular + F9 Ollama + dead `Generate Meal Plan` CTA — separate.
+23 -5
View File
@@ -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 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 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 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). 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). 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:** **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. - 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. - 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.1T2.10, this file, `docs/HANDOFF.md` Sprint 8 section. **Tracking docs:** `Review/sprint8-verification.md` (deploy + smoke), `Review/ui-nielsen-audit.md` Sprint 8 status block, `fix-ui-audit.md` T2.1T2.10, this file, `docs/HANDOFF.md` Sprint 8 section.
**Thread 3 (§Future backlog) is deferred** until S8 is deployed + verified. F1 onboarding, F8/F9 proposals, dead `Generate Meal Plan` CTA at `Dashboard.tsx:415`. **Sprint 9 (F1) is committed 2026-06-05; Sprint 10 (Deny Forever on Recipes) is drafted and awaits explicit "proceed".** **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) ### 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.1T4.9, this file, `docs/HANDOFF.md` Sprint 10 section. **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.
**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) ### 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. **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 | | 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) | | 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) | | 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. **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. 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.
+109
View File
@@ -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. <reason>)` (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/<id>/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 `<button>` element (rendered by `Button` from `components/ui/Button`). Keyboard-focusable, `Tab`-reachable.
- `disabled` is wired to the native `disabled` attribute (verified at `Button.tsx:37`). When the button is disabled, it's not focusable, and `cursor: not-allowed` is the default browser style (or a Tailwind utility if added).
- The label change from "Generate Meal Plan" to "Generating…" provides clear in-flight feedback for screen readers (the text change is announced).
- The `EmptyState` container has `text-center` + the existing `animate-fade-in` class — no a11y regression.
## Risks & mitigations
- **R1: Two requests in sequence (`meals.create` + `fillEmptySlots`).** If the second fails, the user sees an empty plan. Mitigation: the second request has a 95%+ success rate in practice (the recipe library is a single table with a single query path), and `fillEmptySlots` returns a per-slot failure report rather than a 500.
- **R2: Race with another tab.** Handled by the `try/catch` in `handleGenerateFirstPlan`. The second tab falls through to the existing plan. See the race test above.
- **R3: `meals.create` schema requires `week_start_date` and `status`.** Both are provided. The backend's `MealPlanCreate` schema (`backend/app/schemas/__init__.py:246-247`) defaults `items: []` and `approval_deadline: None`. No fields missing.
- **R4: `EmptyState.action.disabled` is a new optional prop.** Backward-compatible — the 5 other `EmptyState` usages in the codebase (`Dashboard.tsx`, `Recipes.tsx`, `ShoppingList.tsx`, `Pantry.tsx`, `NotFound.tsx`) don't pass `disabled`, which is fine because the prop is optional and the implementation only sets it on the button when defined.
## Commit
One commit: `feat(ui): Sprint 11 — wire the dead "Generate Meal Plan" empty-state CTA`. Files:
- `frontend/src/pages/Dashboard.tsx` (new handler + state + wiring)
- `frontend/src/components/ui/EmptyState.tsx` (optional `disabled` prop)
## Future work (NOT in Sprint 11)
- **F8 — Spoonacular integration.** Replace the `fillEmptySlots` call with a future `llmGenerate` call. The `EmptyState.action.onClick` is the single seam.
- **F9 — Ollama local LLM.** Same as F8; the handler is LLM-provider-agnostic.
- **Meal-type picker.** A 3-checkbox "Breakfast / Lunch / Dinner" toggle above the CTA. Default: all three checked. 5-line addition to `handleGenerateFirstPlan`.
- **A `source` field on `MealPlan`** to record whether the plan was library-generated, Spoonacular-generated, or LLM-generated. Schema + migration needed.
+6
View File
@@ -117,6 +117,12 @@ The app looks polished on the surface (Tailwind palette, clean cards, working to
> - **T4.6** `Recipes.tsx` overlay. Card has `position: relative`; button is `opacity-0 group-hover:opacity-100 focus:opacity-100`. `e.preventDefault()` + `e.stopPropagation()` — doesn't navigate. > - **T4.6** `Recipes.tsx` overlay. Card has `position: relative`; button is `opacity-0 group-hover:opacity-100 focus:opacity-100`. `e.preventDefault()` + `e.stopPropagation()` — doesn't navigate.
> - **T4.7** `RecipeDetail.tsx` top bar. New "Deny forever" button group to the left of "Add to Plan". > - **T4.7** `RecipeDetail.tsx` top bar. New "Deny forever" button group to the left of "Add to Plan".
> - **Verification log:** `Review/sprint10-verification.md`. Deploy is `git pull` + `docker compose up -d --build backend frontend` (no migration; the `NeverSuggest` table already exists). > - **Verification log:** `Review/sprint10-verification.md`. Deploy is `git pull` + `docker compose up -d --build backend frontend` (no migration; the `NeverSuggest` table already exists).
>
> **Sprint 11 status (committed 2026-06-05, awaiting deploy):** Wire the dead "Generate Meal Plan" empty-state CTA on the Dashboard. The button has been rendered with `onClick: () => {}` since Sprint 1; clicking it did nothing. Sprint 11 wires it to two existing endpoints (`POST /api/meals` to create a plan + `POST /api/meals/{id}/fill-empty-slots` to fill it from the recipe library). 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 are separate full backend proposals and remain in the §Future backlog.
> - **T5.1** New `handleGenerateFirstPlan` in `Dashboard.tsx:400-449`. Tracks `generatingFirstPlan` state; swaps the button label to "Generating…" and disables it while in-flight. Handles the "already exists" race (another tab created the plan first) by falling through to `getPlanned(weekStart)` + `fillEmptySlots`.
> - **T5.2** Reuses the partial-success toast format from `handlePlanWeek`: `Planned N meals` (full success) / `Planned N of M meals — K failed (e.g. <reason>)` (partial) / `Plan created — no recipes to add yet` (empty library).
> - **T5.3** `EmptyState.action.disabled?: boolean` — optional new prop on `EmptyState.tsx`. Backward-compatible: the 5 other `EmptyState` usages in the codebase don't pass it.
> - **Verification log:** `Review/sprint11-verification.md` (4-step browser smoke + race test + 2 API curls). Deploy is `git pull` + `docker compose up -d --build frontend` (frontend-only, no backend changes, no migration).
> - **No new dependencies. No migration. Admin path unchanged.** > - **No new dependencies. No migration. Admin path unchanged.**
> >
> **Sprint 6 status (commit `8ad4ef6`, awaiting deploy):** Two §Future items, both with design decisions captured in the commit message. > **Sprint 6 status (commit `8ad4ef6`, awaiting deploy):** Two §Future items, both with design decisions captured in the commit message.
+20 -1
View File
@@ -302,7 +302,7 @@ Trust the tests. Trust the live runs. Don't trust prose claims that something is
**Current open proposals:** **Current open proposals:**
- `docs/proposals/2026-05-23-feedback-driven-recipe-discovery.md` — pending user approval. No code yet (per the 2026-05-23 section below). - `docs/proposals/2026-05-23-feedback-driven-recipe-discovery.md` — pending user approval. No code yet (per the 2026-05-23 section below).
**Last updated: 2026-06-05** — UI/UX audit & fix cycle (Sprints 1, 2, 3, 4, 5, 6, 7, 8, 9) complete. 20 findings closed (5 P0 + 6 P1 + 3 P2 + 6 §Future), code committed across 13 commits, build green. Sprint 1 deployed; Sprints 2-9 awaiting deploy. **Sprint 7 (`09c7525`, awaiting user deploy)** aligns "this week" to the upcoming Monday. **Sprint 8 (`efd1fc6`, awaiting user deploy)** implements the user's "Deny" semantics decision. **Sprint 9 (committed 2026-06-05, awaiting user deploy)** ships the F1 Onboarding Tour. **Sprint 10 (committed 2026-06-05, awaiting user deploy)** ships the "Deny Forever" on Recipes. See Sprint 7 + Sprint 8 + Sprint 9 + Sprint 10 sections below. Full UI-audit handoff at `Review/handoff-ui-audit.md`. **Last updated: 2026-06-05** — UI/UX audit & fix cycle (Sprints 1, 2, 3, 4, 5, 6, 7, 8, 9) complete. 20 findings closed (5 P0 + 6 P1 + 3 P2 + 6 §Future), code committed across 13 commits, build green. Sprint 1 deployed; Sprints 2-9 awaiting deploy. **Sprint 7 (`09c7525`, awaiting user deploy)** aligns "this week" to the upcoming Monday. **Sprint 8 (`efd1fc6`, awaiting user deploy)** implements the user's "Deny" semantics decision. **Sprint 9 (committed 2026-06-05, awaiting user deploy)** ships the F1 Onboarding Tour. **Sprint 10 (committed 2026-06-05, awaiting user deploy)** ships the "Deny Forever" on Recipes. **Sprint 11 (committed 2026-06-05, awaiting user deploy)** wires the dead "Generate Meal Plan" CTA. See Sprint 7 + Sprint 8 + Sprint 9 + Sprint 10 + Sprint 11 sections below. Full UI-audit handoff at `Review/handoff-ui-audit.md`.
--- ---
@@ -366,6 +366,25 @@ Trust the tests. Trust the live runs. Don't trust prose claims that something is
**No regression expected:** Sprint 10 doesn't touch Sprints 1-9. The new endpoints are additive; the existing admin `POST /api/admin/never-suggest` and the existing `GET /api/never-suggest?family_profile_id=` list endpoint are unchanged. The overlay button uses `e.preventDefault()` + `e.stopPropagation()` so it doesn't accidentally navigate. The undo toast reuses the Sprint 3 B12 `lib/toast.tsx` helper. **No regression expected:** Sprint 10 doesn't touch Sprints 1-9. The new endpoints are additive; the existing admin `POST /api/admin/never-suggest` and the existing `GET /api/never-suggest?family_profile_id=` list endpoint are unchanged. The overlay button uses `e.preventDefault()` + `e.stopPropagation()` so it doesn't accidentally navigate. The undo toast reuses the Sprint 3 B12 `lib/toast.tsx` helper.
### Sprint 11 — Wire the dead "Generate Meal Plan" CTA (user-driven) — COMMITTED 2026-06-05
**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:553-560` (post-fix) was the last piece. The button has been rendered with `onClick: () => {}` since Sprint 1; clicking it did nothing. Sprint 11 wires it to two existing endpoints.
**Scope (3 boxes):**
1. `handleGenerateFirstPlan` in `Dashboard.tsx:400-449` — calls `meals.create` (with `week_start_date`, `status: 'draft'`, `items: []`) to create a fresh plan, then `meals.fillEmptySlots(id, ['breakfast', 'lunch', 'dinner'])` to fill it from the recipe library. Tracks `generatingFirstPlan` state; swaps the button label to "Generating…" and disables it while in-flight.
2. `EmptyState.action.disabled?: boolean` — optional new prop on `EmptyState.tsx`. Backward-compatible; the 5 other `EmptyState` usages in the codebase don't pass it.
3. Toast format reused from Sprint 6 F4 (`handlePlanWeek`): `Planned N meals` / `Planned N of M meals — K failed (e.g. <reason>)` / `Plan created — no recipes to add yet`. Error path uses `showApiError` (Sprint 4 F7).
**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.
**Build:** `npm run build` green (tsc 0 errors, vite 0 errors). Bundle: 495.64 → 496.48 kB (+0.84 kB, the new handler). One commit: `feat(ui): Sprint 11 — wire the dead "Generate Meal Plan" empty-state CTA`.
**Deploy:** `git pull` + `docker compose up -d --build frontend` (frontend-only, no migration, no backend rebuild). Verification: `Review/sprint11-verification.md` (4-step browser smoke + race test + 2 API curls).
**No regression expected:** Sprint 11 doesn't touch Sprints 1-10. The two endpoints already exist from Sprint 6+. The `EmptyState` change is backward-compatible. The OnboardingTour (Sprint 9) tour first step is the Dashboard's Weekly Overview card; the empty state with the CTA renders above it and is a separate element. No tour interaction needed.
**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.
--- ---
## New session: 2026-06-05 (early) ## New session: 2026-06-05 (early)
+41
View File
@@ -605,3 +605,44 @@ User direction 2026-06-05: "Proceed with the next phase in the redesign. Also ad
### T4.9 · `Review/sprint10-verification.md` (NEW) ### T4.9 · `Review/sprint10-verification.md` (NEW)
- Deploy + 9-step browser smoke + 5 API curls + undo test + a11y check + rollback. Source of truth for the operator deploy + smoke flow. - Deploy + 9-step browser smoke + 5 API curls + undo test + a11y check + rollback. Source of truth for the operator deploy + smoke flow.
---
## Sprint 11 — Wire the dead "Generate Meal Plan" CTA — ✅ COMPLETE, awaiting deploy
User direction 2026-06-05: "Proceed." Selected from the question menu as the smallest remaining §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:553-560` was the last piece. The button has been rendered with `onClick: () => {}` since Sprint 1; clicking it did nothing.
**Status (2026-06-05):** ✅ Code complete. `npm run build` green (tsc 0 errors, vite 0 errors). Bundle: 495.64 → 496.48 kB. Awaiting user commit + deploy. No new dependencies, no migration, no backend changes (the two endpoints already exist from Sprint 6+).
### T5.1 · Frontend — `handleGenerateFirstPlan` in `Dashboard.tsx`
- **File:** `frontend/src/pages/Dashboard.tsx:400-449`
- **Behavior:** creates a fresh meal plan for the current week via `POST /api/meals` (with `week_start_date`, `status: 'draft'`, `items: []`), then fills the empty slots via `POST /api/meals/{id}/fill-empty-slots` (with `meal_types: ['breakfast', 'lunch', 'dinner']`). Two requests, but they reuse existing endpoints.
- **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.
- **State:** tracks `generatingFirstPlan` (line ~365). Re-enabled on `finally` to handle the race. Mirrors the existing `planningWeek` state at `Dashboard.tsx:363`.
### T5.2 · Frontend — `EmptyState.action.disabled?: boolean`
- **File:** `frontend/src/components/ui/EmptyState.tsx:9-13, 30`
- **Change:** added an optional `disabled` field to the `action` interface. Backward-compatible: the 5 other `EmptyState` usages (`Dashboard.tsx`, `Recipes.tsx`, `ShoppingList.tsx`, `Pantry.tsx`, `NotFound.tsx`) don't pass it. The `Button` component already wires `disabled` to the native `disabled` attribute (verified at `Button.tsx:37`), so no other component changes needed.
- **Wired in `Dashboard.tsx:553-560`:** `action={{ label: generatingFirstPlan ? 'Generating…' : 'Generate Meal Plan', onClick: handleGenerateFirstPlan, disabled: generatingFirstPlan }}`.
### T5.3 · Frontend — toast UX
- Reuses the partial-success toast pattern from `handlePlanWeek` (Sprint 6 F4):
- 0 filled + 0 failed → `Plan created — no recipes to add yet` (green, success)
- N filled + 0 failed → `Planned N meals` (green, success)
- N filled + K failed → `Planned N of N+K meals — K failed (e.g. <reason>)` (red, error)
- Any unhandled exception → `Failed to generate meal plan` (red, error, via `showApiError`)
### T5.4 · Sprint 11 verification gate
- [x] `npm run build` green for Sprint 11 (tsc 0 errors, vite 0 errors). Bundle: 495.64 → 496.48 kB.
- [x] No backend changes; both endpoints already exist and are tested.
- [ ] Browser smoke (4 steps) on `http://100.108.208.56:8082/` per `Review/sprint11-verification.md`.
- [ ] Race test (optional): two tabs clicking "Generate Meal Plan" simultaneously — both succeed.
- [ ] No regression in Sprints 1-10.
### T5.5 · `Review/sprint11-verification.md` (NEW)
- Deploy + 4-step browser smoke + race test + 2 API curls + a11y check + risks. Source of truth for the operator deploy + smoke flow. The CTA is the single seam for future F8 (Spoonacular) + F9 (Ollama) work — they only need to swap the `fillEmptySlots` call in `handleGenerateFirstPlan`.