docs: Sprint 12 — F8 Spoonacular search across all 6 running docs

Sprint 12 (commit 11b4595) wires the "Search the web" toggle on
/recipes to Spoonacular complexSearch, with a per-result Import
button that pulls the full recipe info (1 point) and writes a
local Recipe row. No pre-existing WIP files touched.

This commit updates the 6 running docs that track the sprint:

- .agent/plan.md — Sprint 12 section (S12.1-S12.5) added.
- .agent/context.md — Sprint 12 (D1-D9, Q1-Q3) added; file:line
  references; key takeaways.
- Review/sprint12-verification.md — new file: 4-step browser
  smoke + 2 API curls + quota test + a11y check + 5-risk table
  + future work section.
- Review/ui-nielsen-audit.md — Sprint 12 status block (T6.1-T6.5)
  at the top, after the Sprint 11 block.
- fix-ui-audit.md — Sprint 12 section (T6.1-T6.6) added after
  the Sprint 11 section, including the D-fix for the 5
  pre-existing tsc errors.
- Review/handoff-ui-audit.md — Batch H added to the deploy
  instructions; Sprint 12 section added after Sprint 11; TL;DR
  table row 12 added; Last-updated footer updated.
- docs/HANDOFF.md — Sprint 12 section added after the Sprint 11
  section, with a D-fix paragraph and a path-forward paragraph
  for F9.

