Files
Meal-Planner/docs/ORIENTATION.md
T
adminandClaude Opus 4.7 c953111395 docs: refresh ORIENTATION + HANDOFF after r1+r2 recovery and r3-0
Updates ORIENTATION.md and HANDOFF.md to reflect actual state as of
commit 8e89f79: phases 1/2/3/7 complete (verified, not just claimed),
phases 4/5/6/8/9/10/11 not started. Documents the auth model, the
bootstrap login hatch, the SWIFTLY_BEARER_TOKEN expiration handling,
the Swiftly JSON API ingestion path that replaced Playwright, the
canonicalized API paths, and the verification commands to reproduce
the 31/31 pytest gate locally and in CI.

HANDOFF.md is intended for fresh agents and points at .agent/
phase-summaries for the per-phase write-ups. Surfaces 10 caveats and
traps the next agent will hit if they skim, and recommends Phase 9
(meal-planner generation algorithm) as the next move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:41:19 -07:00

7.4 KiB
Raw Blame History

Meal Planner — Orientation

First stop for any agent resuming work. Read this, then docs/HANDOFF.md for the deep dive.


What this project is

Self-hosted meal planning for one family of 4. Pulls weekly grocery prices from Lucky California (San Pablo, store 757) via the Swiftly JSON API, generates a 7-day meal plan, emails per-member approval links, builds a shopping list grouped by aisle. Replaces meal-kit subscriptions (Blue Apron / Sunbasket / etc.) which marked up ingredients ~3× and produced repetitive meals.

Constraint that drives the design: 3 of 4 members do not like mushrooms; family is calorie/budget conscious; no allergies. Must work for non-technical wife + 2 kids; technical owner self-hosts.


Architecture (current)

email ─► SendGrid (R3-C, not yet wired) ──┐
web   ─► nginx :80/:443 ─► React/Vite ────┼─► FastAPI ──► PostgreSQL 15
                                          │      │
                                          │      └─► Swiftly JSON API
                                          │         (prod.swiftlyapi.net)
                                          │
                                          └─► /api/admin/scrape (BackgroundTasks)
  • backend (FastAPI 0.109, SQLAlchemy 2.0, Alembic) — internal only, expose: 8000
  • frontend (React 18 + TS + Vite + Tailwind) — internal only via nginx
  • db (Postgres 15-alpine) — internal only
  • nginx — sole external entry, ports 80/443

Phase status (2026-05-05)

# Phase Status
1 Infra (Docker, FastAPI, React, nginx, Postgres) Complete
2 DB & models (Alembic, Pydantic schemas, API endpoints) Complete (real, verified)
3 Lucky California ingestion (Swiftly JSON API) Complete — 17 categories, ~10k products live
4 Recipe engine (CRUD, search, tagging, never-suggest filter) Not started
5 Meal planner orchestration (generate → email → vote → finalize) Not started
6 SendGrid email integration (proposal/reminder/confirmation) Stub only — app/services/email.py::SendGridEmailBackend raises NotImplementedError
7 Web UI core (Dashboard / Meal Detail / Pantry / Shopping List) Complete (no auth UI yet)
8 Web UI feedback portal Not started
9 Meal-planner generation algorithm Not started — the core of the project
10 Image strategy (scraped + AI fallback) Not started
11 Polish (variety analysis, budget tracking, APScheduler) Not started

Verification gate (R1+R2 + R3-0): 31/31 pytest green; alembic upgrade→downgrade→upgrade clean; frontend npm run build clean; live scrape persists 9,960 grocery_item rows in 36 s; email approval round-trip (approve/deny/single-use) verified end-to-end.


