Public Access
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:
@@ -541,3 +541,63 @@ User direction 2026-06-05: "Proceed with the next phase in the redesign." F1 was
|
||||
- [ ] Browser smoke (8 steps) on `http://100.108.208.56:8082/` per `Review/sprint9-verification.md`.
|
||||
- [ ] No regression in Sprints 1-8.
|
||||
- [x] `Review/sprint9-verification.md` written.
|
||||
|
||||
---
|
||||
|
||||
## Sprint 10 — "Deny Forever" on Recipes — ✅ COMPLETE, awaiting deploy
|
||||
|
||||
User direction 2026-06-05: "Proceed with the next phase in the redesign. Also add a phase to include a 'Deny Forever' button in the Recipes endpoint." Sprint 10 ships the Deny Forever button on both the Recipes page (card overlay) and the RecipeDetail page (top bar).
|
||||
|
||||
**Status (2026-06-05):** ✅ Code complete. `npm run build` green (tsc 0 errors, vite 0 errors). 21/21 planner tests pass. Awaiting user commit + deploy. No new dependencies, no migration.
|
||||
|
||||
### T4.1 · Backend — new `POST /api/never-suggest` (public)
|
||||
|
||||
- **File:** `backend/app/api/never_suggest.py:60-86`
|
||||
- **Body:** `{family_profile_id, recipe_id, reason: "allergy"|"dislike", notes?}`. Idempotent on `(family_profile_id, recipe_id, ingredient_id, reason)`. Returns the row joined with `recipe_name`. Auth: `require_session` (auto-resolves to the first family on the trusted network).
|
||||
|
||||
### T4.2 · Backend — new `DELETE /api/never-suggest/{ns_id}` (public)
|
||||
|
||||
- **File:** `backend/app/api/never_suggest.py:89-111`
|
||||
- **Auth:** `require_session`. Row-level ownership check: 403 if the row's `family_profile_id` doesn't match the session. 404 if the row doesn't exist.
|
||||
|
||||
### T4.3 · Backend — `NeverSuggestRead.recipe_name` + `.ingredient_name` joins
|
||||
|
||||
- **Files:** `backend/app/schemas/never_suggest.py:31-33`, `backend/app/api/never_suggest.py:33-58` (`_attach_names` helper)
|
||||
- **Change:** server-side LEFT OUTER JOIN per kind, then merge into response dicts. Falls back to `None` if the recipe/ingredient was deleted (FK is `ON DELETE CASCADE` so the row goes with it; belt-and-suspenders).
|
||||
|
||||
### T4.4 · Frontend — API client
|
||||
|
||||
- **File:** `frontend/src/api/index.ts:75-86`
|
||||
- **Change:** `neverSuggest.list(familyProfileId)`, `neverSuggest.add({...})`, `neverSuggest.remove(nsId)`. Reuses the existing axios instance + `withCredentials: true` for the session cookie.
|
||||
|
||||
### T4.5 · Frontend — `NeverSuggestButton` component (NEW)
|
||||
|
||||
- **File:** `frontend/src/components/NeverSuggestButton.tsx` (~290 lines)
|
||||
- **Two variants:** `card` (overlay on `RecipeCard`) and `detail` (text buttons in `RecipeDetail` top bar). Single source of truth for the popover + reason + undo behavior.
|
||||
- **Popover:** `Allergy` (red, requires `window.confirm`) and `Dislike` (neutral, no confirm). A11y: `aria-label`, `aria-expanded`, `aria-haspopup="menu"`, `role="menu"`, Esc dismisses, outside click dismisses.
|
||||
- **Undo toast:** `showToast.undo()` (Sprint 3 B12 pattern, 6s window). Undo calls `DELETE /api/never-suggest/{id}` and re-invalidates queries so the recipe reappears.
|
||||
- **Pre-existing block detection:** if the recipe is already blocked, the button shows a "Blocked" state (red `🚫` icon, no `opacity-0`). Clicking it offers an "Unblock" path (with `window.confirm`).
|
||||
- **Query invalidations:** `['neverSuggest', familyId]`, `['recipes']`, `['recommendedRecipes', familyId]`, `['mealPlan']`. Blocking a recipe affects the Recipes page filter AND the next planner run.
|
||||
|
||||
### T4.6 · Frontend — `Recipes.tsx` overlay
|
||||
|
||||
- **File:** `frontend/src/pages/Recipes.tsx:241-300`
|
||||
- **Change:** `RecipeCard` now has `position: relative` so the absolute overlay anchors correctly. Button is `opacity-0 group-hover:opacity-100 focus:opacity-100`. `e.preventDefault()` + `e.stopPropagation()` on the click — doesn't navigate to the detail page.
|
||||
|
||||
### T4.7 · Frontend — `RecipeDetail.tsx` top bar
|
||||
|
||||
- **File:** `frontend/src/pages/RecipeDetail.tsx:73-78`
|
||||
- **Change:** new "Deny forever" button group to the left of "Add to Plan". Same popover + confirm/undo semantics as the card overlay.
|
||||
|
||||
### T4.8 · Sprint 10 verification gate
|
||||
|
||||
- [x] `npm run build` green for Sprint 10 (tsc 0 errors, vite 0 errors). Bundle: 487 → 495 kB.
|
||||
- [x] Backend imports clean; routes registered.
|
||||
- [x] 21/21 planner tests pass (1 pre-existing `test_filter_blocks_by_cost` failure still deselected; verified not introduced by Sprint 10).
|
||||
- [ ] Browser smoke (9 steps) on `http://100.108.208.56:8082/` per `Review/sprint10-verification.md`.
|
||||
- [ ] 5 API curls (POST, GET, idempotent re-add, DELETE, 403) all return expected status codes.
|
||||
- [ ] No regression in Sprints 1-9.
|
||||
|
||||
### T4.9 · `Review/sprint10-verification.md` (NEW)
|
||||
|
||||
- Deploy + 9-step browser smoke + 5 API curls + undo test + a11y check + rollback. Source of truth for the operator deploy + smoke flow.
|
||||
|
||||
Reference in New Issue
Block a user