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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -20,14 +20,15 @@ If you are a new agent continuing this work, do this **in order**:
|
||||
- **Batch E:** Sprint 9 (one `git pull`, `docker compose up -d --build frontend` — frontend-only, no migration, no backend rebuild).
|
||||
- **Batch F:** Sprint 10 (one `git pull`, `docker compose up -d --build backend frontend` — no migration; the `NeverSuggest` table already exists from prior sprints).
|
||||
- **Batch G:** Sprint 11 (one `git pull`, `docker compose up -d --build frontend` — frontend-only, no migration, no backend rebuild).
|
||||
4. **Open issues** in `.agent/plan.md` (the "Phase R1-R3" section is a prior plan; the **Sprint 11 active-sprint** section is the current state) and in `.agent/context.md` (decisions + open Qs for the current sprint).
|
||||
- **Batch H:** Sprint 12 (one `git pull`, `docker compose up -d --build backend frontend` — backend has the new `recipe_search.py` router, frontend has the new toggle).
|
||||
4. **Open issues** in `.agent/plan.md` (the "Phase R1-R3" section is a prior plan; the **Sprint 12 active-sprint** section is the current state) and in `.agent/context.md` (decisions + open Qs for the current sprint).
|
||||
5. **Do not** touch the pre-existing WIP files: `backend/app/api/recipes.py`, `backend/app/schemas/recipe.py`, `nginx/nginx.conf` (untouched since before this work; user's to manage).
|
||||
6. **When you commit,** use the `fix(ui):`, `feat(ui):`, `refactor(frontend):`, `docs(review):` Conventional Commit style. Force-add new files in `frontend/src/lib/` (the `.gitignore` line 17 `lib/` is a pre-existing bug that catches it).
|
||||
|
||||
**TL;DR of where things stand:**
|
||||
|
||||
- Sprints 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8: code committed and build green. Sprint 1 deployed. Sprints 2-8 awaiting user deploy.
|
||||
- The only remaining §Future items are F8 Spoonacular enrichment (proposal) and F9 Ollama LLM matcher (proposal). F1 (onboarding) shipped as Sprint 9; the dead `Generate Meal Plan` CTA shipped as Sprint 11. Both are deployment-pending.
|
||||
- The only remaining §Future item is F9 Ollama LLM matcher (proposal). F1 (onboarding) shipped as Sprint 9; the dead "Generate Meal Plan" CTA shipped as Sprint 11; F8 (Spoonacular) shipped as Sprint 12. All three are deployment-pending.
|
||||
- Pre-existing repo issues: 1 failing test (`test_filter_blocks_by_cost` — verified pre-Sprint 8), `.gitignore` `lib/` bug, no CI. Documented.
|
||||
|
||||
---
|
||||
@@ -53,7 +54,7 @@ If you are a new agent continuing this work, do this **in order**:
|
||||
|
||||
**Tracking docs:** `Review/sprint8-verification.md` (deploy + smoke), `Review/ui-nielsen-audit.md` Sprint 8 status block, `fix-ui-audit.md` T2.1–T2.10, this file, `docs/HANDOFF.md` Sprint 8 section.
|
||||
|
||||
**Thread 3 (§Future backlog) is deferred** until S8 is deployed + verified. F8/F9 proposals remain. **Sprint 9 (F1 onboarding) + post-deploy fix (`1562929`), Sprint 10 (Deny Forever on Recipes), and Sprint 11 (wire the dead "Generate Meal Plan" CTA) are all committed 2026-06-05, awaiting user deploy.**
|
||||
**Thread 3 (§Future backlog) is deferred** until S8 is deployed + verified. F9 (Ollama) proposal remains. **Sprint 9 (F1 onboarding) + post-deploy fix (`1562929`), Sprint 10 (Deny Forever on Recipes), Sprint 11 (wire the dead "Generate Meal Plan" CTA), and Sprint 12 (F8 Spoonacular search) are all committed 2026-06-05, awaiting user deploy.**
|
||||
|
||||
### Sprint 9 — F1 Onboarding Tour (H10)
|
||||
|
||||
@@ -103,7 +104,19 @@ If you are a new agent continuing this work, do this **in order**:
|
||||
|
||||
**Tracking docs:** `Review/sprint11-verification.md` (deploy + 4-step browser smoke + race test + 2 API curls), `Review/ui-nielsen-audit.md` Sprint 11 status block, `fix-ui-audit.md` T5.1–T5.5, this file, `docs/HANDOFF.md` Sprint 11 section.
|
||||
|
||||
**Path forward to F8/F9:** the `EmptyState.action.onClick` is the single seam. Future F8 (Spoonacular) or F9 (Ollama) work only needs to swap the `fillEmptySlots` call in `handleGenerateFirstPlan` for an LLM call. No DOM, copy, or component structure changes needed.
|
||||
### Sprint 12 — F8 Spoonacular search (§Future H10) (user-driven)
|
||||
|
||||
**Status: COMMITTED on 2026-06-05. Build green. Backend + frontend.** Awaiting user to `git pull` + `docker compose up -d --build backend frontend` (no migration).
|
||||
|
||||
**Root cause (one-liner):** the user can browse ~150 local recipes on `/recipes` but has no path to find new ones without leaving the app.
|
||||
|
||||
**Scope (6 boxes):** NEW `backend/app/api/recipe_search.py` (~270 lines, 2 endpoints + module-level quota counter), 1 schema addition (`RecipeSearchHit` + `RecipeImportRequest`), 1 `Settings` field (`SPOONACULAR_API_KEY`), 1 router registration, 5 new `mealPlannerApi.recipes` methods, 1 toggle button + 1 web-search panel + 1 import mutation in `Recipes.tsx`, 2 optional `RecipeIngredient` fields. **No new dependencies. No migration. No pre-existing WIP files touched.**
|
||||
|
||||
**Free-tier quota:** Spoonacular = 150 points/day. `complexSearch` = 1 pt + 0.01/result. The new search uses summary-only (1.1 pts/query). The import uses 1 pt for the info call + ingredient upserts (no extra points). 140-pt daily budget leaves a 10-pt safety margin. Counter resets on process restart.
|
||||
|
||||
**Pre-existing tsc errors exposed:** adding 5 methods to the `recipes` object literal exposed 5 latent errors in Pantry/MealDetail/Recommended.tsx (calls to non-existent methods + 2 missing `RecipeIngredient` fields). Resolved per user decision: 3 stub methods + 2 optional type fields. ~7 lines of fixes; no WIP touched. Documented in `Review/sprint12-verification.md` D-fix section + `fix-ui-audit.md` T6.4.
|
||||
|
||||
**Tracking docs:** `Review/sprint12-verification.md` (deploy + 4-step browser smoke + 2 API curls + quota test + a11y check + 5-risk table), `Review/ui-nielsen-audit.md` Sprint 12 status block, `fix-ui-audit.md` T6.1–T6.6, this file, `docs/HANDOFF.md` Sprint 12 section.
|
||||
|
||||
### Sprint 7 — Fix webui "empty meal plan" (date-semantics mismatch)
|
||||
|
||||
@@ -135,8 +148,9 @@ Twelve commits land all 14 audit findings + 6 §Future items + 2 user-driven spr
|
||||
| 9 | (committed 2026-06-05) | F1 Onboarding Tour (H10) — hand-rolled, no new deps, 4-step welcome tour with `?reset-tour=1` reset | ✅ green | ⚠️ committed; awaiting user deploy (frontend-only) |
|
||||
| 10 | (committed 2026-06-05) | "Deny Forever" on Recipes — card overlay + RecipeDetail top bar + reason dropdown (allergy/dislike) + undo toast. New `POST`/`DELETE /api/never-suggest` (public) + `recipe_name` join. | ✅ green | ⚠️ committed; awaiting user deploy (backend + frontend, no migration) |
|
||||
| 11 | (committed 2026-06-05) | Wire the dead "Generate Meal Plan" empty-state CTA — `meals.create` + `meals.fillEmptySlots`; race-safe; reusable for F8/F9. | ✅ green | ⚠️ committed; awaiting user deploy (frontend-only) |
|
||||
| 12 | (committed 2026-06-05) | F8 Spoonacular search — "Search the web" toggle on `/recipes` + Import button. New `GET /api/recipes/search` + `POST /api/recipes/import`. Quota-gated (140pt/day). | ✅ green | ⚠️ committed; awaiting user deploy (backend + frontend) |
|
||||
|
||||
All work is on `main` ahead of `origin/main` (pre-existing WIP also present). All 11 sprints compile. **Sprint 1 is live. Sprints 2-11 are not yet live on `100.108.208.56:8082/`.**
|
||||
All work is on `main` ahead of `origin/main` (pre-existing WIP also present). All 12 sprints compile. **Sprint 1 is live. Sprints 2-12 are not yet live on `100.108.208.56:8082/`.**
|
||||
|
||||
**CRITICAL — Sprint 2 was effectively undeployable** because the CASE expression in `0015_normalize_pantry_aisles.py` failed with `text = boolean` on the `varchar(100) aisle` column. The bug is fixed in `d78bd18` (Sprint 5). Without that commit, `alembic upgrade head` would have failed on the deployment host, blocking Sprints 2, 3, 4 from going live. **The deployment host's DB still has the pre-0015 schema** — the migration must be run as part of the Sprints 2-5 batch deploy.
|
||||
|
||||
@@ -380,4 +394,4 @@ cd frontend && npm run build
|
||||
|
||||
Trust the build output. Trust the smoke checklist. Don't trust the deployment host's UI until the user confirms. The verification model is "I shipped, you verified, you reported, I fixed" — the agent in this role never sees the live UI directly.
|
||||
|
||||
**Last updated: 2026-06-05** — Sprint 1 deployed; Sprints 2-6 awaiting user deploy; **Sprint 7 (`09c7525`), Sprint 8 (`efd1fc6`), Sprint 9 (F1 Onboarding Tour) + post-deploy fix (`1562929`), Sprint 10 (Deny Forever on Recipes), and Sprint 11 (Wire the dead "Generate Meal Plan" CTA) committed on 2026-06-05, awaiting user deploy**. See the "How to take over" and "Pending user deploy" sections at the top of this file.
|
||||
**Last updated: 2026-06-05** — Sprint 1 deployed; Sprints 2-6 awaiting user deploy; **Sprint 7 (`09c7525`), Sprint 8 (`efd1fc6`), Sprint 9 (F1 Onboarding Tour) + post-deploy fix (`1562929`), Sprint 10 (Deny Forever on Recipes), Sprint 11 (Wire the dead "Generate Meal Plan" CTA), and Sprint 12 (F8 Spoonacular search) committed on 2026-06-05, awaiting user deploy**. See the "How to take over" and "Pending user deploy" sections at the top of this file.
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Sprint 12 — F8 Spoonacular search — verification
|
||||
|
||||
**Status (2026-06-05):** ✅ Code complete. `npm run build` green. Awaiting user deploy.
|
||||
|
||||
## Summary
|
||||
|
||||
Sprint 12 adds a "Search the web" toggle on `/recipes` that hits the Spoonacular `complexSearch` API. Each result has an "Import" button that pulls the full recipe info (1 point) and writes a local `Recipe` row with the right schema fields. Spoonacular ingredients are upserted into the local `Ingredient` table via the existing idempotent `POST /api/ingredients` endpoint.
|
||||
|
||||
**No pre-existing WIP files were touched.** Sprint 12 creates a new `backend/app/api/recipe_search.py` router (separate from the WIP `recipes.py`) and adds 2 Pydantic models to `backend/app/schemas/__init__.py` (the canonical location for the registered endpoints). The pre-existing WIP `recipes.py` is registered in `main.py` (lines 54-55) and handles `GET /api/recipes`, `GET /api/recipes/recommended`, `GET /api/recipes/{id}` — none of which collide with my new endpoints.
|
||||
|
||||
**Pre-existing tsc errors exposed by the API surface expansion (D-fix):** adding 5 new methods to `mealPlannerApi.recipes` (search, importRecipe, recommended, listIngredients, createIngredient) caused TypeScript to evaluate the recipes object as a closed type, exposing 5 latent errors in Pantry.tsx / MealDetail.tsx / Recommended.tsx (calls to non-existent `listIngredients` / `createIngredient` / `recommended` + 2 missing fields on `RecipeIngredient`). User decision: add stub methods + fix the `RecipeIngredient` type. 7 lines of fixes; no pre-existing WIP touched.
|
||||
|
||||
## Files changed
|
||||
|
||||
### Backend
|
||||
- **NEW** `backend/app/api/recipe_search.py` (~270 lines) — 2 endpoints + module-level quota counter + thread-safe lock
|
||||
- **MODIFIED** `backend/app/config.py` — added `SPOONACULAR_API_KEY: Optional[str] = None` to `Settings` (was previously read via `getattr` since `extra="ignore"`)
|
||||
- **MODIFIED** `backend/app/schemas/__init__.py` — added `RecipeSearchHit` + `RecipeImportRequest` Pydantic models
|
||||
- **MODIFIED** `backend/app/main.py` — registered `recipe_search_api.router` at `/api/recipes`
|
||||
|
||||
### Frontend
|
||||
- **MODIFIED** `frontend/src/api/index.ts` — added 5 methods to `recipes`: `search`, `importRecipe`, `recommended`, `listIngredients`, `createIngredient` (the last 3 are stubs for pre-existing call sites)
|
||||
- **MODIFIED** `frontend/src/pages/Recipes.tsx` — added `searchWeb` toggle state + `importedExternalIds` set + `webHits` query + `importMutation` + the toggle button (with `aria-pressed`) + the web-search panel
|
||||
- **MODIFIED** `frontend/src/types/index.ts` — added optional `ingredient` + `is_optional` to `RecipeIngredient` (for pre-existing MealDetail.tsx call sites)
|
||||
|
||||
## Build verification
|
||||
|
||||
```text
|
||||
vite v5.4.21 building for production...
|
||||
transforming...
|
||||
✓ 1897 modules transformed.
|
||||
rendering chunks...
|
||||
computing gzip size...
|
||||
dist/index.html 0.54 kB │ gzip: 0.32 kB
|
||||
dist/assets/index-BEAXfX_U.css 42.06 kB │ gzip: 7.26 kB
|
||||
dist/assets/index-w9IMb94J.js 500.28 kB │ gzip: 153.46 kB
|
||||
✓ built in 2.63s
|
||||
```
|
||||
|
||||
- `tsc` 0 errors, `vite` 0 errors.
|
||||
- Bundle: 496.48 → 500.28 kB (+3.8 kB for the web-search panel + the import mutation).
|
||||
|
||||
## Backend verification (manual, post-deploy)
|
||||
|
||||
```bash
|
||||
# 1) Search Spoonacular (summary only, 1.1 points)
|
||||
curl -sS 'http://100.108.208.56:8082/api/recipes/search?q=chicken&limit=5' \
|
||||
-H 'Cookie: mealplanner_session=...' | jq '.[0] | {name, external_id, cuisine_tags}'
|
||||
|
||||
# Expected: { "name": "Chicken ...", "external_id": "...", "cuisine_tags": [...] }
|
||||
|
||||
# 2) Import the first result (1 point + ingredient upserts)
|
||||
curl -sS -X POST 'http://100.108.208.56:8082/api/recipes/import' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Cookie: mealplanner_session=...' \
|
||||
-d '{"external_id": "<id from step 1>"}' | jq
|
||||
|
||||
# Expected: { "id": "<uuid>", "name": "...", "external_id": "...",
|
||||
# "ingredients_imported": 7 }
|
||||
|
||||
# 3) Re-import the same one (idempotent check)
|
||||
curl -sS -X POST 'http://100.108.208.56:8082/api/recipes/import' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Cookie: mealplanner_session=...' \
|
||||
-d '{"external_id": "<id from step 1>"}' -i | head -1
|
||||
# Expected: HTTP/1.1 409 Conflict
|
||||
```
|
||||
|
||||
## Browser smoke (4 steps)
|
||||
|
||||
Run on `http://100.108.208.56:8082/recipes`.
|
||||
|
||||
1. **Land on `/recipes`.** Confirm: the "Search the web" toggle is OFF (secondary variant). The local list shows. No web-search panel.
|
||||
2. **Click the toggle.** Confirm: button flips to primary variant. The web-search panel renders with the header "Search the web — Spoonacular" and the prompt "Type at least 2 characters in the search bar above...". The local list still shows.
|
||||
3. **Type "pasta" in the search bar** (300ms debounce). Confirm: the web-search panel shows 5-10 results, each with image + name + cuisine/dietary badges + total-time + an "Import" button. The local list is unchanged.
|
||||
4. **Click "Import" on a result.** Confirm: button label flips to "Imported" (with a check icon), toast appears with "Imported "<name>"", and the local list re-fetches (the new recipe is in the list). Click another import button — confirm the same flow.
|
||||
|
||||
## Quota test (manual, post-deploy)
|
||||
|
||||
```bash
|
||||
# Hit search 50 times. Each call costs 1.1 points.
|
||||
for i in $(seq 1 50); do
|
||||
curl -sS 'http://100.108.208.56:8082/api/recipes/search?q=test&limit=10' \
|
||||
-H 'Cookie: mealplanner_session=...' > /dev/null
|
||||
done
|
||||
# 51st call should be 503 (over the 140-point daily budget)
|
||||
curl -sS 'http://100.108.208.56:8082/api/recipes/search?q=test&limit=10' \
|
||||
-H 'Cookie: mealplanner_session=...' -i | head -1
|
||||
# Expected: HTTP/1.1 503 Service Unavailable
|
||||
```
|
||||
|
||||
The 140-point daily budget leaves a 10-point safety margin under the 150-point free tier. The counter resets on process restart (operator recovers by `docker compose restart backend`).
|
||||
|
||||
## A11y check
|
||||
|
||||
- The toggle is a real `<button>` with `aria-pressed={searchWeb}` (T6.3 D5).
|
||||
- The web-search panel wraps in a `<div role="region" aria-label="Web recipe search" aria-busy={webLoading}>` — same pattern as the Filters region (line 196 in Recipes.tsx).
|
||||
- The loader inside the panel header is `aria-hidden` by default (no special treatment needed; the `aria-busy` on the parent region handles the in-flight state).
|
||||
- Each result card is a `<li>` with semantic structure: `<h3>` for the name, `<span>` for the time, `<button>` for the import.
|
||||
- The "Import" button's state is communicated via both the label ("Import" → "Importing…" → "Imported") and the icon (Sparkles → Loader2 → Check).
|
||||
|
||||
## Risks & mitigations
|
||||
|
||||
- **R1: Spoonacular API changes their schema.** Mitigation: the `_normalize_spoonacular_summary` helper is the single point of contact; if Spoonacular renames a field, only that helper changes. Tested with the current `complexSearch` shape; future drift is a 1-file change.
|
||||
- **R2: Quota exhaustion.** Mitigation: process-wide `_points_used` counter + 503 with clear message. Counter resets on restart. Logged on every call. The verification doc includes a quota-exhaustion test.
|
||||
- **R3: Pre-existing WIP could collide with my new endpoints.** Verified: the WIP `recipes.py` registers `GET /api/recipes`, `GET /api/recipes/recommended`, `GET /api/recipes/{id}`, and admin POST/PATCH/DELETE. My new `GET /api/recipes/search` and `POST /api/recipes/import` are unique. No collision.
|
||||
- **R4: SPOONACULAR_API_KEY unset.** Mitigation: `_ensure_spoonular_configured()` returns 503 with `detail: "SPOONACULAR_API_KEY not configured; set it in the backend env"`. Logged once at first call.
|
||||
- **R5: Frontend pre-existing tsc errors exposed by the API surface expansion.** Resolved with 5 stub methods + 2 type fields (per user decision). Documented in the file:line references in `.agent/context.md`.
|
||||
|
||||
## Commit
|
||||
|
||||
One commit: `feat(ui): Sprint 12 — F8 Spoonacular search (web-search toggle + import)`. Files:
|
||||
|
||||
- `backend/app/api/recipe_search.py` (NEW, ~270 lines)
|
||||
- `backend/app/config.py` (Settings addition)
|
||||
- `backend/app/schemas/__init__.py` (2 Pydantic models)
|
||||
- `backend/app/main.py` (router registration)
|
||||
- `frontend/src/api/index.ts` (5 new methods)
|
||||
- `frontend/src/pages/Recipes.tsx` (toggle + panel + mutation)
|
||||
- `frontend/src/types/index.ts` (2 optional fields on `RecipeIngredient`)
|
||||
|
||||
## Future work (NOT in Sprint 12)
|
||||
|
||||
- **F9 — Ollama local LLM.** Different backend proposal (model pull + ollama-py + `/api/llm/plan` endpoint). Separate sprint.
|
||||
- **Vitest unit test for `useOnboarding`** (Q4 from Sprint 9). Lifts "no new npm deps" for testing-only deps. Prevents the S9 bug class from recurring.
|
||||
- **Auto-enriching existing recipes** with macros (would require Spoonacular's `nutrition` endpoint = 1 pt per recipe; out of free quota).
|
||||
- **Side-dish support** in the import (the Spoonacular free-tier `/information` endpoint doesn't return structured side-dishes; the description blob contains them).
|
||||
- **Batch import** ("import top 10 results with one click"). Currently one import per click. The endpoint is already idempotent on `(external_source, external_id)`.
|
||||
@@ -123,6 +123,14 @@ The app looks polished on the surface (Tailwind palette, clean cards, working to
|
||||
> - **T5.2** Reuses the partial-success toast format from `handlePlanWeek`: `Planned N meals` (full success) / `Planned N of M meals — K failed (e.g. <reason>)` (partial) / `Plan created — no recipes to add yet` (empty library).
|
||||
> - **T5.3** `EmptyState.action.disabled?: boolean` — optional new prop on `EmptyState.tsx`. Backward-compatible: the 5 other `EmptyState` usages in the codebase don't pass it.
|
||||
> - **Verification log:** `Review/sprint11-verification.md` (4-step browser smoke + race test + 2 API curls). Deploy is `git pull` + `docker compose up -d --build frontend` (frontend-only, no backend changes, no migration).
|
||||
>
|
||||
> **Sprint 12 status (committed 2026-06-05, awaiting deploy):** F8 Spoonacular search — adds a "Search the web" toggle on `/recipes` that hits Spoonacular's `complexSearch` API. Each result has an "Import" button that pulls the full recipe info (1 point) and writes a local `Recipe` row with the right schema fields. Spoonacular ingredients are upserted via the existing idempotent `POST /api/ingredients` endpoint. **No pre-existing WIP files touched.** F9 (Ollama local LLM) remains a separate full backend proposal in the §Future backlog.
|
||||
> - **T6.1** `backend/app/api/recipe_search.py` (NEW, ~270 lines). 2 endpoints: `GET /api/recipes/search?q=&limit=` (1.1 points/query, summary only — NO info endpoint call) and `POST /api/recipes/import` (1 point + ingredient upserts + Recipe insert). Process-wide `_points_used` counter with thread-safe lock; 503 when over 140 (10-point safety margin under the 150-point free tier).
|
||||
> - **T6.2** `backend/app/config.py` — added `SPOONACULAR_API_KEY: Optional[str] = None` to `Settings` (was previously read via `getattr` since `extra="ignore"`). The 503 path surfaces a clear "SPOONACULAR_API_KEY not configured" message.
|
||||
> - **T6.3** `backend/app/schemas/__init__.py` — added `RecipeSearchHit` and `RecipeImportRequest` Pydantic models. The router is registered in `main.py:62-63` at the `/api/recipes` prefix.
|
||||
> - **T6.4** Frontend: `frontend/src/api/index.ts` adds `recipes.search` + `recipes.importRecipe` + 3 stub methods (`recommended`, `listIngredients`, `createIngredient`) to satisfy pre-existing call sites that were previously hidden by a smaller API surface. `frontend/src/pages/Recipes.tsx` adds the toggle button (with `aria-pressed`) + the web-search panel (`<div role="region" aria-label="Web recipe search" aria-busy={webLoading}>`) + the import mutation (toast on success, `showApiError` on failure). `frontend/src/types/index.ts` adds optional `ingredient` + `is_optional` to `RecipeIngredient` for pre-existing MealDetail.tsx call sites.
|
||||
> - **T6.5** Pre-existing tsc errors exposed by the API surface expansion (5 errors in Pantry/MealDetail/Recommended.tsx) — resolved per user decision: added 5 stub API methods + 2 type fields. Documented in `Review/sprint12-verification.md` D-fix section.
|
||||
> - **Verification log:** `Review/sprint12-verification.md` (deploy + 4-step browser smoke + 2 API curls + quota test + a11y check + 5-risk table). Deploy is `git pull` + `docker compose up -d --build backend frontend` (backend has the new router; frontend has the new toggle).
|
||||
> - **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.
|
||||
|
||||
+23
-1
@@ -302,7 +302,7 @@ Trust the tests. Trust the live runs. Don't trust prose claims that something is
|
||||
**Current open proposals:**
|
||||
- `docs/proposals/2026-05-23-feedback-driven-recipe-discovery.md` — pending user approval. No code yet (per the 2026-05-23 section below).
|
||||
|
||||
**Last updated: 2026-06-05** — UI/UX audit & fix cycle (Sprints 1, 2, 3, 4, 5, 6, 7, 8, 9) complete. 20 findings closed (5 P0 + 6 P1 + 3 P2 + 6 §Future), code committed across 13 commits, build green. Sprint 1 deployed; Sprints 2-9 awaiting deploy. **Sprint 7 (`09c7525`, awaiting user deploy)** aligns "this week" to the upcoming Monday. **Sprint 8 (`efd1fc6`, awaiting user deploy)** implements the user's "Deny" semantics decision. **Sprint 9 (committed 2026-06-05, awaiting user deploy)** ships the F1 Onboarding Tour. **Sprint 10 (committed 2026-06-05, awaiting user deploy)** ships the "Deny Forever" on Recipes. **Sprint 11 (committed 2026-06-05, awaiting user deploy)** wires the dead "Generate Meal Plan" CTA. See Sprint 7 + Sprint 8 + Sprint 9 + Sprint 10 + Sprint 11 sections below. Full UI-audit handoff at `Review/handoff-ui-audit.md`.
|
||||
**Last updated: 2026-06-05** — UI/UX audit & fix cycle (Sprints 1, 2, 3, 4, 5, 6, 7, 8, 9) complete. 20 findings closed (5 P0 + 6 P1 + 3 P2 + 6 §Future), code committed across 13 commits, build green. Sprint 1 deployed; Sprints 2-9 awaiting deploy. **Sprint 7 (`09c7525`, awaiting user deploy)** aligns "this week" to the upcoming Monday. **Sprint 8 (`efd1fc6`, awaiting user deploy)** implements the user's "Deny" semantics decision. **Sprint 9 (committed 2026-06-05, awaiting user deploy)** ships the F1 Onboarding Tour. **Sprint 10 (committed 2026-06-05, awaiting user deploy)** ships the "Deny Forever" on Recipes. **Sprint 11 (committed 2026-06-05, awaiting user deploy)** wires the dead "Generate Meal Plan" CTA. **Sprint 12 (committed 2026-06-05, awaiting user deploy)** ships the F8 Spoonacular search. See Sprint 7 + Sprint 8 + Sprint 9 + Sprint 10 + Sprint 11 + Sprint 12 sections below. Full UI-audit handoff at `Review/handoff-ui-audit.md`.
|
||||
|
||||
---
|
||||
|
||||
@@ -385,6 +385,28 @@ Trust the tests. Trust the live runs. Don't trust prose claims that something is
|
||||
|
||||
**Path forward to F8/F9:** the `EmptyState.action.onClick` is the single seam. Future F8 (Spoonacular) or F9 (Ollama) work only needs to swap the `fillEmptySlots` call in `handleGenerateFirstPlan` for an LLM call. No DOM, copy, or component structure changes needed.
|
||||
|
||||
### Sprint 12 — F8 Spoonacular search (§Future H10) (user-driven) — COMMITTED 2026-06-05
|
||||
|
||||
**User direction (2026-06-05):** "Proceed." Selected from the question menu as the smallest remaining §Future item with a clear UI scope. F1 (Sprint 9) shipped, the dead CTA (Sprint 11) shipped, and F8 (Spoonacular) was the last piece. F9 (Ollama) remains a separate full backend proposal (model pull + ollama-py + `/api/llm/plan` endpoint).
|
||||
|
||||
**Scope (6 boxes):**
|
||||
1. **NEW** `backend/app/api/recipe_search.py` (~270 lines) — 2 endpoints + module-level quota counter with thread-safe lock. `GET /api/recipes/search?q=&limit=` (1.1 pts/query, summary only — NO info endpoint call, unlike the pre-existing `_search_spoonacular`). `POST /api/recipes/import` (1 pt + idempotent ingredient upserts + Recipe insert). 503 when over 140-pt daily budget.
|
||||
2. `backend/app/config.py` — added `SPOONACULAR_API_KEY: Optional[str] = None` to `Settings` (was previously read via `getattr` since `extra="ignore"`). The schema declaration surfaces it in `.env.example` and tools.
|
||||
3. `backend/app/schemas/__init__.py` — added `RecipeSearchHit` (mirror of the `ExternalRecipe` dataclass at `recipe_discovery.py:28-44`) + `RecipeImportRequest`.
|
||||
4. `backend/app/main.py:62-63` — registered `recipe_search_api.router` at the `/api/recipes` prefix. No collision with the pre-existing WIP `recipes.py`.
|
||||
5. **Frontend:** `frontend/src/api/index.ts` adds 5 new `recipes` methods (`search`, `importRecipe`, `recommended`, `listIngredients`, `createIngredient` — the last 3 are stubs for pre-existing call sites). `frontend/src/pages/Recipes.tsx` adds the toggle button (with `aria-pressed`) + the web-search panel (`<div role="region" aria-label="Web recipe search" aria-busy={webLoading}>`) + the import mutation (toast on success, `showApiError` on failure). Toggle defaults to OFF; reuses the existing `q` + `handleSearch` (300ms debounce).
|
||||
6. `frontend/src/types/index.ts` — added optional `ingredient` + `is_optional` to `RecipeIngredient` (for pre-existing MealDetail.tsx call sites).
|
||||
|
||||
**D-fix (user-decision):** the API surface expansion exposed 5 latent tsc errors in Pantry/MealDetail/Recommended.tsx. Resolved with 3 stub API methods + 2 type fields. ~7 lines of fixes; no WIP touched. Documented in `Review/sprint12-verification.md` D-fix section + `fix-ui-audit.md` T6.4.
|
||||
|
||||
**Build:** `npm run build` green (tsc 0 errors, vite 0 errors). Bundle: 496.48 → 500.28 kB (+3.8 kB for the web-search panel + the import mutation). Backend AST clean. Backend pytest skipped (venv on docker-willester is broken; pre-existing). One commit: `feat(ui): Sprint 12 — F8 Spoonacular search (web-search toggle + import)`.
|
||||
|
||||
**Deploy:** `git pull` + `docker compose up -d --build backend frontend` (backend has the new router; frontend has the new toggle). Verification: `Review/sprint12-verification.md` (4-step browser smoke + 2 API curls + quota test + a11y check + 5-risk table).
|
||||
|
||||
**No regression expected:** Sprint 12 doesn't touch the pre-existing WIP `backend/app/api/recipes.py` / `schemas/recipe.py` / `nginx/nginx.conf`. The new router is in a separate file (`recipe_search.py`) and registered at a non-colliding path. Sprints 1-11 are untouched. The 3 stub API methods satisfy pre-existing call sites that were previously hidden by a smaller API surface.
|
||||
|
||||
**Path forward to F9:** the `handleGenerateFirstPlan` (Sprint 11) + the `recipe_search.import` (Sprint 12) are the two seams. Future F9 (Ollama local LLM) work plugs into the same `fillEmptySlots` / `import` flow — no DOM, copy, or component structure changes needed.
|
||||
|
||||
---
|
||||
|
||||
## New session: 2026-06-05 (early)
|
||||
|
||||
@@ -646,3 +646,52 @@ User direction 2026-06-05: "Proceed." Selected from the question menu as the sma
|
||||
### T5.5 · `Review/sprint11-verification.md` (NEW)
|
||||
|
||||
- Deploy + 4-step browser smoke + race test + 2 API curls + a11y check + risks. Source of truth for the operator deploy + smoke flow. The CTA is the single seam for future F8 (Spoonacular) + F9 (Ollama) work — they only need to swap the `fillEmptySlots` call in `handleGenerateFirstPlan`.
|
||||
|
||||
---
|
||||
|
||||
## Sprint 12 — F8 Spoonacular search — ✅ COMPLETE, awaiting deploy
|
||||
|
||||
User direction 2026-06-05: "Proceed." Selected from the question menu as the smallest remaining §Future item with a clear UI scope. F1 (Sprint 9) shipped, the dead CTA (Sprint 11) shipped, and F8 (Spoonacular) was the last piece. F9 (Ollama) remains a separate full backend proposal.
|
||||
|
||||
**Status (2026-06-05):** ✅ Code complete. `npm run build` green (tsc 0 errors, vite 0 errors). Bundle: 496.48 → 500.28 kB. Backend AST clean. Backend pytest skipped (venv broken on host; known pre-existing issue). Awaiting user commit + deploy. **No new dependencies, no migration, no pre-existing WIP files touched.**
|
||||
|
||||
### T6.1 · Backend — `backend/app/api/recipe_search.py` (NEW, ~270 lines)
|
||||
|
||||
- 2 endpoints:
|
||||
- `GET /api/recipes/search?q=&limit=` (public, `require_session`) — calls Spoonacular `complexSearch` with `addRecipeInformation=true, fillIngredients=true, instructionsRequired=true`. Returns normalized `RecipeSearchHit[]`. **No info endpoint call** (saves 1 pt per result; the pre-existing `_search_spoonacular` calls the info endpoint for every result, which would burn the whole daily quota on a 10-result search).
|
||||
- `POST /api/recipes/import` (public, `require_session`) — body `{external_id, external_source: "spoonacular"}`. Fetches `/recipes/{id}/information` (1 pt), normalizes, upserts ingredients via the existing idempotent `_upsert_ingredient` helper (mirrors the public `POST /api/ingredients` logic without the HTTP roundtrip), creates a local `Recipe` with `external_source="spoonacular"` + `external_id` + `is_manually_added=True`, returns the new recipe id.
|
||||
- Process-wide `_points_used` counter (module-level singleton + `threading.Lock`). 503 with `detail: "spoonacular daily quota reached; try again tomorrow"` when over 140 (10-pt safety margin under the 150-pt free tier). Resets on process restart.
|
||||
- 503 with `detail: "SPOONACULAR_API_KEY not configured; set it in the backend env"` when env var unset. Logged once.
|
||||
- Idempotent import: 409 with `detail: "recipe already imported: <id>"` if a row with the same `(external_source, external_id)` already exists.
|
||||
|
||||
### T6.2 · Backend — config + schemas + main.py wiring
|
||||
|
||||
- **File:** `backend/app/config.py` — added `SPOONACULAR_API_KEY: Optional[str] = None` to `Settings`. Was previously read via `getattr` because `extra="ignore"` silently accepts unknown env vars. The schema declaration surfaces it in `.env.example` and tools; runtime behavior is unchanged.
|
||||
- **File:** `backend/app/schemas/__init__.py` — added `RecipeSearchHit` (Pydantic mirror of the `ExternalRecipe` dataclass at `recipe_discovery.py:28-44`) and `RecipeImportRequest` (just `external_id` + `external_source`).
|
||||
- **File:** `backend/app/main.py:62-63` — registered `recipe_search_api.router` at the `/api/recipes` prefix. No collision with the pre-existing WIP `recipes.py` (which registers `GET /api/recipes`, `GET /api/recipes/recommended`, `GET /api/recipes/{id}`).
|
||||
|
||||
### T6.3 · Frontend — `Recipes.tsx` toggle + panel + mutation
|
||||
|
||||
- **File:** `frontend/src/pages/Recipes.tsx` — added the "Search the web" toggle button (with `aria-pressed={searchWeb}`) to the header. Toggle defaults to OFF so the existing UX is preserved. When ON, a `<div role="region" aria-label="Web recipe search" aria-busy={webLoading}>` panel renders above the local list. The panel reuses the existing `q` + `handleSearch` (line 77-81, 300ms debounce) so the local search bar drives both. The `useQuery` for the web search is `enabled: searchWeb && debouncedQ.length >= 2` to avoid burning quota on idle toggling.
|
||||
- `importMutation` (useMutation) calls `mealPlannerApi.recipes.importRecipe`; on success, marks the hit as imported (local `Set<string>` of external_ids) + invalidates `['recipes']` + shows a success toast. On error, uses `showApiError` (Sprint 4 F7).
|
||||
- The "Import" button state machine: "Import" (Sparkles icon) → "Importing…" (Loader2 spin) → "Imported" (Check, disabled). Communicates state via label + icon.
|
||||
- **File:** `frontend/src/api/index.ts` — added `recipes.search(q, limit)` + `recipes.importRecipe(data)` + 3 stub methods (`recommended`, `listIngredients`, `createIngredient`) for pre-existing call sites.
|
||||
|
||||
### T6.4 · D-fix — pre-existing tsc errors exposed by the API surface expansion
|
||||
|
||||
- Adding 5 new methods to `mealPlannerApi.recipes` (search, importRecipe, recommended, listIngredients, createIngredient) caused TypeScript to evaluate the recipes object as a closed type, exposing 5 latent errors in Pantry.tsx / MealDetail.tsx / Recommended.tsx (calls to non-existent `listIngredients` / `createIngredient` / `recommended` + 2 missing fields on `RecipeIngredient`).
|
||||
- **User decision:** add stub methods + fix the `RecipeIngredient` type. 7 lines of fixes total; no pre-existing WIP touched.
|
||||
- **File:** `frontend/src/types/index.ts` — added optional `ingredient: { id: string; name: string }` + `is_optional: boolean` to `RecipeIngredient`. The backend JSONB column can carry arbitrary keys; we surface the most common ones as optional.
|
||||
|
||||
### T6.5 · Sprint 12 verification gate
|
||||
|
||||
- [x] `npm run build` green (tsc 0 errors, vite 0 errors). Bundle: 496.48 → 500.28 kB.
|
||||
- [x] Backend AST clean on all 4 changed files (recipe_search.py, config.py, schemas/__init__.py, main.py).
|
||||
- [ ] Backend pytest skipped — venv on docker-willester is broken (pre-existing, not caused by Sprint 12). Pytest is part of the operator's deploy checklist; the 4 tests in `backend/tests/test_recipe_search.py` would cover: search happy path, search empty query (422), import happy path, import duplicate (409). The endpoint code follows the same patterns as the existing `never_suggest.py` and `meals.py` routers.
|
||||
- [ ] Browser smoke (4 steps) on `http://100.108.208.56:8082/recipes` per `Review/sprint12-verification.md`.
|
||||
- [ ] Quota test: 50 searches in a row, 51st returns 503.
|
||||
- [ ] No regression in Sprints 1-11.
|
||||
|
||||
### T6.6 · `Review/sprint12-verification.md` (NEW)
|
||||
|
||||
- Deploy + 4-step browser smoke + 2 API curls + quota test + a11y check + 5-risk table + future work section. Source of truth for the operator deploy + smoke flow.
|
||||
|
||||
Reference in New Issue
Block a user