From 8cb4d4198c2e14a5852900338b48121528a69506 Mon Sep 17 00:00:00 2001 From: Peter Woolery Date: Fri, 5 Jun 2026 16:59:20 -0700 Subject: [PATCH] =?UTF-8?q?docs:=20Sprint=2013=20=E2=80=94=20F9-lite=20(Ol?= =?UTF-8?q?lama=20Cloud=20free-text=20plan=20synthesis)=20across=20all=206?= =?UTF-8?q?=20running=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sprint 13 (commit bae9403) splits the Sprint 11 "Generate Meal Plan" CTA into a 2-step modal: "Use the recipe library" (default, Sprint 11 unchanged) or "Ask the LLM" (new). The LLM path POSTs to /api/llm/plan; the backend calls kimi-k2.6:cloud on ollama.com, parses the LLM’s JSON picks, creates a fresh plan, fills the LLM’s picks, and falls through to the Sprint 6+ fillEmptySlots pattern for the slots the LLM didn’t cover. No pre-existing WIP files touched. This commit updates the 6 running docs that track the sprint: - .agent/plan.md — Sprint 13 section (S13.1-S13.4) added. - .agent/context.md — Sprint 13 (D1-D9, Q1-Q3) added; file:line references; key takeaways. - Review/sprint13-verification.md — new file: 3-step browser smoke + 4 API curls + a11y check + 6-risk table + future work section. - Review/ui-nielsen-audit.md — Sprint 13 status block (T7.1-T7.3) at the top, after the Sprint 12 block. - fix-ui-audit.md — Sprint 13 section (T7.1-T7.5) added after the Sprint 12 section. - Review/handoff-ui-audit.md — Batch I added to the deploy instructions; Sprint 13 section added after Sprint 12; TL;DR table row 13 added; Last-updated footer updated. - docs/HANDOFF.md — Sprint 13 section added after the Sprint 12 section, with a path-forward paragraph for F9-full. All 6 docs now reflect Sprint 13. §Future backlog remaining: F9-full (local Ollama model pull on the host) — opt-in based on cloud-billing feedback. _ask_llm is the single seam: F9-full only needs to swap the URL + model name. --- .agent/context.md | 61 ++++++++++++++++ .agent/plan.md | 77 ++++++++++++++++++++ Review/handoff-ui-audit.md | 24 +++++-- Review/sprint13-verification.md | 122 ++++++++++++++++++++++++++++++++ Review/ui-nielsen-audit.md | 6 ++ docs/HANDOFF.md | 23 +++++- fix-ui-audit.md | 54 ++++++++++++++ 7 files changed, 361 insertions(+), 6 deletions(-) create mode 100644 Review/sprint13-verification.md diff --git a/.agent/context.md b/.agent/context.md index e2d9e3f..25ab09e 100644 --- a/.agent/context.md +++ b/.agent/context.md @@ -408,3 +408,64 @@ The user can browse ~150 local recipes on `/recipes` (admin seeds them) but has - `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) +--- + +# Context — Sprint 13 (F9-lite Ollama Cloud plan synthesis) + +## Why Sprint 13 exists + +User direction 2026-06-05: "Proceed." F9-lite reuses the pre-existing `OLLAMA_*` config (`backend/app/config.py:36-38: OLLAMA_BASE_URL=https://ollama.com/v1, OLLAMA_API_KEY, OLLAMA_MODEL=kimi-k2.6:cloud`). Avoids the local model pull (F9-full would be 4 GB on disk + a separate `ollama serve` process). Cloud LLM — operator's existing OLLAMA billing applies per call. + +The Sprint 11 "Generate Meal Plan" CTA was library-only. Sprint 13 splits it into a 2-step modal: "Use the recipe library" (Sprint 11 unchanged) or "Ask the LLM" (new). The LLM path lets the user describe what they want for the week ("Italian-inspired, vegetarian", "easy weeknight dinners, no fish") and uses kimi-k2.6:cloud on ollama.com to pick meals from the local library. + +## Decisions (locked in for Sprint 13) + +- **D1. Reuse the pre-existing `llm_matcher._ask_ollama` call pattern.** `backend/app/services/llm_matcher.py:97-144` already implements the exact `POST ${OLLAMA_BASE_URL}/chat/completions` + `Authorization: Bearer ${OLLAMA_API_KEY}` + `model: settings.OLLAMA_MODEL` + `max_tokens` + `temperature: 0` + `re.sub` for `` blocks pattern. Sprint 13's `_ask_llm` helper mirrors it (same call, same headers, same strip). The only difference: `max_tokens=800` (vs `500`) for kimi-k2's reasoning headroom — 21 picks + per-pick UUID validation can run long. +- **D2. 200-recipe cap on the library sent to the LLM.** Larger libraries would exceed the prompt token budget for kimi-k2.6. The cap is alphabetical-by-name, deterministic. The library fill on the back end iterates the full library regardless of the cap. +- **D3. Tolerant JSON parsing.** The LLM may return valid JSON, JSON in markdown code fences, JSON with trailing commentary, or pure prose. `_parse_picks` handles all four: tries the regex for ` ```json ... ``` ` first, then finds the first `[` and the matching `]`, then `json.loads`. On any failure, returns `[]` — the library fill takes over. The user never sees a crash. +- **D4. Per-pick validation.** Each entry must have a valid `day_of_week` (1-7), a known `meal_type` (breakfast/lunch/dinner), AND a `recipe_id` that's in the local library. Invalid entries are dropped silently. The library fill then covers the dropped slots. +- **D5. Library fill = Sprint 6+ pattern, re-implemented inline.** The original `meals.fillEmptySlots` is a public HTTP endpoint. Calling it from `/api/llm/plan` would be a self-HTTP-call (works but ugly). Re-implementing the logic inline (read library, skip already-used recipe_ids, pick the first remaining) keeps the endpoint self-contained. Same partial-success semantics. +- **D6. Modal is inline in `Dashboard.tsx`, not a separate component.** Depends on 4 local states + 3 handlers. Extracting to a separate component would require prop-drilling or context. ~50 lines of JSX, easy to read inline. +- **D7. 60s LLM timeout.** kimi-k2.6:cloud typical latency is 5-15s for a 21-pick request. 60s is generous. On timeout, the user sees a success toast with `picked_count: 0` — same UX as if the LLM returned 0 picks. +- **D8. `OLLLAMA_CLOUD` costs apply.** The endpoint is public + `require_session`-gated (no public abuse). A future sprint could add a per-day rate limit if the operator's OLLAMA billing becomes painful. Out of scope for the initial ship. +- **D9. Default mode = "Use the recipe library".** The LLM mode is opt-in. Users who don't toggle the radio get the Sprint 11 flow (free, fast, no API call). Users who want the LLM experience explicitly opt in by selecting the radio + typing a prompt. + +## Open questions to surface to the user, not to assume + +- **Q1. Should the LLM-picked items be visually distinguished from the library-filled items in the plan grid?** Default: no (out of scope). The Sprint 8 vote + Sprint 10 deny buttons work on any item, LLM-picked or not. A future sprint could add a small "✨ LLM pick" badge. +- **Q2. Should the prompt modal remember the user's last prompt + last mode across sessions?** Default: no (would require `localStorage`). A future sprint could persist `{promptMode, lastPrompt}` to `mealplanner:*` localStorage keys. +- **Q3. Should the LLM call be streaming (show a "thinking…" indicator that updates as the model generates)?** Default: no. The Ollama streaming API is a different request shape (`stream: true` + NDJSON parsing). Out of scope for the initial ship. A future sprint could add streaming with a "Asking LLM… (so far: )" toast. + +## Sprint 13 verification gate + +- `cd frontend && npm run build` → green (tsc 0 errors, vite 0 errors) +- Backend AST clean on all 3 changed files +- Browser smoke (3 steps) on `http://100.108.208.56:8082/` per `Review/sprint13-verification.md` +- 4 API curls: happy path + OLLAMA_API_KEY unset (503) + empty prompt (422) + duplicate week (400) +- No regression in Sprints 1-12 + +## Sprint 13 — does NOT touch + +- The OnboardingTour (Sprint 9) — unchanged. The tour's first step is the Dashboard's Weekly Overview card; the modal renders on top via `z-50` + `bg-black/40` scrim. +- The NeverSuggestButton (Sprint 10) — unchanged. +- The handleGenerateFirstPlan (Sprint 11) — extracted into `generateFromLibrary` (identical body) + `generateFromLLM` (new). +- The web-search toggle + import flow (Sprint 12) — unchanged. Sprint 12's `recipes.search` + `recipes.importRecipe` are independent of the LLM synthesis. +- The pre-existing WIP `backend/app/api/recipes.py` / `schemas/recipe.py` / `nginx/nginx.conf` — untouched. +- The existing `RecipeDiscoveryService` at `backend/app/services/recipe_discovery.py` — unchanged. Sprint 13 uses the pre-existing `OLLAMA_*` config, not the service. +- The existing `llm_matcher.py` — unchanged. Sprint 13's `_ask_llm` helper mirrors `_ask_ollama` but is a separate function (different `max_tokens`, different prompt shape). + +## Key file:line references (Sprint 13) + +- `backend/app/services/llm_matcher.py:97-144` — `_ask_ollama` (the call pattern Sprint 13 mirrors) +- `backend/app/services/recipe_enrichment.py:25-106` — parallel LLM helper (different prompt shape; not reused) +- `backend/app/config.py:36-38` — OLLAMA config (reused as-is) +- `backend/app/api/llm_plan.py` (NEW, ~280 lines) — Sprint 13 endpoint + helpers +- `backend/app/models/__init__.py:200-211` — `MealPlan` model (target for the create) +- `backend/app/models/__init__.py:223-224` — `MealPlanItemBase` (target for the items) +- `backend/app/schemas/__init__.py:255-265` — `LLMPlanRequest` + `LLMPlanResponse` (S13.1 schemas) +- `backend/app/main.py:65-66` — `llm_plan_api.router` registration +- `frontend/src/api/index.ts:124-128` — `llm.plan` client method +- `frontend/src/pages/Dashboard.tsx:399-487` — `handlePromptSubmit` + `generateFromLibrary` + `generateFromLLM` (the 3 Sprint 13 handlers) +- `frontend/src/pages/Dashboard.tsx:775-870` — `renderPromptModal` (the Sprint 13 modal) +- `Review/sprint13-verification.md` — new file (deploy + 3-step browser smoke + 4 API curls + a11y check + 6-risk table) + diff --git a/.agent/plan.md b/.agent/plan.md index fed6cca..f38a7fd 100644 --- a/.agent/plan.md +++ b/.agent/plan.md @@ -424,3 +424,80 @@ User reported post-deploy: "The tour window looks great, but Clicking the X nor - **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. + +--- + +## Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis) — DRAFTED, awaiting user approval + +**User direction (2026-06-05):** "Proceed." F9-lite reuses the pre-existing `OLLAMA_*` config (config.py:36-38: `OLLAMA_BASE_URL=https://ollama.com/v1`, `OLLAMA_API_KEY`, `OLLAMA_MODEL=kimi-k2.6:cloud`). Avoids the local model pull (F9-full would be 4 GB on disk + a separate uvicorn process). Cloud LLM — costs apply per call (operator's existing OLLAMA billing). + +**Pre-existing infrastructure to reuse (not recreate):** +- `backend/app/services/llm_matcher.py:97-144` — `_ask_ollama(ingredient_name, candidates)` helper. The exact call pattern Sprint 13 mirrors: `POST ${OLLAMA_BASE_URL}/chat/completions` with `Authorization: Bearer ${OLLAMA_API_KEY}`, `model: settings.OLLAMA_MODEL`, `max_tokens: 500, temperature: 0`, parse `choices[0].message.content`, strip `` blocks. +- `backend/app/services/recipe_enrichment.py` — parallel LLM helper for recipes (different prompt shape; not reused). +- `backend/app/config.py:36-38` — OLLAMA config. + +**Pre-existing WIP (NOT touched):** same as Sprint 12. + +### S13.1 — Backend: `POST /api/llm/plan` (public, webui-facing) + +- [ ] **NEW** `backend/app/api/llm_plan.py` — single endpoint + a thin `_ask_llm(prompt)` helper. The endpoint: + 1. Validates the request body (`prompt: str` 1-500 chars, `week_start: date`). + 2. Reads the local recipe library (`Recipe` table, filtered by `family_profile_id`); serializes a compact list `{id, name, cuisine_tags, dietary_tags, protein_type, total_time_minutes, dietary preferences}`. + 3. Builds a prompt: "You are planning a 7-day meal plan (Mon-Sun). The user wants: ''. Pick up to 21 meals (7 breakfasts + 7 lunches + 7 dinners) from the recipe library. Return JSON: `[{"day_of_week": 1-7, "meal_type": "breakfast|lunch|dinner", "recipe_id": ""}]`. If a slot has no good match, omit it. Use only recipe_ids from the list. Reply with JSON only — no commentary." + 4. Calls `_ask_llm(prompt)` (mirrors `_ask_ollama` from `llm_matcher.py:97-144`). + 5. Parses the JSON response (try `json.loads`, fall back to `re.search(r"\[.*\]", content)` to handle markdown code fences). + 6. Validates each entry: `recipe_id` is a UUID, `day_of_week` in 1-7, `meal_type` in {breakfast, lunch, dinner}. Drop invalid entries. + 7. Creates an empty plan via `meals.create` (Sprint 6+ endpoint), then bulk-inserts the LLM-picked items + calls `fillEmptySlots` for the slots the LLM didn't cover. + 8. Returns `{plan_id, picked_count, filled_count, failed_count, reasoning: }`. +- [ ] **MODIFIED** `backend/app/schemas/__init__.py` — add `LLMPlanRequest` + `LLMPlanResponse` Pydantic models. +- [ ] **MODIFIED** `backend/app/main.py` — register the new router at `/api/llm`. +- [ ] 503 with clear `detail: "OLLAMA_API_KEY not configured"` when env var unset. +- [ ] 422 on empty / oversized prompt. +- [ ] Cap on library size sent to the LLM: 200 recipes max (alphabetical by name). Larger libraries would exceed prompt tokens. + +### S13.2 — Frontend: free-text prompt in the Sprint 11 flow + +- [ ] **MODIFIED** `frontend/src/pages/Dashboard.tsx` — turn the Sprint 11 `handleGenerateFirstPlan` into a 2-step: + 1. New `MealPlanPromptModal` component (inline in `Dashboard.tsx`, ~40 lines, reuse `Card`/`Button`/`Input` from `components/ui/*`): a small modal with a textarea (max 500 chars, char counter) + two radio options: "Use the recipe library" (default) and "Ask the LLM". On submit, calls one of two API methods. + 2. **Library path** (default): keep Sprint 11's `meals.create` + `fillEmptySlots` exactly as-is. + 3. **LLM path** (new): `mealPlannerApi.llm.plan({prompt, week_start})`. On success, invalidate `['mealPlan', weekStart]` and toast `"Planned N meals (LLM picked N, library filled the rest)"`. +- [ ] **MODIFIED** `frontend/src/api/index.ts` — add `llm.plan(data)`. +- [ ] A11y: modal has `role="dialog"`, `aria-modal="true"`, focus trapped on the textarea on open, restored to the CTA button on close. Esc dismisses. Tab cycles within the modal. + +### S13.3 — Verify + +- [ ] `cd frontend && npm run build` → green (tsc 0 errors, vite 0 errors). +- [ ] Backend AST clean. +- [ ] Manual API smoke: + - `curl -X POST /api/llm/plan -d '{"prompt": "easy weeknight dinners, no fish", "week_start": "2026-06-08"}'` → 200 with `{plan_id, picked_count: 7+, filled_count: 14-, ...}`. (Requires `OLLAMA_API_KEY` set on the host.) + - With `OLLAMA_API_KEY` unset → 503. + - Empty prompt → 422. +- [ ] Manual UI smoke (3 steps): + 1. Land on `/` with no plan. Click "Generate Meal Plan". Modal opens with the textarea + the two radio options. + 2. Type "Italian-inspired, vegetarian" + select "Ask the LLM" + click submit. Confirm: button label flips to "Asking LLM…", modal shows a small spinner, ~5-15s later the modal closes, plan grid renders, toast shows the picked/filled split. + 3. Refresh the page. Confirm the plan persists. +- [ ] No regression in Sprints 1-12. + +### S13.4 — Docs (all 6 running docs updated) + +- [ ] `Review/ui-nielsen-audit.md` — Sprint 13 status block. +- [ ] `fix-ui-audit.md` — Sprint 13 plan section (T7.1-T7.4). +- [ ] `Review/handoff-ui-audit.md` — Batch I + Sprint 13 entry + TL;DR row 13. +- [ ] `docs/HANDOFF.md` — Sprint 13 section. +- [ ] `.agent/plan.md` — this section. +- [ ] `.agent/context.md` — Sprint 13 decisions + file:line references. +- [ ] `Review/sprint13-verification.md` — written. + +### Done when (Sprint 13) + +- All boxes above ticked. +- `npm run build` green. +- `Review/sprint13-verification.md` exists. +- All 6 doc files have a Sprint 13 status block. + +### Out of scope (Sprint 13) + +- **F9-full — local Ollama model pull on the host.** Would require pulling Mistral 7B or Llama 3 8B (~4 GB) + a separate `ollama serve` process + a different config (`OLLAMA_BASE_URL=http://localhost:11434`). F9-lite uses the cloud tier. Future sprint if the cloud costs become painful. +- **Prompt engineering / quality iteration.** The prompt is a first cut. If the LLM returns 0 picks or 21 identical recipes, the operator can iterate on the prompt. Out of scope for the initial ship. +- **Multi-week plans.** One week at a time. +- **Save the prompt as a template** for reuse. Future sprint. diff --git a/Review/handoff-ui-audit.md b/Review/handoff-ui-audit.md index f9414f3..9890ab8 100644 --- a/Review/handoff-ui-audit.md +++ b/Review/handoff-ui-audit.md @@ -21,14 +21,15 @@ If you are a new agent continuing this work, do this **in order**: - **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). - **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). + - **Batch I:** Sprint 13 (one `git pull`, `docker compose up -d --build backend frontend` — backend has the new `llm_plan.py` router, frontend has the new prompt modal). +4. **Open issues** in `.agent/plan.md` (the "Phase R1-R3" section is a prior plan; the **Sprint 13 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 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. +- The §Future backlog is now empty: F1 (onboarding) shipped as Sprint 9; the dead "Generate Meal Plan" CTA shipped as Sprint 11; F8 (Spoonacular) shipped as Sprint 12; F9-lite shipped as Sprint 13. All four are deployment-pending. F9-full (local Ollama model pull) is the only remaining §Future item — opt-in based on cloud-billing feedback. - Pre-existing repo issues: 1 failing test (`test_filter_blocks_by_cost` — verified pre-Sprint 8), `.gitignore` `lib/` bug, no CI. Documented. --- @@ -54,7 +55,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. 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.** +**Thread 3 (§Future backlog) is deferred** until S8 is deployed + verified. F9-full (local Ollama model pull on the host) remains a future sprint. **Sprint 9 (F1 onboarding) + post-deploy fix (`1562929`), Sprint 10 (Deny Forever on Recipes), Sprint 11 (wire the dead "Generate Meal Plan" CTA), Sprint 12 (F8 Spoonacular search), and Sprint 13 (F9-lite Ollama Cloud plan synthesis) are all committed 2026-06-05, awaiting user deploy.** ### Sprint 9 — F1 Onboarding Tour (H10) @@ -118,6 +119,18 @@ If you are a new agent continuing this work, do this **in order**: **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 13 — F9-lite (Ollama Cloud plan synthesis) (§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 Sprint 11 "Generate Meal Plan" CTA was library-only. Sprint 13 splits it into a 2-step modal: "Use the recipe library" (Sprint 11 unchanged) or "Ask the LLM" (new). The LLM path lets the user describe what they want for the week ("Italian-inspired, vegetarian") and uses kimi-k2.6:cloud on ollama.com to pick meals from the local library. + +**Scope (5 boxes):** NEW `backend/app/api/llm_plan.py` (~280 lines, 1 endpoint + 4 helpers + tolerance for malformed LLM responses), 2 schema additions (`LLMPlanRequest` + `LLMPlanResponse`), 1 router registration, 1 `mealPlannerApi.llm.plan` method, prompt modal in `Dashboard.tsx` (radio + textarea + click-outside-to-dismiss). **No new dependencies. No migration. No pre-existing WIP files touched.** Reuses `OLLAMA_BASE_URL` / `OLLAMA_API_KEY` / `OLLAMA_MODEL` from `config.py:36-38`. + +**LLM tolerance:** a 60s timeout, parse-failure (markdown code fences, trailing commentary), or empty response all return 0 picks; the library fill takes over. The user sees a success toast with `picked_count: 0` — same as if the LLM had returned 0 picks — never a crash. + +**Tracking docs:** `Review/sprint13-verification.md` (deploy + 3-step browser smoke + 4 API curls + a11y check + 6-risk table), `Review/ui-nielsen-audit.md` Sprint 13 status block, `fix-ui-audit.md` T7.1–T7.5, this file, `docs/HANDOFF.md` Sprint 13 section. + ### Sprint 7 — Fix webui "empty meal plan" (date-semantics mismatch) **Status: COMMITTED `09c7525` on 2026-06-05. Build green.** Awaiting user to `git pull` + run the SQL fix + rebuild. @@ -149,8 +162,9 @@ Twelve commits land all 14 audit findings + 6 §Future items + 2 user-driven spr | 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) | +| 13 | (committed 2026-06-05) | F9-lite Ollama Cloud plan synthesis — prompt modal on Dashboard CTA. New `POST /api/llm/plan`. Library-or-LLM radio; 60s timeout tolerance; library fills the rest. | ✅ green | ⚠️ committed; awaiting user deploy (backend + frontend) | -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/`.** +All work is on `main` ahead of `origin/main` (pre-existing WIP also present). All 13 sprints compile. **Sprint 1 is live. Sprints 2-13 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. @@ -394,4 +408,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), 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. +**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), Sprint 12 (F8 Spoonacular search), and Sprint 13 (F9-lite Ollama Cloud plan synthesis) 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. diff --git a/Review/sprint13-verification.md b/Review/sprint13-verification.md new file mode 100644 index 0000000..75cf02b --- /dev/null +++ b/Review/sprint13-verification.md @@ -0,0 +1,122 @@ +# Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis) — verification + +**Status (2026-06-05):** ✅ Code complete. `npm run build` green. Awaiting user deploy. + +## Summary + +Sprint 13 splits the Sprint 11 "Generate Meal Plan" CTA into a 2-step modal: the user picks "Use the recipe library" (default, Sprint 11's existing flow) or "Ask the LLM" (new). The LLM path POSTs to `/api/llm/plan` with a free-text prompt; the backend calls `kimi-k2.6:cloud` on `ollama.com`, parses the LLM's JSON picks, creates a fresh plan, fills the LLM's picks, and falls through to the Sprint 6+ `fillEmptySlots` pattern for the slots the LLM didn't cover. + +**No pre-existing WIP files touched.** Sprint 13 creates a new `backend/app/api/llm_plan.py` router (separate from the existing `llm_matcher.py` service) and reuses the established LLM call pattern (POST `${OLLAMA_BASE_URL}/chat/completions`, `Authorization: Bearer ${OLLAMA_API_KEY}`, same `max_tokens: 800, temperature: 0`, strip `` blocks). + +## Files changed + +### Backend +- **NEW** `backend/app/api/llm_plan.py` (~280 lines) — 1 endpoint + 4 helpers: `_ensure_ollama_configured`, `_serialize_library`, `_ask_llm` (mirrors `llm_matcher._ask_ollama`), `_parse_picks` (tolerant of markdown code fences), `_validate_picks`. 200-recipe cap on the library sent to the LLM. +- **MODIFIED** `backend/app/schemas/__init__.py` — added `LLMPlanRequest` + `LLMPlanResponse`. +- **MODIFIED** `backend/app/main.py` — registered `llm_plan_api.router` at `/api/llm`. + +### Frontend +- **MODIFIED** `frontend/src/api/index.ts` — added `llm.plan(data)`. +- **MODIFIED** `frontend/src/pages/Dashboard.tsx` — added the prompt modal (radio for library vs. LLM + textarea for the LLM path) + extracted Sprint 11's body into `generateFromLibrary` + added `generateFromLLM`. New state: `showPromptModal`, `promptMode`, `promptText`, `promptBusy`. The CTA now opens the modal; the modal's submit button dispatches on the radio. + +## 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-CPlRXkCg.css 42.38 kB │ gzip: 7.30 kB +dist/assets/index-665b6KrF.js 503.82 kB │ gzip: 154.33 kB +✓ built in 2.84s +``` + +- `tsc` 0 errors, `vite` 0 errors. +- Bundle: 500.28 → 503.82 kB (+3.5 kB for the modal + the LLM handler). +- Backend AST clean on all 3 changed files. + +## Backend verification (manual, post-deploy) + +```bash +# Requires OLLAMA_API_KEY + OLLAMA_BASE_URL + OLLAMA_MODEL set in the +# backend env. (All three already exist; Sprint 13 just adds the +# router that reads them.) + +# 1) Happy path +curl -sS -X POST 'http://100.108.208.56:8082/api/llm/plan' \ + -H 'Content-Type: application/json' \ + -H 'Cookie: mealplanner_session=...' \ + -d '{"prompt": "Italian-inspired, vegetarian", "week_start": "2026-06-08"}' | jq + +# Expected: { "plan_id": "", "picked_count": 7+, +# "filled_count": 14-, "failed_count": 0, +# "reasoning": null } + +# 2) OLLAMA_API_KEY unset (or temporarily unset in env + restart) +curl -sS -X POST 'http://100.108.208.56:8082/api/llm/plan' \ + -H 'Content-Type: application/json' \ + -H 'Cookie: mealplanner_session=...' \ + -d '{"prompt": "test", "week_start": "2026-06-08"}' -i | head -1 +# Expected: HTTP/1.1 503 Service Unavailable + +# 3) Empty prompt +curl -sS -X POST 'http://100.108.208.56:8082/api/llm/plan' \ + -H 'Content-Type: application/json' \ + -H 'Cookie: mealplanner_session=...' \ + -d '{"prompt": "", "week_start": "2026-06-08"}' -i | head -1 +# Expected: HTTP/1.1 422 Unprocessable Entity + +# 4) Plan for this week already exists +curl -sS -X POST 'http://100.108.208.56:8082/api/llm/plan' \ + -H 'Content-Type: application/json' \ + -H 'Cookie: mealplanner_session=...' \ + -d '{"prompt": "test", "week_start": "2026-06-01"}' -i | head -1 +# Expected: HTTP/1.1 400 Bad Request (if a plan for 2026-06-01 exists) +``` + +## Browser smoke (3 steps) + +Run on `http://100.108.208.56:8082/`. + +1. **Land on `/` with no plan.** Click "Generate Meal Plan". Confirm: the modal opens with the title "Generate Meal Plan", a short description, and the 2 radio options ("Use the recipe library" selected by default, "Ask the LLM" unselected). The "Generate" button is enabled; the "Cancel" button is enabled. +2. **Type "Italian-inspired, vegetarian" + select "Ask the LLM" + click Generate.** Confirm: button label flips to "Asking LLM…" (with a spinning Loader2 icon), the modal is non-dismissible, ~5-15s later (depends on the LLM latency) the modal closes, the plan grid renders with the LLM's picks, and a toast shows `"Planned N meals (LLM picked K, library filled the rest)"`. +3. **Refresh the page.** Confirm the plan persists. The empty state does NOT re-appear. + +## A11y check + +- The modal is a `
` with a click-outside-to-dismiss handler (skipped while `promptBusy` is true). +- The radios are real `` with associated `