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

153 lines
7.4 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-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_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.
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
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
```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/
# 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`).