Public Access
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:
@@ -339,4 +339,88 @@ User reported post-deploy: "The tour window looks great, but Clicking the X nor
|
||||
- LLM-powered generation (F8 Spoonacular, F9 Ollama) — separate backend proposals, future sprints. Sprint 11 only wires the existing recipe-library-based fill.
|
||||
- A "what would you like for dinner?" prompt before generation — the existing flow generates from the library with no user input.
|
||||
- A "regenerate" button after the plan exists — the existing `Plan Week` menu at `Dashboard.tsx:366-392` already handles this case.
|
||||
|
||||
---
|
||||
|
||||
## Sprint 12 — F8 Spoonacular search (§Future H10) — DRAFTED, awaiting user approval
|
||||
|
||||
**User direction (2026-06-05):** "Proceed." Selected from the question menu. F8 is the smallest remaining §Future item: search-by-name on a public API, brings external recipe data into the system. F9 (Ollama) remains a separate full-backend proposal.
|
||||
|
||||
**Root cause:** the user can browse ~150 local recipes on `/recipes` (admin seeds them) but has no path to find new ones without leaving the app. F8 adds a "Search the web" toggle that hits the Spoonacular `complexSearch` API and lets the user import a result into the local library in one click.
|
||||
|
||||
**Pre-existing infrastructure to reuse (not recreate):**
|
||||
- `backend/app/services/recipe_discovery.py` (226 lines) — full `RecipeDiscoveryService` with `_search_spoonacular()`, `_fetch_recipe_info()`, `_normalize_spoonacular()`. Reads `SPOONACULAR_API_KEY` via `getattr(settings, ...)`. Cites 150/day free quota.
|
||||
- `backend/app/api/ingredients.py:58-103` — public `POST /api/ingredients` is **idempotent** on `name_lower` + aliases. The ingredient-resolution helper for the import flow.
|
||||
- `scripts/enrich_recipes_spoonacular.py` (76 lines) — standalone one-shot script, reference for the env + URL pattern.
|
||||
|
||||
**Pre-existing WIP (NOT touched by Sprint 12):**
|
||||
- `backend/app/api/recipes.py` (352 lines, not registered in `main.py`)
|
||||
- `backend/app/schemas/recipe.py` (93 lines, has `RecipeCreate` + `RecipeIngredientRef`)
|
||||
- `nginx/nginx.conf`
|
||||
|
||||
### S12.1 — Backend: `GET /api/recipes/search` (public, webui-facing)
|
||||
|
||||
- [ ] **NEW** `backend/app/api/recipe_search.py` — 2 endpoints + a thin `search_spoonacular_summary(q, limit)` wrapper. Reuses the existing `requests.get(SPOONACULAR_SEARCH_URL, params={...})` pattern.
|
||||
- `GET /recipes/search?q=&limit=` — public, `require_session`. Calls `complexSearch` with `addRecipeInformation=true, fillIngredients=true, instructionsRequired=true, number=limit`. Returns normalized `RecipeSearchHit[]`. **No info endpoint call** (saves 1 point per result; search summary is enough for browsing).
|
||||
- `POST /recipes/import` — public, `require_session`. Body `{external_id, external_source: "spoonacular"}`. Fetches `/recipes/{id}/information` (1 point), normalizes, upserts ingredients via idempotent `POST /api/ingredients`, creates a local `Recipe` with `external_source`+`external_id`+`is_manually_added=true`. Returns the new Recipe.
|
||||
- [ ] **MODIFIED** `backend/app/schemas/__init__.py` — add `RecipeSearchHit` and `RecipeImportRequest` Pydantic models. Mirror the `ExternalRecipe` dataclass shape from `recipe_discovery.py:28-44` (but with Pydantic).
|
||||
- [ ] **MODIFIED** `backend/app/config.py` — add `SPOONACULAR_API_KEY: Optional[str] = None` to `Settings` for schema consistency. (Currently read via `getattr` because `extra="ignore"`. Adding it surfaces it in `.env.example` and tools.)
|
||||
- [ ] **MODIFIED** `backend/app/main.py` — register the new router. Reuses the `app.include_router` pattern at line 44-50.
|
||||
- [ ] Process-wide `_points_used` counter (module-level singleton in `recipe_search.py`). 503 with `detail: "spoonacular daily quota reached"` when over 140. Logged on every call.
|
||||
- [ ] 503 with `detail: "SPOONACULAR_API_KEY not configured"` when env var unset. Logged once at startup.
|
||||
|
||||
### S12.2 — Backend: tests
|
||||
|
||||
- [ ] **NEW** `backend/tests/test_recipe_search.py` — 4 tests, mock the Spoonacular `requests.get` calls.
|
||||
1. `GET /api/recipes/search?q=chicken` returns 200 + 1 normalized hit (mock summary).
|
||||
2. `GET /api/recipes/search?q=` returns 422 (empty query).
|
||||
3. `POST /api/recipes/import` happy path: mock info call + idempotent ingredient upsert + 201 with the new Recipe id.
|
||||
4. `POST /api/recipes/import` duplicate external_id → 409.
|
||||
|
||||
### S12.3 — Frontend: API client + Recipes page
|
||||
|
||||
- [ ] **MODIFIED** `frontend/src/api/index.ts:27-33` — add `recipes.search(q, limit)` and `recipes.import(data)`.
|
||||
- [ ] **MODIFIED** `frontend/src/pages/Recipes.tsx` — add a "Search the web" toggle next to the search bar (small button + `Sparkles` icon from lucide). When ON, the existing `useQuery` switches from `recipes.list(params)` to `recipes.search({q: debouncedQ, limit: 10})`. Renders results in a separate panel above the local list. Each result card has an "Import" button + the existing `NeverSuggestButton` removed (since these are not-yet-imported Spoonacular results, not local recipes).
|
||||
- Toggle defaults to OFF so the existing UX is preserved.
|
||||
- Toggle is a real `<button>` with `aria-pressed={searchWeb}`.
|
||||
- Debounced 300ms, same as the local search (reuse `handleSearch` from line 77-81).
|
||||
- Panel has `aria-busy={isLoading}` while fetching.
|
||||
|
||||
### S12.4 — Verify
|
||||
|
||||
- [ ] `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).
|
||||
- [ ] Manual API smoke: `curl -sS 'http://100.108.208.56:8082/api/recipes/search?q=chicken&limit=5' -b session.txt` → 200 JSON array.
|
||||
- [ ] Manual UI smoke (4 steps):
|
||||
1. Open `/recipes` in incognito. Confirm "Search the web" toggle is OFF, only the local list shows.
|
||||
2. Click the toggle. Confirm the panel header changes to "Search the web — Spoonacular" and a debounced search bar appears.
|
||||
3. Type "pasta" with 300ms debounce. Confirm 5-10 results render with name + image + cuisine tags.
|
||||
4. Click "Import" on a result. Confirm: toast "Imported!" + result card shows "Already imported" + toggle closes + the local list re-fetches and now contains the imported recipe.
|
||||
- [ ] Quota test: hit search 50 times in a row, confirm `_points_used` increments. The 51st within the budget returns 503.
|
||||
- [ ] No regression in Sprints 1-11.
|
||||
|
||||
### S12.5 — Docs (all 6 running docs updated)
|
||||
|
||||
- [ ] `Review/ui-nielsen-audit.md` — Sprint 12 status block (T6.1–T6.4) at the top.
|
||||
- [ ] `fix-ui-audit.md` — Sprint 12 plan section (T6.1–T6.5).
|
||||
- [ ] `Review/handoff-ui-audit.md` — Batch H + Sprint 12 entry + TL;DR row 12.
|
||||
- [ ] `docs/HANDOFF.md` — Sprint 12 section.
|
||||
- [ ] `.agent/plan.md` — this section.
|
||||
- [ ] `.agent/context.md` — Sprint 12 decisions + file:line references.
|
||||
- [ ] `Review/sprint12-verification.md` — written (4-step browser smoke + 2 API curls + quota test + a11y check).
|
||||
|
||||
### Done when (Sprint 12)
|
||||
|
||||
- All boxes above ticked.
|
||||
- `npm run build` green.
|
||||
- `pytest tests/test_recipe_search.py` green (4/4).
|
||||
- `Review/sprint12-verification.md` exists.
|
||||
- All 6 doc files have a Sprint 12 status block.
|
||||
|
||||
### Out of scope (Sprint 12)
|
||||
|
||||
- **F9 — Ollama local LLM.** Different backend proposal (model pull + ollama-py + `/api/llm/plan` endpoint). Separate sprint.
|
||||
- **Image generation.** `AI_IMAGE_ENABLED` env gate already exists; not enabled. Sprint 12 imports the Spoonacular image as-is.
|
||||
- **Auto-enriching existing recipes** with macros (would require `nutrition` endpoint = 1 pt per recipe; out of free quota).
|
||||
- **Modifying the pre-existing WIP** `backend/app/api/recipes.py` / `schemas/recipe.py` / `nginx/nginx.conf` — untouched.
|
||||
- F8 Spoonacular + F9 Ollama + dead `Generate Meal Plan` CTA — separate.
|
||||
|
||||
Reference in New Issue
Block a user