Files
Meal-Planner/Review/sprint13-verification.md
admin 8cb4d4198c
CI / backend (pytest + alembic) (push) Has been cancelled
CI / frontend (build) (push) Has been cancelled
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.
2026-06-05 16:59:20 -07:00

8.4 KiB

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

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)

# 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.