Auth model (R1-B+D, locked in)

  • Bearer token ADMIN_TOKEN for every /api/admin/* route.
  • Signed-cookie session (itsdangerous, key=SECRET_KEY) for all NON-GET routes on profile/pantry/recipes/meals/shopping-list. GET reads stay open inside the trusted network.
  • /api/auth/login accepts {password} matching SESSION_PASSWORD. Sets the cookie.
  • /api/meals/vote/{item_id}?token=… keeps its per-voter token flow; not session-gated.
  • Bootstrap hatch: when no family_profile row exists, login signs the literal string "bootstrap" instead of a UUID. First-run convenience only — replace with a real setup gate before any non-trusted exposure.

Database schema highlights

Core tables: family_profile, family_member, recipe, ingredient, meal_plan, meal_plan_item, meal_plan_vote, approval_token, home_pantry, feedback, grocery_item, scrape_log, email_log.

Conventions: UUID PKs everywhere, TIMESTAMPTZ, Postgres ENUMs (with values_callable=lambda obj: [e.value for e in obj] on every SQLEnum — name-mode silently breaks otherwise), ISO day-of-week (1=Mon).

Key relationships: family_profilefamily_member; family_membermeal_plan_vote (per-voter); recipe.ingredients JSONB (no recipe-ingredient join table); grocery_item.(source, external_id) is the upsert key for scrape ingestion.

Migrations applied: 0001 initial, 0002 seed (idempotent via ON CONFLICT DO NOTHING), 0003 grocery_item.description, 0004 family_profile.calorie_target, 0005 grocery_item.external_id + source + composite index.

Full schema: docs/database-schema.md.


Environment variables (verified)

# Database
POSTGRES_PASSWORD=...
DATABASE_URL=postgresql://mealplanner:${POSTGRES_PASSWORD}@db:5432/mealplanner

# Auth
SECRET_KEY=...                    # signs session cookies + approval tokens
ADMIN_TOKEN=...                   # bearer for /api/admin/*
SESSION_PASSWORD=...               # family-shared password for /api/auth/login

# Email
EMAIL_BACKEND=console              # 'console' (default) or 'sendgrid'
SENDGRID_API_KEY=...              # only when EMAIL_BACKEND=sendgrid (R3-C)

# Lucky / Swiftly
LUCKY_STORE_ID=757                 # Lucky California — San Pablo
SWIFTLY_API_BASE=https://prod.swiftlyapi.net
SWIFTLY_CATEGORIES_URL=https://luckysupermarkets.com/categories
SWIFTLY_BEARER_TOKEN=...           # Firebase anon JWT, expires hourly

# Other
LUCKY_CA_URL=https://luckysupermarkets.com
AI_IMAGE_ENABLED=false
LOG_LEVEL=INFO

.env.example carries a literal expiring bearer token — rotate before any real run. On expiry, the next scrape's ScrapeLog.error_message reads SWIFTLY_BEARER_TOKEN expired — request a fresh token from the user (capture from luckysupermarkets.com network tab on a /search/api/v1 request). Fix: capture a fresh Authorization: Bearer … from devtools, update env, restart backend, re-trigger.


Verification commands

# Full local stack
docker compose --env-file .env.test up -d db backend
docker compose --env-file .env.test exec backend alembic upgrade head
docker compose --env-file .env.test exec -e TEST_DATABASE_URL=postgresql://mealplanner:${POSTGRES_PASSWORD}@db:5432/mealplanner backend pytest -q tests/

# Frontend
cd frontend && npm ci && npm run build

# CI: .github/workflows/ci.yml runs both jobs on push/PR.

A .env.test template lives in the repo root (gitignored) for local stack runs. Pytest uses TEST_DATABASE_URL; alembic uses DATABASE_URL.


Conventions

  • Python: Black + isort. TypeScript: Prettier + ESLint.
  • Conventional commits (feat:, fix:, docs:, refactor:, test:).
  • API paths: no trailing slash, no /list//planned suffixes.
  • Tests: pytest; requires_postgres marker auto-skips locally without TEST_DATABASE_URL.
  • Migrations: Alembic only. Never Base.metadata.create_all() at runtime.
  • Background work: FastAPI BackgroundTasks (current). APScheduler with --workers 1 planned for Phase 11.

Where to look

  • docs/HANDOFF.md — comprehensive handoff for fresh agents (start here for non-trivial work).
  • docs/SPEC.md — product spec.
  • docs/ARCHITECTURE.md — system design.
  • docs/database-schema.md — full DDL reference.
  • docs/implementation-plan.md — original phased plan.
  • docs/RUNNING.md — local dev workflow.
  • .agent/plan.md, .agent/context.md, .agent/phase-summaries/ — recovery decisions and per-phase summaries from the R1+R2+R3-0 work.
  • Review/reviewconcensus.md — the adversarial review that drove the recovery.

Last updated: 2026-05-05 — after R1+R2 stabilization + R3-0 Swiftly ingestion (commit 8e89f79).