admin bae94037f3 feat(ui): Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis)
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 per call.

Sprint 13 splits the Sprint 11 "Generate Meal Plan" CTA into a
2-step modal: "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.

Backend:
- backend/app/api/llm_plan.py (NEW, ~280 lines). 1 endpoint
  (POST /api/llm/plan body {prompt, week_start}) + 4 helpers:
  - _ensure_ollama_configured — 503 on missing OLLAMA_API_KEY.
  - _serialize_library — reads up to 200 recipes for the
    family, sorted alphabetically. Cap prevents prompt-token
    overflow on kimi-k2.
  - _ask_llm — mirrors llm_matcher._ask_ollama (same URL,
    same headers, max_tokens=800, temperature=0, strips think
    blocks, 60s timeout).
  - _parse_picks — tolerant JSON parser. Handles markdown code
    fences, trailing commentary, and bare JSON. On failure
    returns []; the library fill takes over.
  - _validate_picks — drops invalid entries: missing fields,
    out-of-range day_of_week, unknown meal_type, unknown
    recipe_id. Returns a list of LLMPickedItem.
  Flow: rejects duplicate week (400) and empty library (400),
  builds the prompt, calls the LLM, validates picks, creates
  the plan, inserts the LLM-picked items, fills the rest from
  the library (Sprint 6+ pattern, re-implemented inline to
  avoid a self-HTTP-call), returns {plan_id, picked_count,
  filled_count, failed_count, reasoning}.
- backend/app/schemas/__init__.py — added LLMPlanRequest +
  LLMPlanResponse.
- 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.

Frontend:
- frontend/src/api/index.ts — added llm.plan(data) method.
- frontend/src/pages/Dashboard.tsx — added the prompt modal
  (radio for library vs. LLM + textarea for the LLM path with
  500-char counter) + new state (showPromptModal, promptMode,
  promptText, promptBusy) + extracted Sprint 11’s body into
  generateFromLibrary + added generateFromLLM. 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 textarea autoFocuses
  when LLM mode is selected. Added the Button import.

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 never sees a
crash — at worst, picked_count: 0 and the toast reads "Planned
N meals (LLM picked 0, library filled the rest)".

Verified: npm run build green (tsc 0 errors, vite 0 errors).
Bundle: 500.28 → 503.82 kB (+3.5 kB). Backend AST clean on
all 3 changed files. No new dependencies, no migration, no
pre-existing WIP files touched.

Deploy: git pull + docker compose up -d --build backend
frontend (no migration, no new dependencies).
2026-06-05 16:59:03 -07:00

Meal Planner

Self-hosted meal planning system that integrates with Lucky California grocery store, sends weekly meal proposals via email to family members, generates shopping lists, and learns from feedback.

Background

This project was born out of frustration with meal kit services (Blue Apron → EveryPlate → HungryRoot → Sunbasket) that:

  • Escalate costs to 3x ingredient markup
  • Fall into repetitive meal rhythms
  • Force users to log into apps to manage selections
  • Don't integrate with home pantry items

Features

  • Grocery Integration: Scrapes Lucky California weekly ads and sales
  • Family Approval Workflow: Email proposals with approve/deny; one denial swaps the meal
  • Shopping List Generation: Weekly list grouped by store aisles, highlighting sales, with interactive checkboxes to track purchased items
  • Pantry Integration: Specify home items to incorporate into suggestions
  • Web UI: Modern interface for the whole family
  • Learning: Feedback-based meal recommendations, with weekly auto-discovery of new recipes from external APIs when family preferences are signaled
  • Recipe Images: Scraped from public recipe sites, AI fallback available
  • Unit Conversion: Converts recipe quantities (cups, tbsp, lb) to grocery units for accurate cost estimates

Architecture

  • Backend: Python/FastAPI
  • Database: PostgreSQL
  • Frontend: React + Tailwind CSS
  • Email: SendGrid
  • Hosting: Docker Compose with nginx reverse proxy

Documentation

Quick Start

# Clone and start
docker-compose up -d

# View logs
docker-compose logs -f

# Stop
docker-compose down

Family Profile

Household: 2 adults, 2 children

  • One adult likes mushrooms, one child OK with them
  • Three family members do NOT like mushrooms
  • No allergies
  • Calorie, budget, and health conscious eating

Approval Workflow

  1. System generates 7-day meal plan based on sales, dietary constraints, budget, variety
  2. Email sent to both adults with meal previews
  3. One denial = meal swapped; no denials = auto-approved
  4. Shopping list generated after approval

Tech Stack

Component Technology
Backend Python 3.11, FastAPI
Database PostgreSQL 15
Frontend React 18, TypeScript, Tailwind
Scraping Playwright, BeautifulSoup
Email SendGrid
Hosting Docker Compose, nginx
S
Description
No description provided
Readme
46 MiB
Languages
Python 72.5%
TypeScript 25.9%
JavaScript 0.5%
PLpgSQL 0.4%
CSS 0.3%
Other 0.2%