Files
Meal-Planner/docs/ORIENTATION.md
T
adminandClaude Opus 4.7 dc01a0fefb docs: refresh HANDOFF + ORIENTATION for fresh-agent pickup
Bring docs in line with the post-AM-6 state so a fresh agent can pick
up cleanly without first reconciling stale numbers:

- pytest count: 88/59/31 → 92 across all references
- live scrape: 9,960 rows / 36s → 9,980 rows / 44s (latest run, 2026-05-06)
- migrations applied: 0001-0005 → 0001-0007 in both docs
- verification gate updated with auto-minted JWT detail and the 29,779
  ingredient_grocery_match rows produced post-scrape
- "What is real" / Backend: added swiftly_auth.py bullet describing
  get_token() / mint_anonymous_token() / cache semantics + the
  10,928-item live verification
- file map: added services/swiftly_auth.py, services/matcher.py,
  services/planner/, mentioned scraper_service runs matcher post-scrape
- file map: alembic versions 0001 → 0007, tests/ count = 92, marked
  config.py as no-longer-carrying SWIFTLY_BEARER_TOKEN
- spec map line for swiftly-token-auto-mint: "next-up" → "Implemented"

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

155 lines
8.2 KiB
Markdown
Raw 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 (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-06)
| # | 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) | 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 | **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-06): **92/92 pytest green**; alembic upgrade→downgrade→upgrade clean; 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.
---
## 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.
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=... # 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
# (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
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/ # → 92 passed
# 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.
- `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).
- `.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-06 — Swiftly auto-mint shipped (AM-1..AM-6); 92/92 pytest green; live scrape verified end-to-end with no `SWIFTLY_BEARER_TOKEN` env var. Next pickup: Phase 5 weekly orchestration.