# Sprint 4 — Deploy & smoke-check (F7 + F6) **Goal:** verify the global react-query error handler and plan-status aria-label work end-to-end on `http://100.108.208.56:8082/`. **Commit:** `d71b67a` (`feat(ui): global react-query error handler + plan-status a11y (Sprint 4 F7+F6)`) **Sprint scope:** F7 + F6 (both small, both polish, no new deps) **Backend changes:** none **Frontend changes:** `App.tsx`, `lib/toast.tsx`, `pages/Dashboard.tsx`, `Pantry.tsx`, `MealDetail.tsx` ## 1. Deploy This sprint is **frontend-only** — no backend rebuild, no migration, no schema change. ```bash # On deployment host (100.108.224.12) cd /path/to/MealPlanner git pull docker compose -f docker-compose.yml up -d --build frontend docker compose logs -f frontend # wait for "Application startup complete" ``` Then sanity-check the bundle is the new one: ```bash curl -s http://100.108.208.56:8082/ | grep -o 'index-[A-Za-z0-9_-]*\.js' # Should contain a new hash (e.g. index-C_trtYuT.js → new one). Compare to the # dist/assets/ output from the last `npm run build` on this machine. ``` ## 2. Build verification (already green) ``` $ cd frontend && npm run build > tsc && vite build ✓ 1891 modules transformed. dist/index.html 0.54 kB │ gzip: 0.31 kB dist/assets/index-DudtjbEb.css 37.14 kB │ gzip: 6.67 kB dist/assets/index-C_trtYuT.js 466.14 kB │ gzip: 144.12 kB ✓ built in 7.27s ``` tsc 0 errors, vite 0 errors. (The `C_trtYuT` hash was from Sprint 3; the new bundle will have a different hash. Check the output of your local build for the actual filename.) ## 3. Smoke checks ### S4.A — F7 happy path: success toasts still work For each of the following actions, the success toast should fire with the same string as before: | Action | Expected success toast | |---|---| | Dashboard: drag a meal to a different slot | "Meal moved" | | Dashboard: click Approve on a pending meal | "Meal approved" | | Dashboard: click Deny on a pending meal | "Meal denied" | | Dashboard: click the trash icon on a meal | "Meal deleted" (with Undo button) | | Dashboard: click Undo on the delete toast within 5s | "Slot filled with a new meal" | | Dashboard: click Generate on an empty slot | "Meal generated" | | Dashboard: send the weekly vote email | "queued" (or similar from `res.data.status`) | | Pantry: add a new item | "Item added to pantry" | | Pantry: remove an item | "Item removed" (with Undo button) | | Pantry: click Undo on the remove toast within 5s | "Item restored" | | MealDetail: submit feedback | "Feedback saved!" | ### S4.B — F7 error path: backend errors show the FastAPI detail The simplest way to trigger a backend error is to send an out-of-range `day_of_week` or an invalid `meal_type`. Without a frontend tweak, the easiest check is to **stop the backend** before exercising the actions: ```bash docker compose stop backend ``` Then, in the browser, try each of the 7 Dashboard mutations. Each should show a **toast** with a message like `Network Error` or `Failed to load data` (the global handler's fallback), NOT the legacy `"Failed to move meal"` / `"Failed to approve meal"` / etc. Then **restart the backend** and verify success paths still work: ```bash docker compose start backend ``` For a *real* FastAPI 4xx (not a network error), the cleanest check is the **`/meals//move`** action with an invalid target. The frontend doesn't expose a way to send an invalid `meal_type` directly, so the smoke check is the network-down case. If you want to test the FastAPI 422 path, the pantry form does it naturally: type a 1-char `name` (the schema has `min_length=1` → 1 char is fine, but try an empty name, which the frontend blocks, or use a recipe rating > 5 via DevTools). For a controlled test, hit the API directly with curl: ```bash # Should return 422 with a Pydantic detail array curl -i -X POST http://100.108.208.56:8082/api/feedback \ -H 'Content-Type: application/json' \ -d '{"meal_plan_item_id":"00000000-0000-0000-0000-000000000000","rating":99}' # Expected body: {"detail":[{"type":"greater_than_equal","loc":["body","rating"],"msg":"Input should be greater than or equal to 1",...}]} ``` Then in the MealDetail page for any meal, click "Submit feedback" with rating 5 stars. Then open DevTools → Network → find the POST → "Edit and resend" with `rating: 99`. The toast should show the first `msg` from the Pydantic array (`"Input should be greater than or equal to 1"`) — not the legacy `"Failed to save feedback. Please try again."`. ### S4.C — F7 pre-flight checks still work These are the local checks we deliberately **didn't** route through the global handler: | Action | Expected toast | |---|---| | Pantry: open the Add form, leave name empty, click Add | "Please enter an ingredient name" | | Pantry: try to remove an item whose `ingredient_id` is null (none in seed data, but visible in DevTools if you clear the field) | "Cannot remove: missing ingredient link" | Both should fire *immediately* (no network call), and no global error toast should follow. ### S4.D — F6 plan-status aria-label 1. Open the Dashboard (`http://100.108.208.56:8082/`). 2. In the top-right of the meal-plan card, there is a status badge (e.g. "awaiting approval", "approved", or "draft"). 3. Open DevTools → Elements tab → find the badge → confirm it has `aria-label="Plan status: awaiting approval"` (or whatever the status text is). 4. Optional: turn on a screen reader (VoiceOver on macOS: `Cmd+F5`; NVDA on Windows). Tab to the badge. It should announce `"Plan status: awaiting approval"` (or similar). ### S4.E — Regression check: Sprints 1-3 still work Quick spot-check of the headline features from previous sprints: - [ ] `/recommended` redirects to `/recipes/recommended` (Sprint 1 B4) - [ ] `/this-does-not-exist` renders the NotFound page (Sprint 1 B4) - [ ] Recipe detail shows ingredients with proper spacing (Sprint 1 B1) - [ ] Meal detail shows `$X.XX per serving` (Sprint 1 B3) - [ ] Pantry aisle `