# 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 `