feat(ui): Sprint 10 — Deny Forever on Recipes (card overlay + detail button + undo toast)

User-driven follow-up to Sprint 8: surface the Sprint 1-3 NeverSuggest
infrastructure on the Recipes surface so a family can pre-emptively
mark a recipe as never-suggest before it appears in a plan.

Backend (3 changes):
- POST /api/never-suggest (public, webui-facing). Idempotent on
  (family, recipe, reason). Returns the row joined with recipe_name.
- DELETE /api/never-suggest/{ns_id} (public, webui-facing). Row-level
  ownership check (403 if cross-family), 404 if absent.
- NeverSuggestRead.recipe_name + .ingredient_name server-side joins
  via _attach_names() helper (one LEFT OUTER JOIN per kind).
- Admin path (POST/DELETE /api/admin/never-suggest) unchanged.

Frontend (4 changes):
- New NeverSuggestButton component (~290 lines). Two variants: card
  (overlay on RecipeCard) and detail (text buttons in RecipeDetail
  top bar). Popover with Allergy (red, window.confirm) + Dislike
  (neutral, no confirm). Undo toast via showToast.undo() (Sprint 3
  B12 pattern, 6s window). Pre-existing block detection shows a
  Blocked state with an Unblock path.
- mealPlannerApi.neverSuggest.list/add/remove in api/index.ts.
- Recipes.tsx overlay: RecipeCard has position: relative; button is
  opacity-0 group-hover:opacity-100 focus:opacity-100. e.preventDefault
  + e.stopPropagation prevents accidental navigation.
- RecipeDetail.tsx top bar: new Deny forever button group to the left
  of Add to Plan.

Build: npm run build green (tsc 0 errors, vite 0 errors) on
docker-willester. Bundle 487 -> 495 kB. No new dependencies. No
migration (NeverSuggest table exists from prior sprints).

Tracking: Review/sprint10-verification.md (9-step browser smoke +
5 API curls + undo test + a11y check).
This commit is contained in:
2026-06-05 13:29:04 -07:00
parent 6e386baf6e
commit 0b6c5dcfb7
13 changed files with 867 additions and 15 deletions
+11
View File
@@ -108,6 +108,17 @@ The app looks polished on the surface (Tailwind palette, clean cards, working to
> - **Verification log:** `Review/sprint9-verification.md`. Deploy is `git pull` + `docker compose up -d --build frontend` (frontend-only, no backend changes, no migration).
> - **No new dependencies. No backend changes.**
>
> **Sprint 10 status (committed 2026-06-05, awaiting deploy):** User-driven — "Deny Forever" button on the Recipes surface (card overlay + RecipeDetail top bar). Surfaces the Sprint 13 `NeverSuggest` infrastructure on the webui Recipes page. Backend adds family-facing `POST` + `DELETE /api/never-suggest` endpoints; the existing admin path stays unchanged.
> - **T4.1** `POST /api/never-suggest` (public, webui-facing). Idempotent on `(family, recipe, reason)`. Returns the row joined with `recipe_name`.
> - **T4.2** `DELETE /api/never-suggest/{ns_id}` (public, webui-facing). Row-level ownership check (403 if cross-family).
> - **T4.3** `NeverSuggestRead.recipe_name` + `.ingredient_name` server-side joins. One LEFT OUTER JOIN per kind via `_attach_names()` helper.
> - **T4.4** `mealPlannerApi.neverSuggest.list/add/remove` in `frontend/src/api/index.ts`.
> - **T4.5** New `frontend/src/components/NeverSuggestButton.tsx` (~290 lines). Two variants: `card` (overlay) + `detail` (text buttons in top bar). Popover with `Allergy` (red, `window.confirm`) + `Dislike` (neutral, no confirm). Undo toast via `showToast.undo()` (Sprint 3 B12 pattern, 6s window). Pre-existing block detection shows a "Blocked" state with an "Unblock" path.
> - **T4.6** `Recipes.tsx` overlay. Card has `position: relative`; button is `opacity-0 group-hover:opacity-100 focus:opacity-100`. `e.preventDefault()` + `e.stopPropagation()` — doesn't navigate.
> - **T4.7** `RecipeDetail.tsx` top bar. New "Deny forever" button group to the left of "Add to Plan".
> - **Verification log:** `Review/sprint10-verification.md`. Deploy is `git pull` + `docker compose up -d --build backend frontend` (no migration; the `NeverSuggest` table already exists).
> - **No new dependencies. No migration. Admin path unchanged.**
>
> **Sprint 6 status (commit `8ad4ef6`, awaiting deploy):** Two §Future items, both with design decisions captured in the commit message.
> - **F3** Bulk 'add checked to pantry' on ShoppingList. Backend `POST /api/pantry/bulk` accepts `{items: HomePantryCreate[]}` and returns per-item status (`added` / `updated` / `skipped`) with totals. Per-item failure model: unknown ingredient → `skipped` with reason, not a 4xx. Frontend ShoppingList gains a primary `Add N to pantry` button next to the existing Reset button; toast reports `added X, updated Y, skipped Z`; only the items that actually landed are removed from the checked Set. **Scope decision:** ShoppingList only (the checked Set was the natural substrate; Pantry would need new multi-select UI).
> - **F4** Plan the whole week on Dashboard. Backend `POST /api/meals/{id}/fill-empty-slots` with body `{meal_types: [str, ...]}` returns `FillEmptySlotsResult { filled: [{day, meal_type, item}], failed: [{day, meal_type, reason}] }`. Iterates day 1..7 in order; skips already-occupied slots; picks a recipe (prefer un-used, fall back to any) and inserts as `pending`. Per-slot failure model — never aborts mid-batch. Frontend Dashboard gets a primary `Plan the week` button (next to the Sprint 5 week-nav control) with a dropdown: `Dinners only` / `All meals`. Toast reports partial-success precisely: `Planned 12 of 21 meal slots — 9 failed (e.g. <reason>)`.