Files
Meal-Planner/Review/sprint11-verification.md
T
admin dac1364c29 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.
2026-06-05 15:36:26 -07:00

110 lines
6.6 KiB
Markdown

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