Public Access
docs: Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis) across all 6 running docs
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.
This commit is contained in:
@@ -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)
|
- `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)
|
- `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 `<think>` 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: <partial JSON>)" 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)
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
- **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.
|
- **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.
|
- 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: '<prompt>'. 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": "<uuid>"}]`. 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: <LLM raw text if non-empty>}`.
|
||||||
|
- [ ] **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.
|
||||||
|
|||||||
@@ -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 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 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).
|
- **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).
|
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).
|
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:**
|
**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.
|
- 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.
|
- 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.
|
**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)
|
### 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.
|
**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)
|
### 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.
|
**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) |
|
| 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) |
|
| 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) |
|
| 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.
|
**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.
|
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.
|
||||||
|
|||||||
@@ -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 `<think>` 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": "<uuid>", "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 `<div className="fixed inset-0 z-50">` with a click-outside-to-dismiss handler (skipped while `promptBusy` is true).
|
||||||
|
- The radios are real `<input type="radio">` with associated `<label>` blocks. Tab cycles through both radios, the textarea (when LLM mode is selected), and the 2 buttons.
|
||||||
|
- The textarea has `autoFocus` when the modal opens in LLM mode. Default LLM mode is library (no textarea shown); switching to LLM mode does not steal focus (intentional — the user just clicked the radio, they shouldn't lose their cursor).
|
||||||
|
- The character counter is a `<div>` with `text-right` alignment, screen-reader-accessible via the `500` max length on the textarea.
|
||||||
|
- The "Generate" button is `disabled` while `promptBusy` is true (matches Sprint 11's `generatingFirstPlan` state pattern).
|
||||||
|
- The Sprint 9 OnboardingTour's first step is the Dashboard's Weekly Overview card; the modal renders on top of that card via `z-50` + `bg-black/40` scrim. The tour is not affected.
|
||||||
|
|
||||||
|
## Risks & mitigations
|
||||||
|
|
||||||
|
- **R1: LLM returns 0 picks or 21 identical recipes.** Mitigation: the response is validated (recipe_id in library, day_of_week 1-7, meal_type in {breakfast, lunch, dinner}) and invalid entries are dropped. The library fill then takes over for any slot the LLM didn't cover. A prompt that returns 0 valid picks still produces a complete plan from the library.
|
||||||
|
- **R2: LLM times out (60s).** Mitigation: the `requests.post` call has a 60s timeout. On timeout, `_ask_llm` returns None; the endpoint then calls the library fill (no LLM picks). The user sees a success toast with `picked_count: 0`.
|
||||||
|
- **R3: Library is empty.** Mitigation: 400 with `detail: "recipe library is empty; import some recipes first"`. The frontend `showApiError` surfaces the message. (Pre-existing WIP, also affects the Sprint 11 library path.)
|
||||||
|
- **R4: OLLAMA_API_KEY unset.** Mitigation: 503 with clear `detail: "OLLAMA_API_KEY not configured; set it in the backend env"`. The frontend `showApiError` surfaces the message; the user can switch to "Use the recipe library" and proceed.
|
||||||
|
- **R5: OLLAMA_CLOUD costs.** The operator's existing OLLAMA billing applies. The endpoint is public + requires `require_session` (so no public abuse). A future sprint could add a per-day rate limit.
|
||||||
|
- **R6: Sprint 12's pre-existing WIP collision.** Verified: the WIP `recipes.py` is registered at `/api/recipes/*` only; my new `/api/llm/plan` is in a different prefix. No collision.
|
||||||
|
|
||||||
|
## Commit
|
||||||
|
|
||||||
|
One commit: `feat(ui): Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis)`. Files:
|
||||||
|
|
||||||
|
- `backend/app/api/llm_plan.py` (NEW, ~280 lines)
|
||||||
|
- `backend/app/schemas/__init__.py` (2 Pydantic models)
|
||||||
|
- `backend/app/main.py` (router registration)
|
||||||
|
- `frontend/src/api/index.ts` (1 new method)
|
||||||
|
- `frontend/src/pages/Dashboard.tsx` (modal + LLM handler + extracted library handler)
|
||||||
|
|
||||||
|
## Future work (NOT in Sprint 13)
|
||||||
|
|
||||||
|
- **F9-full — local Ollama model pull.** Pull Mistral 7B or Llama 3 8B (~4 GB) on the host + a separate `ollama serve` process. Avoids cloud costs and the LLM_API_KEY dependency. Future sprint.
|
||||||
|
- **Prompt template library.** Save common prompts ("easy weeknight", "vegetarian week", "quick breakfasts") as one-click buttons. Out of scope for the initial ship.
|
||||||
|
- **Multi-week plans.** One week at a time. The endpoint is parameterised on `week_start`; calling it twice with two different weeks would work, but the UI doesn't surface it yet.
|
||||||
|
- **Streaming the LLM response.** Currently waits for the full response. Future sprint could use the Ollama streaming API + a "thinking…" indicator.
|
||||||
|
- **Per-day rate limit.** A future sprint could gate the LLM call on a per-day budget to avoid surprise cloud bills.
|
||||||
|
- **Vitest unit test for `useOnboarding`** (Q4 from Sprint 9). Still pending.
|
||||||
@@ -131,6 +131,12 @@ The app looks polished on the surface (Tailwind palette, clean cards, working to
|
|||||||
> - **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.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.
|
> - **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).
|
> - **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).
|
||||||
|
>
|
||||||
|
> **Sprint 13 status (committed 2026-06-05, awaiting deploy):** F9-lite — splits the Sprint 11 "Generate Meal Plan" CTA into a 2-step modal: the user picks "Use the recipe library" (default, Sprint 11's flow) 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.** F9-full (local Ollama model pull on the host) remains a future sprint.
|
||||||
|
> - **T7.1** `backend/app/api/llm_plan.py` (NEW, ~280 lines). 1 endpoint (`POST /api/llm/plan` body `{prompt, week_start}`) + 4 helpers (`_ensure_ollama_configured`, `_serialize_library` with a 200-recipe cap, `_ask_llm` mirroring the `llm_matcher._ask_ollama` pattern, `_parse_picks` tolerant of markdown code fences, `_validate_picks` that drops invalid entries). 60s timeout, 422 on empty/oversized prompt, 503 on missing OLLAMA_API_KEY, 400 on duplicate week.
|
||||||
|
> - **T7.2** `backend/app/schemas/__init__.py` — added `LLMPlanRequest` + `LLMPlanResponse` Pydantic models. The router is registered in `main.py:65-66` at the `/api/llm` prefix.
|
||||||
|
> - **T7.3** Frontend: `frontend/src/api/index.ts` adds `llm.plan(data)`. `frontend/src/pages/Dashboard.tsx` adds the prompt modal (radio for library vs. LLM + textarea for the LLM path with 500-char counter) + extracted Sprint 11's body into `generateFromLibrary` + added `generateFromLLM`. New state: `showPromptModal`, `promptMode`, `promptText`, `promptBusy`. Click-outside-to-dismiss is disabled while `promptBusy` is true. The textarea `autoFocus`es when LLM mode is selected.
|
||||||
|
> - **Verification log:** `Review/sprint13-verification.md` (deploy + 3-step browser smoke + 4 API curls + a11y check + 6-risk table). Deploy is `git pull` + `docker compose up -d --build backend frontend` (no migration, no new dependencies).
|
||||||
> - **No new dependencies. No migration. Admin path unchanged.**
|
> - **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.
|
> **Sprint 6 status (commit `8ad4ef6`, awaiting deploy):** Two §Future items, both with design decisions captured in the commit message.
|
||||||
|
|||||||
+22
-1
@@ -302,7 +302,7 @@ Trust the tests. Trust the live runs. Don't trust prose claims that something is
|
|||||||
**Current open proposals:**
|
**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).
|
- `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. **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`.
|
**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. **Sprint 13 (committed 2026-06-05, awaiting user deploy)** ships the F9-lite Ollama Cloud plan synthesis. See Sprint 7 + Sprint 8 + Sprint 9 + Sprint 10 + Sprint 11 + Sprint 12 + Sprint 13 sections below. Full UI-audit handoff at `Review/handoff-ui-audit.md`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -407,6 +407,27 @@ Trust the tests. Trust the live runs. Don't trust prose claims that something is
|
|||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
|
### Sprint 13 — F9-lite (Ollama Cloud plan synthesis) (§Future H10) (user-driven) — COMMITTED 2026-06-05
|
||||||
|
|
||||||
|
**User direction (2026-06-05):** "Proceed." F9-lite reuses the pre-existing `OLLAMA_*` config (`config.py:36-38`) — avoids the local model pull (F9-full would be 4 GB on disk + a separate uvicorn process). Cloud LLM — operator's existing OLLAMA billing applies.
|
||||||
|
|
||||||
|
**Scope (5 boxes):**
|
||||||
|
1. **NEW** `backend/app/api/llm_plan.py` (~280 lines) — 1 endpoint (`POST /api/llm/plan` body `{prompt, week_start}`) + 4 helpers (`_ensure_ollama_configured`, `_serialize_library` with a 200-recipe cap, `_ask_llm` mirroring `llm_matcher._ask_ollama`, `_parse_picks` tolerant of markdown code fences, `_validate_picks` that drops invalid entries). 60s timeout. 422 on empty/oversized prompt. 503 on missing OLLAMA_API_KEY. 400 on duplicate week.
|
||||||
|
2. `backend/app/schemas/__init__.py` — added `LLMPlanRequest` + `LLMPlanResponse`.
|
||||||
|
3. `backend/app/main.py:65-66` — registered `llm_plan_api.router` at the `/api/llm` prefix. No collision with the pre-existing WIP `recipes.py`.
|
||||||
|
4. **Frontend:** `frontend/src/api/index.ts` adds `llm.plan(data)`. `frontend/src/pages/Dashboard.tsx` adds the prompt modal (radio for library vs. LLM + textarea for the LLM path with 500-char counter) + extracted Sprint 11's body into `generateFromLibrary` + added `generateFromLLM`. New state: `showPromptModal`, `promptMode`, `promptText`, `promptBusy`. Click-outside-to-dismiss is disabled while `promptBusy` is true. The textarea `autoFocus`es when LLM mode is selected.
|
||||||
|
5. **LLM tolerance:** 60s timeout, parse-failure (markdown code fences, trailing commentary), or empty response all return 0 picks; the library fill takes over. The user never sees a crash — at worst, `picked_count: 0` and the toast reads "Planned N meals (LLM picked 0, library filled the rest)".
|
||||||
|
|
||||||
|
**Build:** `npm run build` green (tsc 0 errors, vite 0 errors). Bundle: 500.28 → 503.82 kB (+3.5 kB for the modal + the LLM handler). Backend AST clean. One commit: `feat(ui): Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis)`.
|
||||||
|
|
||||||
|
**Deploy:** `git pull` + `docker compose up -d --build backend frontend` (no migration, no new dependencies). Verification: `Review/sprint13-verification.md` (3-step browser smoke + 4 API curls + a11y check + 6-risk table).
|
||||||
|
|
||||||
|
**No regression expected:** Sprint 13 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 (`llm_plan.py`) and registered at a non-colliding `/api/llm` prefix. Sprints 1-12 are untouched. The Sprint 11 library path is unchanged (extracted into `generateFromLibrary`, identical body).
|
||||||
|
|
||||||
|
**§Future backlog status after Sprint 13:** F1 (onboarding) ✓, F8 (Spoonacular) ✓, F9-lite (Ollama Cloud) ✓. Only F9-full (local Ollama model pull) remains — opt-in based on cloud-billing feedback.
|
||||||
|
|
||||||
|
**Path forward to F9-full:** the `_ask_llm` helper is the single seam. F9-full only needs to swap the URL (`https://ollama.com/v1` → `http://localhost:11434`) and model name (`kimi-k2.6:cloud` → local). The endpoint code, prompt, and validation stay unchanged.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## New session: 2026-06-05 (early)
|
## New session: 2026-06-05 (early)
|
||||||
|
|||||||
@@ -695,3 +695,57 @@ User direction 2026-06-05: "Proceed." Selected from the question menu as the sma
|
|||||||
### T6.6 · `Review/sprint12-verification.md` (NEW)
|
### 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.
|
- 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis) — ✅ COMPLETE, awaiting deploy
|
||||||
|
|
||||||
|
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 — operator's existing OLLAMA billing applies.
|
||||||
|
|
||||||
|
**Status (2026-06-05):** ✅ Code complete. `npm run build` green (tsc 0 errors, vite 0 errors). Bundle: 500.28 → 503.82 kB. Backend AST clean. Awaiting user commit + deploy. **No new dependencies, no migration, no pre-existing WIP files touched.**
|
||||||
|
|
||||||
|
### T7.1 · Backend — `backend/app/api/llm_plan.py` (NEW, ~280 lines)
|
||||||
|
|
||||||
|
- 1 endpoint: `POST /api/llm/plan` (public, `require_session`). Body: `{prompt: str 1-500, week_start: date}`.
|
||||||
|
- 4 helpers:
|
||||||
|
- `_ensure_ollama_configured()` — 503 with clear `detail: "OLLAMA_API_KEY not configured; set it in the backend env"`.
|
||||||
|
- `_serialize_library(db, profile_id)` — reads up to 200 recipes for the family, sorted alphabetically. Cap prevents prompt-token overflow on kimi-k2.6.
|
||||||
|
- `_ask_llm(prompt)` — mirrors `llm_matcher._ask_ollama:97-144`. Same call pattern: `POST ${OLLAMA_BASE_URL}/chat/completions`, `Authorization: Bearer ${OLLAMA_API_KEY}`, `model: settings.OLLAMA_MODEL`, `max_tokens: 800, temperature: 0`, strips `<think>` blocks. 60s timeout.
|
||||||
|
- `_parse_picks(raw)` — tolerant JSON parser. Handles markdown code fences (` ```json ... ``` `), trailing commentary, and bare JSON. Returns a list of dicts (validated by the caller).
|
||||||
|
- `_validate_picks(picks, valid_recipe_ids)` — drops invalid entries: missing fields, out-of-range `day_of_week`, unknown `meal_type`, unknown `recipe_id`. Returns a list of `LLMPickedItem`.
|
||||||
|
- Flow:
|
||||||
|
1. Reject if a plan for `week_start` already exists (400 with the existing plan id; matches Sprint 11's `meals.create` 400 path).
|
||||||
|
2. Reject if the recipe library is empty (400 with `detail: "recipe library is empty; import some recipes first"`).
|
||||||
|
3. Build the prompt: "You are planning a 7-day meal plan (Monday through Sunday) for a family. Each day has 3 meals: breakfast, lunch, dinner. Pick up to 21 meals total from the recipe library below. If a slot has no good match for the user's request, OMIT it (do not invent a recipe). Use only recipe_ids from the list. USER REQUEST: <prompt>. RECIPE LIBRARY (<n> recipes): <list>. RETURN FORMAT — valid JSON only, no markdown, no commentary: [{day_of_week, meal_type, recipe_id}, ...]"
|
||||||
|
4. Call `_ask_llm(prompt)`. On timeout / network error / parse failure, return 0 picks; the library fill takes over.
|
||||||
|
5. Validate picks.
|
||||||
|
6. Create the plan (`MealPlan(family_profile_id, week_start_date, status='draft', notes=<prompt[:200]>)`).
|
||||||
|
7. Insert the LLM-picked items.
|
||||||
|
8. Fill the remaining slots from the library (Sprint 6+ pattern, re-implemented inline to avoid a self-HTTP-call). Uses the first non-already-used recipe per slot.
|
||||||
|
9. Return `{plan_id, picked_count, filled_count, failed_count, reasoning: <raw LLM text>}`.
|
||||||
|
|
||||||
|
### T7.2 · Backend — config + schemas + main.py wiring
|
||||||
|
|
||||||
|
- **File:** `backend/app/schemas/__init__.py` — added `LLMPlanRequest` (Pydantic, `prompt: str = Field(min_length=1, max_length=500)`, `week_start: date`) + `LLMPlanResponse` (`{plan_id: str, picked_count: int, filled_count: int, failed_count: int, reasoning: Optional[str]}`).
|
||||||
|
- **File:** `backend/app/main.py:65-66` — registered `llm_plan_api.router` at the `/api/llm` prefix. No collision with the pre-existing WIP `recipes.py` (which is at `/api/recipes`).
|
||||||
|
|
||||||
|
### T7.3 · Frontend — `Dashboard.tsx` modal + LLM handler
|
||||||
|
|
||||||
|
- **File:** `frontend/src/pages/Dashboard.tsx` — added the prompt modal + new state (`showPromptModal`, `promptMode`, `promptText`, `promptBusy`). The Sprint 11 `handleGenerateFirstPlan` body was extracted into two functions:
|
||||||
|
- `generateFromLibrary()` — unchanged Sprint 11 flow (`meals.create` + `meals.fillEmptySlots`).
|
||||||
|
- `generateFromLLM()` — new, calls `mealPlannerApi.llm.plan({prompt, week_start})`. On success, toasts `"Planned N meals (LLM picked K, library filled the rest)"`. On error, uses `showApiError` (Sprint 4 F7) which surfaces the backend's 503 / 422 / 400 detail.
|
||||||
|
- The modal is inline (not a separate component) because it depends on 4 local states + 3 handlers. Click-outside-to-dismiss is disabled while `promptBusy` is true. The "Generate" button label flips to `"Asking LLM…"` (with a spinning Loader2 icon) when LLM mode is selected, or `"Generating…"` when library mode is selected.
|
||||||
|
- The textarea `autoFocus`es when LLM mode is selected. The character counter shows `current / 500` (right-aligned, screen-reader-accessible via the textarea's `maxLength`).
|
||||||
|
- **File:** `frontend/src/api/index.ts` — added `llm.plan(data)` method.
|
||||||
|
|
||||||
|
### T7.4 · Sprint 13 verification gate
|
||||||
|
|
||||||
|
- [x] `npm run build` green (tsc 0 errors, vite 0 errors). Bundle: 500.28 → 503.82 kB (+3.5 kB for the modal + the LLM handler).
|
||||||
|
- [x] Backend AST clean on all 3 changed files (`llm_plan.py`, `schemas/__init__.py`, `main.py`).
|
||||||
|
- [ ] Browser smoke (3 steps) on `http://100.108.208.56:8082/` per `Review/sprint13-verification.md`.
|
||||||
|
- [ ] Manual API smoke (4 curls): happy path + OLLAMA_API_KEY unset (503) + empty prompt (422) + duplicate week (400).
|
||||||
|
- [ ] No regression in Sprints 1-12.
|
||||||
|
|
||||||
|
### T7.5 · `Review/sprint13-verification.md` (NEW)
|
||||||
|
|
||||||
|
- Deploy + 3-step browser smoke + 4 API curls + a11y check + 6-risk table + future work section. Source of truth for the operator deploy + smoke flow.
|
||||||
|
|||||||
Reference in New Issue
Block a user