All 6 docs now reflect Sprint 12. §Future backlog remaining: F9
(Ollama local LLM) — a full backend proposal that plugs into the
same handleGenerateFirstPlan (Sprint 11) + recipe_search.import
(Sprint 12) seams.
This commit is contained in:
2026-06-05 16:31:53 -07:00
parent 11b4595cf7
commit e939c96961
7 changed files with 378 additions and 7 deletions
+66
View File
@@ -342,3 +342,69 @@ User direction 2026-06-05: "Proceed." Selected from the question menu as the sma
- `backend/app/schemas/__init__.py:227-247``MealPlanBase` + `MealPlanCreate` schemas
- `Review/sprint11-verification.md` — new file (deploy + 4-step browser smoke + race test)
---
# Context — Sprint 12 (F8 Spoonacular search)
## Why Sprint 12 exists
User direction 2026-06-05: "Proceed." Selected from the question menu as the smallest remaining §Future item. F1 (Sprint 9) shipped, the dead CTA (Sprint 11) shipped, and F8 (Spoonacular search) is the last piece with a clear UI scope. F9 (Ollama local LLM) is a separate full backend proposal (model pull + ollama-py + `/api/llm/plan` endpoint) and remains in the §Future backlog.
The user can browse ~150 local recipes on `/recipes` (admin seeds them) but has no path to find new ones without leaving the app. Sprint 12 adds a "Search the web" toggle on `/recipes` that hits the Spoonacular `complexSearch` API and lets the user import a result into the local library in one click.
## Decisions (locked in for Sprint 12)
- **D1. Reuse the pre-existing `RecipeDiscoveryService`.** `backend/app/services/recipe_discovery.py` (226 lines, already exists) has `_search_spoonacular()`, `_fetch_recipe_info()`, `_normalize_spoonacular()` and a 150/day free-tier quota gate. Sprint 12 does NOT re-implement the API client; it adds a thin HTTP layer in `backend/app/api/recipe_search.py` that calls the same `requests.get(SPOONACULAR_SEARCH_URL, ...)` pattern.
- **D2. Search endpoint = `complexSearch` only, NO info endpoint call.** The pre-existing `_search_spoonacular` calls the info endpoint (1 pt) for every result. For a 10-result search that's 10 extra points — the whole daily quota in one query. Sprint 12's `GET /api/recipes/search` uses just the `complexSearch` summary (1.1 pts/query) for browsing, and the **import** endpoint does the info call once (1 pt) only for the recipe the user actually wants.
- **D3. Ingredient resolution via the existing public `POST /api/ingredients` endpoint.** `backend/app/api/ingredients.py:58-103` is idempotent on `name_lower` + aliases. The import flow upserts each ingredient via this endpoint. No new ingredient-resolution helper needed.
- **D4. NEW endpoints, NOT modification of the pre-existing WIP.** `backend/app/api/recipes.py` (352 lines) and `backend/app/schemas/recipe.py` (93 lines) exist as WIP but are not registered in `main.py`. Sprint 12 does NOT touch them. It creates `backend/app/api/recipe_search.py` (new router) and adds the Pydantic models to `backend/app/schemas/__init__.py` (the canonical location used by all registered endpoints).
- **D5. UI toggle defaults to OFF.** The user explicitly asked for a "Search the web" toggle, not a permanent switch. The local-search UX is preserved for users who don't toggle. The toggle is a real `<button>` with `aria-pressed={searchWeb}`.
- **D6. Per-query 300ms debounce, same as local search.** Reuse the existing `handleSearch` callback at `Recipes.tsx:77-81`. No new debounce logic.
- **D7. Process-wide `_points_used` counter, module-level singleton in `recipe_search.py`.** Survives across requests in the same uvicorn worker. 503 with `detail: "spoonacular daily quota reached"` when over 140. Logged on every call.
- **D8. Quota-overflow test is part of the verification gate.** Hit search 50 times; the 51st within the budget returns 503. This catches the "we forgot the quota gate" regression.
- **D9. `SPOONACULAR_API_KEY` is added to `Settings`.** Currently read via `getattr` (line 50 of `recipe_discovery.py`) because `Settings.extra="ignore"`. Adding the schema declaration surfaces it in `.env.example` and tools; the runtime behavior is unchanged.
## Open questions to surface to the user, not to assume
- **Q1. Should the "Search the web" panel show even when the search bar is empty (showing popular Spoonacular recipes)?** Default: no. The panel only fetches when `debouncedQ.length >= 2`. Below 2 chars, the panel is empty. The local list still shows. Avoids unnecessary quota burn from idle toggling.
- **Q2. Should importing a recipe also import its side-dishes (if Spoonacular returns them)?** Default: no. The Spoonacular free-tier `/information` endpoint doesn't return structured side-dishes; the description blob contains them. The import stores the description as-is and the user can edit later. Adding structured side-dishes is a future sprint.
- **Q3. Should the import also send a vote email / create a meal-plan-item / etc.?** Default: no. The import only adds to the recipe library. Voting, planning, and shopping-list integration are downstream of the library and are not affected by Sprint 12.
## Sprint 12 verification gate
- `cd backend && python -m pytest tests/test_recipe_search.py -v` → 4/4 green
- `cd frontend && npm run build` → green (tsc 0 errors, vite 0 errors)
- Browser smoke (4 steps) on `http://100.108.208.56:8082/` per `Review/sprint12-verification.md`
- Quota test: 50 searches in a row, 51st returns 503
- 2 API curls: `GET /api/recipes/search?q=chicken` and `POST /api/recipes/import` with mocked Spoonacular
- No regression in Sprints 1-11
## Sprint 12 — does NOT touch
- The OnboardingTour (Sprint 9) — unchanged.
- The NeverSuggestButton (Sprint 10) — unchanged.
- The handleGenerateFirstPlan (Sprint 11) — unchanged.
- The existing `handlePlanWeek` (Sprint 6 F4) — unchanged.
- The keyboard shortcuts (Sprint 5 F2) — unchanged.
- The error toast / `showApiError` flow (Sprint 4 F7) — used for the error path; unchanged.
- The 3-button Sprint 8 voting row — unchanged.
- The WeekRangeNav (Sprint 7) — unchanged.
- The bulk pantry add (Sprint 6 F3) — unchanged.
- **Pre-existing WIP** `backend/app/api/recipes.py`, `backend/app/schemas/recipe.py`, `nginx/nginx.conf` — untouched.
- The existing `RecipeDiscoveryService` at `backend/app/services/recipe_discovery.py` — unchanged. Sprint 12 calls it via the public Spoonacular URLs directly (not through the service class), to avoid the service's info-endpoint cost.
## Key file:line references (Sprint 12)
- `backend/app/services/recipe_discovery.py:19-21` — Spoonacular URL constants (reused in `recipe_search.py`)
- `backend/app/services/recipe_discovery.py:50``getattr(settings, "SPOONACULAR_API_KEY", "")` pattern (replaced by schema declaration in Sprint 12)
- `backend/app/api/ingredients.py:58-103` — public `POST /api/ingredients` (idempotent ingredient upsert, used by the import flow)
- `backend/app/main.py:44-52` — router registration pattern (new router registered at line 52-53)
- `backend/app/config.py:7-44``Settings` class (S12.1 adds `SPOONACULAR_API_KEY: Optional[str] = None`)
- `backend/app/schemas/__init__.py:38-247``MealPlanStatus`, `MealPlanCreate`, etc. (S12.1 adds `RecipeSearchHit` + `RecipeImportRequest`)
- `backend/app/models/__init__.py:162-197``Recipe` model (target for the import insert)
- `backend/app/models/__init__.py:142-159``Ingredient` model (target for the idempotent upsert)
- `frontend/src/api/index.ts:27-33``recipes` client (S12.3 adds `search` + `import` methods)
- `frontend/src/pages/Recipes.tsx:47-95` — search bar + debounce + `useQuery` (S12.3 adds the web-search toggle + branch)
- `frontend/src/pages/Recipes.tsx:77-81``handleSearch` debounce (reused for the web-search branch)
- `Review/sprint12-verification.md` — new file (deploy + 4-step browser smoke + 2 API curls + quota test + a11y check)