Files

177 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ───────────────────────────┐
web ─► nginx :80/:443 ─► React/Vite ───────┼─► FastAPI ──► PostgreSQL 15
│ │
│ └─► Swiftly JSON API
│ (prod.swiftlyapi.net)
└─► /api/admin/scrape (BackgroundTasks)
scheduler ─► APScheduler (America/Los_Angeles)
Fri 02:00 scrape → 05:00 generate → 06:00 email
Fri 16:00 reminder → 17:00 deadline → 18:00 finalize+shopping-list
(same backend image; command: python -m app.scheduler)
```
- **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-08)
| # | 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) | **Thin slice complete** — recipe + ingredient CRUD, ingredient↔grocery match layer (rapidfuzz, manual override), NeverSuggest CRUD, 30-recipe seed. Ingestion source decision deferred (see spec). |
| 5 | Meal planner orchestration (generate → email → vote → finalize) | **Complete** — Friday Pacific cadence; APScheduler container; `weekly_run` state table; idempotent steps; stale-data fallback; configurable deadline policy. |
| 6 | SendGrid email integration (proposal/reminder/confirmation) | **Complete**`SendGridEmailBackend` live; `step_reminder` (Fri 16:00 PT) nudges non-voters; all templates HTML-safe. |
| 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 | **Complete** — POST /api/admin/meal-plans/generate produces 3-dinner plans against seeded recipes + matched grocery prices. Filter (6 hard constraints), score (5 signals), top-K=20 set enumeration with diversity penalty. |
| 10 | Image strategy (scraped + AI fallback) | Not started |
| 11 | Polish (variety analysis, budget tracking, APScheduler) | Not started |
Verification gate (current, 2026-05-08): **123/123 pytest green**; alembic upgrade→downgrade→upgrade clean (00010009); frontend `npm run build` clean; live scrape persists **9,980 grocery_item rows in 44 s** with auto-minted JWT (no env var); 29,779 ingredient_grocery_match rows produced; email approval round-trip (approve/deny/single-use) verified end-to-end; scheduler container starts with all **6** Friday jobs registered.
---
## 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_profile``family_member`; `family_member``meal_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, 0006 thin Phase 4 (ingredient.aliases, recipe.calories_per_serving, ingredient_grocery_match), 0007 seed canonical ingredients (50) + 30 starter recipes, 0008 Phase 5 (`weekly_run` table + `family_profile.pending_approval_policy`), 0009 Phase 6 (`weekly_run.reminded_at TIMESTAMPTZ NULL`).
Full schema: `docs/database-schema.md`.
---
## Environment variables (verified)
```bash
# 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=... # required when EMAIL_BACKEND=sendgrid
SENDGRID_FROM_EMAIL=peter@research.bike # sender address (default)
SENDGRID_REPLY_TO=peter@research.bike # reply-to address (default)
# Phase 5 orchestration
ADMIN_EMAIL=... # alert destination; empty = alerts dropped silently
APP_BASE_URL=http://localhost # base URL for vote links in emails
# Lucky / Swiftly
LUCKY_STORE_ID=757 # Lucky California — San Pablo
SWIFTLY_API_BASE=https://prod.swiftlyapi.net
SWIFTLY_CATEGORIES_URL=https://luckysupermarkets.com/categories
# (no bearer-token env var — minted on demand by app/services/swiftly_auth.py)
# Other
LUCKY_CA_URL=https://luckysupermarkets.com
AI_IMAGE_ENABLED=false
LOG_LEVEL=INFO
```
The Swiftly bearer JWT is auto-minted at request time via Firebase REST anon-signUp (`backend/app/services/swiftly_auth.py`, spec `docs/specs/2026-05-06-swiftly-token-auto-mint.md`). Process-local cache; ~99% cache-hit rate in steady state. A mint failure surfaces as `SwiftlyAuthMintError` and lands verbatim in `ScrapeLog.error_message`.
---
## Verification commands
```bash
# Full local stack (include scheduler for Phase 5)
docker compose --env-file .env.test up -d db backend scheduler
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/ # → 123 passed
# Verify scheduler
docker compose --env-file .env.test logs scheduler | grep Registered
# Expected: 6 lines — scrape/generate/email/reminder/deadline/finalize
# Manual step trigger (useful for testing without waiting for Friday)
curl -X POST http://localhost:8000/api/admin/orchestrate/scrape \
-H "Authorization: Bearer ${ADMIN_TOKEN}"
# 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` for on-demand scrape. APScheduler `BlockingScheduler` in the `scheduler` container for the weekly cycle (6 Friday Pacific jobs — Phases 5+6 complete).
---
## 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.
- `docs/specs/2026-05-05-meal-planner-algorithm-design.md` — Phase 9 + thin Phase 4 design.
- `docs/specs/2026-05-06-swiftly-token-auto-mint.md` — Swiftly JWT auto-mint design (Implemented 2026-05-06).
- `docs/superpowers/plans/2026-05-07-phase5-orchestration.md` — Phase 5 implementation plan (executed 2026-05-07).
- `docs/superpowers/specs/2026-05-07-phase6-sendgrid-design.md` — Phase 6 design spec.
- `docs/superpowers/plans/2026-05-07-phase6-sendgrid.md` — Phase 6 implementation plan (executed 2026-05-08).
- `.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-08 — Phase 6 SendGrid shipped; 123/123 pytest green; scheduler container live with 6 Friday Pacific jobs. Next pickup: Frontend login UI.