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
+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 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.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)
@@ -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.
### 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)
**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.
+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.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).
>
> **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.**
>
> **Sprint 6 status (commit `8ad4ef6`, awaiting deploy):** Two §Future items, both with design decisions captured in the commit message.