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

6.6 KiB

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

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:

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