# 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/<id>/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 `<select>` has all 9 canonical options (Sprint 2 B8)
- [ ] Shopping list section headers are sentence-case (Sprint 2 B9)
- [ ] Recipes filter panel has Apply/Reset and active count badge (Sprint 2 B11)
- [ ] Dashboard mobile viewport (390 px) shows empty meal slots (Sprint 1 B5)
- [ ] Dashboard delete shows the Undo toast (Sprint 3 B12)
- [ ] Nav links have `aria-current="page"` on the active route (Sprint 3 S3.5)

## 4. Acceptance criteria

Sprint 4 is done when:
- [ ] `git pull` + `docker compose up -d --build frontend` on the deployment host succeeds
- [ ] Browser at `http://100.108.208.56:8082/` loads the new bundle
- [ ] All S4.A success toasts still fire with the same text
- [ ] At least one S4.B error path is verified (network-down is the easiest)
- [ ] S4.C pre-flight toasts still fire without a network call
- [ ] S4.D DevTools confirms the plan-status badge has the right `aria-label`
- [ ] No regression in S4.E

## 5. Rollback

Sprint 4 is a single frontend commit. To roll back:

```bash
git revert d71b67a
docker compose -f docker-compose.yml up -d --build frontend
```

No DB migration to revert. No data loss possible — the only "writes" are user actions, and a backend call that fails simply doesn't write.
