Public Access
docs: refresh HANDOFF + ORIENTATION for Phase 5 completion
This commit is contained in:
+40
-24
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
You are taking over a project in mid-flight. Read `docs/ORIENTATION.md` first for the high-level. This file is the deep dive: what's real, what's stubbed, where the bodies are buried, and what to do next.
|
You are taking over a project in mid-flight. Read `docs/ORIENTATION.md` first for the high-level. This file is the deep dive: what's real, what's stubbed, where the bodies are buried, and what to do next.
|
||||||
|
|
||||||
Date of handoff: 2026-05-06. Last commit before handoff: Swiftly token auto-mint shipped (AM-1..AM-6).
|
Date of handoff: 2026-05-07. Last commit before handoff: Phase 5 weekly orchestration shipped.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## TL;DR
|
## TL;DR
|
||||||
|
|
||||||
The project completed a **recovery pass** (R1+R2+R3-0) from 2026-05-04 to 2026-05-05 (12 defects from a prior agent fixed), then shipped **thin Phase 4** (recipe engine + ingredient↔grocery match layer + 30-recipe seed) and **Phase 9** (meal-planner generation algorithm: filter → score → top-K=20 set enumeration with diversity penalty → persist MealPlan + items via `POST /api/admin/meal-plans/generate`).
|
The project completed a **recovery pass** (R1+R2+R3-0) from 2026-05-04 to 2026-05-05 (12 defects from a prior agent fixed), then shipped **thin Phase 4** (recipe engine + ingredient↔grocery match layer + 30-recipe seed), **Phase 9** (meal-planner generation algorithm), and **Phase 5** (weekly orchestration cycle — scrape → generate → email → deadline → finalize on a Friday Pacific cadence, driven by a dedicated APScheduler container).
|
||||||
|
|
||||||
The project's reason to exist is now real and verified end-to-end. 92/92 pytest tests pass.
|
The project's reason to exist is now real and verified end-to-end. 115/115 pytest tests pass.
|
||||||
|
|
||||||
**Operator toil eliminated.** Swiftly bearer JWTs are now auto-minted via Firebase REST anon-signUp (`backend/app/services/swiftly_auth.py`); the `SWIFTLY_BEARER_TOKEN` env var is gone. Cache hit ratio in steady state is ~99% (one mint per ~hour). See `docs/specs/2026-05-06-swiftly-token-auto-mint.md` (status: Implemented).
|
**Operator toil eliminated.** Swiftly bearer JWTs are now auto-minted via Firebase REST anon-signUp (`backend/app/services/swiftly_auth.py`); the `SWIFTLY_BEARER_TOKEN` env var is gone. Cache hit ratio in steady state is ~99% (one mint per ~hour). See `docs/specs/2026-05-06-swiftly-token-auto-mint.md` (status: Implemented).
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ The project's reason to exist is now real and verified end-to-end. 92/92 pytest
|
|||||||
## What is real (verified)
|
## What is real (verified)
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
- `backend/app/main.py` imports cleanly with 22 routes wired.
|
- `backend/app/main.py` imports cleanly with 25 routes wired (22 prior + 3 orchestrate endpoints).
|
||||||
- Health: `GET /health`, `GET /health/db`.
|
- Health: `GET /health`, `GET /health/db`.
|
||||||
- Auth: bearer `ADMIN_TOKEN` for admin; signed-cookie session via `app.security.require_session` for mutations on family-facing routes; `/api/auth/login` with shared `SESSION_PASSWORD`.
|
- Auth: bearer `ADMIN_TOKEN` for admin; signed-cookie session via `app.security.require_session` for mutations on family-facing routes; `/api/auth/login` with shared `SESSION_PASSWORD`.
|
||||||
- Routers (`backend/app/api/`): `profile.py`, `recipes.py`, `meals.py`, `pantry.py`, `shopping_list.py`, `admin.py`, `auth.py`. CRUD shapes are stubbed/partial — they validate request bodies and persist correctly but business logic is thin.
|
- Routers (`backend/app/api/`): `profile.py`, `recipes.py`, `meals.py`, `pantry.py`, `shopping_list.py`, `admin.py`, `auth.py`. CRUD shapes are stubbed/partial — they validate request bodies and persist correctly but business logic is thin.
|
||||||
@@ -30,10 +30,12 @@ The project's reason to exist is now real and verified end-to-end. 92/92 pytest
|
|||||||
- 401 from Swiftly is now rare (we always send a freshly minted JWT). When it does happen, `SwiftlyAuthError` carries a message pointing at the auto-mint spec; mint failures upstream surface as `SwiftlyAuthMintError`. Either lands verbatim in `ScrapeLog.error_message` via the bg runner.
|
- 401 from Swiftly is now rare (we always send a freshly minted JWT). When it does happen, `SwiftlyAuthError` carries a message pointing at the auto-mint spec; mint failures upstream surface as `SwiftlyAuthMintError`. Either lands verbatim in `ScrapeLog.error_message` via the bg runner.
|
||||||
- Thin Phase 4: ingredient + recipe CRUD endpoints with admin gating; NeverSuggest CRUD (covers both ingredient blocklist and recipe blocklist via the existing schema); ingredient↔grocery_item match layer (rapidfuzz top-3 ranking with confidence threshold 0.75, manual override via /api/admin/ingredients/{id}/matches and /api/admin/ingredient-matches/{id}); 50 canonical ingredients seeded with aliases enriching pre-existing rows from migration 0002; 30 starter recipes spanning chicken/beef/turkey/pork/fish/vegetarian with varied cuisines, all under 45 min for 28/30. Match job runs after each successful scrape; matcher failures don't flip the scrape to FAILED.
|
- Thin Phase 4: ingredient + recipe CRUD endpoints with admin gating; NeverSuggest CRUD (covers both ingredient blocklist and recipe blocklist via the existing schema); ingredient↔grocery_item match layer (rapidfuzz top-3 ranking with confidence threshold 0.75, manual override via /api/admin/ingredients/{id}/matches and /api/admin/ingredient-matches/{id}); 50 canonical ingredients seeded with aliases enriching pre-existing rows from migration 0002; 30 starter recipes spanning chicken/beef/turkey/pork/fish/vegetarian with varied cuisines, all under 45 min for 28/30. Match job runs after each successful scrape; matcher failures don't flip the scrape to FAILED.
|
||||||
- Phase 9: meal-plan generation. POST /api/admin/meal-plans/generate runs the full filter→score→set-select pipeline against seeded recipes and produces a persisted MealPlan with up to 3 MealPlanItem dinners. Regenerate endpoint accepts relaxed constraint overrides (`relax_time_max_minutes`, `relax_calorie_pct`, `relax_max_meal_cost`) and deletes any prior plan for the same `(family, week_start_date)` before re-running. Per-meal cost matched against ingredient_grocery_match using the top-confidence grocery row.
|
- Phase 9: meal-plan generation. POST /api/admin/meal-plans/generate runs the full filter→score→set-select pipeline against seeded recipes and produces a persisted MealPlan with up to 3 MealPlanItem dinners. Regenerate endpoint accepts relaxed constraint overrides (`relax_time_max_minutes`, `relax_calorie_pct`, `relax_max_meal_cost`) and deletes any prior plan for the same `(family, week_start_date)` before re-running. Per-meal cost matched against ingredient_grocery_match using the top-confidence grocery row.
|
||||||
|
- **Phase 5 orchestration** (`backend/app/services/orchestrator/`): five idempotent step functions (`step_scrape`, `step_generate`, `step_email`, `step_deadline`, `step_finalize`) chained by `runner.run_step()` / `run_week()`. State tracked in `weekly_run` table (one row per family per week; each step sets its timestamp column on completion — re-firing is a no-op). Scrape failure retries once then proceeds with stale data + admin alert banner in email. Deadline resolves PENDING items per `family_profile.pending_approval_policy` (default `"approve"`). Shopping-list email sent at finalize. Admin override endpoints: `POST /api/admin/orchestrate/{step}`, `POST /api/admin/orchestrate/run-week`, `GET /api/admin/orchestrate/status`. New env vars: `ADMIN_EMAIL` (alert destination, empty = silent), `APP_BASE_URL` (vote link base, default `http://localhost`).
|
||||||
|
- **Scheduler container** (`backend/app/scheduler/__main__.py`): `BlockingScheduler(timezone="America/Los_Angeles")` with five `CronTrigger` jobs — Fri 02:00 scrape, 05:00 generate, 06:00 email, 17:00 deadline, 18:00 finalize. Runs as a separate Docker service (`scheduler:`) using the same backend image with `command: python -m app.scheduler`. Started via `docker compose up -d scheduler`.
|
||||||
- **Swiftly auto-mint** (`backend/app/services/swiftly_auth.py`): `get_token()` returns a Firebase anon-signUp JWT, cached in process memory until exp − 5min. `mint_anonymous_token()` fetches `firebaseApiKey` from `luckysupermarkets.com/config.json`, posts to `identitytoolkit.googleapis.com/v1/accounts:signUp` with `Origin`/`Referer` set to `https://luckysupermarkets.com`, validates `iss`/`exp` on the returned JWT. `LuckyCaliforniaScraper.fetch_category()` calls it; mint failures surface as `SwiftlyAuthMintError`. Live-verified 2026-05-06: 10,928 items scraped in 44s, 29,779 ingredient_grocery_match rows produced.
|
- **Swiftly auto-mint** (`backend/app/services/swiftly_auth.py`): `get_token()` returns a Firebase anon-signUp JWT, cached in process memory until exp − 5min. `mint_anonymous_token()` fetches `firebaseApiKey` from `luckysupermarkets.com/config.json`, posts to `identitytoolkit.googleapis.com/v1/accounts:signUp` with `Origin`/`Referer` set to `https://luckysupermarkets.com`, validates `iss`/`exp` on the returned JWT. `LuckyCaliforniaScraper.fetch_category()` calls it; mint failures surface as `SwiftlyAuthMintError`. Live-verified 2026-05-06: 10,928 items scraped in 44s, 29,779 ingredient_grocery_match rows produced.
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
- Postgres 15. Seven migrations head-at: `0001_initial_migration`, `0002_seed_data`, `0003_grocery_item_description`, `0004_family_profile_calorie_target`, `0005_grocery_item_external_id`, `0006_thin_phase4` (ingredient.aliases, recipe.calories_per_serving, ingredient_grocery_match), `0007_seed_canonical_ingredients` (50 ingredients + 30 recipes).
|
- Postgres 15. Eight migrations head-at: `0001_initial_migration`, `0002_seed_data`, `0003_grocery_item_description`, `0004_family_profile_calorie_target`, `0005_grocery_item_external_id`, `0006_thin_phase4` (ingredient.aliases, recipe.calories_per_serving, ingredient_grocery_match), `0007_seed_canonical_ingredients` (50 ingredients + 30 recipes), `0008_phase5_orchestration` (`weekly_run` table + `family_profile.pending_approval_policy`).
|
||||||
- `0001` downgrade now does a `DO $$ … DROP TABLE … DROP TYPE … END $$;` block that preserves `alembic_version`. Round-trip works.
|
- `0001` downgrade now does a `DO $$ … DROP TABLE … DROP TYPE … END $$;` block that preserves `alembic_version`. Round-trip works.
|
||||||
- `0002` seed is idempotent (`ON CONFLICT (name_lower) DO NOTHING`).
|
- `0002` seed is idempotent (`ON CONFLICT (name_lower) DO NOTHING`).
|
||||||
- Every `SQLEnum(...)` column carries `values_callable=lambda obj: [e.value for e in obj]` — without this, name-mode breaks reads against the lowercase Postgres enum values.
|
- Every `SQLEnum(...)` column carries `values_callable=lambda obj: [e.value for e in obj]` — without this, name-mode breaks reads against the lowercase Postgres enum values.
|
||||||
@@ -46,7 +48,7 @@ The project's reason to exist is now real and verified end-to-end. 92/92 pytest
|
|||||||
- **No login UI yet.** No feedback page. No tests.
|
- **No login UI yet.** No feedback page. No tests.
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
- **92 tests** under `backend/tests/` — 88 from Phase 4 + Phase 9 work, plus 4 new in `test_swiftly_auth.py` (mint, cache hit, near-expiry re-mint, Firebase non-200 → SwiftlyAuthMintError). All green when `TEST_DATABASE_URL` is set; `requires_postgres` marker auto-skips locally without it.
|
- **115 tests** under `backend/tests/` — 92 prior + 23 new in `test_orchestrator.py` (all 5 step functions, runner, admin endpoints, fixtures). All green when `TEST_DATABASE_URL` is set; `requires_postgres` marker auto-skips locally without it.
|
||||||
- Live spike scripts in `scripts/`: `spike_lucky_scrape.py` (R2-A archived path), `send_test_approval.py` (email round-trip prover, supports `--simulate-click {approve|deny}`), `spike_swiftly_ingest.py` (live Swiftly ingestion prover; requires `--confirm-live`; uses auto-minted JWT — no env var needed).
|
- Live spike scripts in `scripts/`: `spike_lucky_scrape.py` (R2-A archived path), `send_test_approval.py` (email round-trip prover, supports `--simulate-click {approve|deny}`), `spike_swiftly_ingest.py` (live Swiftly ingestion prover; requires `--confirm-live`; uses auto-minted JWT — no env var needed).
|
||||||
|
|
||||||
### CI
|
### CI
|
||||||
@@ -61,9 +63,12 @@ The project's reason to exist is now real and verified end-to-end. 92/92 pytest
|
|||||||
- Phase 4 ingestion source (Spoonacular / TheMealDB / manual-only) — pros/cons table in `docs/specs/2026-05-05-meal-planner-algorithm-design.md` §6; decision deferred until Phase 9 lands.
|
- Phase 4 ingestion source (Spoonacular / TheMealDB / manual-only) — pros/cons table in `docs/specs/2026-05-05-meal-planner-algorithm-design.md` §6; decision deferred until Phase 9 lands.
|
||||||
- Still thin: full-text recipe search, advanced tag filtering, bulk import endpoints — punted until the engine demonstrates which surfaces it actually needs.
|
- Still thin: full-text recipe search, advanced tag filtering, bulk import endpoints — punted until the engine demonstrates which surfaces it actually needs.
|
||||||
|
|
||||||
### Phase 5 — Meal-planner orchestration (not started)
|
### Phase 5 — Meal-planner orchestration (complete)
|
||||||
- The weekly cycle: scrape Sunday → generate Monday → email Monday-evening → deadline Thursday → finalize Friday.
|
- Weekly cycle ships every Friday (Pacific): 02:00 scrape, 05:00 generate, 06:00 email, 17:00 deadline, 18:00 finalize + shopping list.
|
||||||
- All the parts exist (scrape works; email works; vote works; approval rule works) but nothing chains them.
|
- `weekly_run` table is the state machine; each step is idempotent on its timestamp column.
|
||||||
|
- Pending approval policy is configurable per family (`family_profile.pending_approval_policy`); default `"approve"` (silence = ok).
|
||||||
|
- Scrape failure retries once then falls back to stale data with a visible banner in the proposal email.
|
||||||
|
- Minor follow-ups: (a) HTML-escape recipe/ingredient names in email templates; (b) simplify dead `getattr` default in `step_deadline`.
|
||||||
|
|
||||||
### Phase 6 — SendGrid (stub)
|
### Phase 6 — SendGrid (stub)
|
||||||
- `SendGridEmailBackend.send` raises `NotImplementedError("Wire SendGrid in R3-C")`. Templates: meal proposal, reminder (T-24h), confirmation, denial.
|
- `SendGridEmailBackend.send` raises `NotImplementedError("Wire SendGrid in R3-C")`. Templates: meal proposal, reminder (T-24h), confirmation, denial.
|
||||||
@@ -77,10 +82,10 @@ The project's reason to exist is now real and verified end-to-end. 92/92 pytest
|
|||||||
- Recipe images: scrape from source sites first, AI fallback (`AI_IMAGE_ENABLED=false` flag exists, no implementation).
|
- Recipe images: scrape from source sites first, AI fallback (`AI_IMAGE_ENABLED=false` flag exists, no implementation).
|
||||||
|
|
||||||
### Phase 11 — Polish (not started)
|
### Phase 11 — Polish (not started)
|
||||||
- APScheduler container with `--workers 1` to run weekly cadence.
|
|
||||||
- Variety analysis dashboard.
|
- Variety analysis dashboard.
|
||||||
- Budget tracking.
|
- Budget tracking.
|
||||||
- WhatsApp via Twilio (out of MVP scope).
|
- WhatsApp via Twilio (out of MVP scope).
|
||||||
|
- Note: APScheduler container is now live (Phase 5). `--workers 1` constraint applies to the scheduler service only.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -119,14 +124,17 @@ The project's reason to exist is now real and verified end-to-end. 92/92 pytest
|
|||||||
Same as `docs/ORIENTATION.md`:
|
Same as `docs/ORIENTATION.md`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Stack up
|
# Stack up (include scheduler to test the full Phase 5 setup)
|
||||||
docker compose --env-file .env.test up -d db backend
|
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 backend alembic upgrade head
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
docker compose --env-file .env.test exec \
|
docker compose --env-file .env.test exec \
|
||||||
-e TEST_DATABASE_URL=postgresql://mealplanner:${POSTGRES_PASSWORD}@db:5432/mealplanner \
|
-e TEST_DATABASE_URL=postgresql://mealplanner:${POSTGRES_PASSWORD}@db:5432/mealplanner \
|
||||||
backend pytest -q tests/ # → 92 passed
|
backend pytest -q tests/ # → 115 passed
|
||||||
|
|
||||||
|
# Verify scheduler registered all 5 jobs
|
||||||
|
docker compose --env-file .env.test logs scheduler | grep Registered
|
||||||
|
|
||||||
# Frontend
|
# Frontend
|
||||||
cd frontend && npm ci && npm run build
|
cd frontend && npm ci && npm run build
|
||||||
@@ -146,16 +154,15 @@ docker compose --env-file .env.test exec backend \
|
|||||||
|
|
||||||
## Suggested next move
|
## Suggested next move
|
||||||
|
|
||||||
Swiftly auto-mint shipped (AM-1..AM-6). Phases 4 (thin slice) and 9 are both real and verified end-to-end. The system can now scrape → match → generate without operator toil.
|
Phase 5 weekly orchestration shipped. The system now runs fully automated: scrape → match → generate → email → vote → finalize every Friday without operator intervention.
|
||||||
|
|
||||||
### Priority order
|
### Priority order
|
||||||
|
|
||||||
1. **Phase 5 — meal-planner orchestration.** Chain scrape → generate → email → vote → finalize on a weekly cadence. APScheduler container with `--workers 1` was the original plan. All the parts exist (scrape, generate, email-stub, approval round-trip); nothing chains them.
|
1. **Phase 6 — SendGrid.** Replace the `ConsoleEmailBackend` JSONL stub with real SendGrid. The proposal email and shopping-list email are now being sent by Phase 5 — they currently land in `backend/var/email_outbox.jsonl`. Templates needed: meal proposal (already rendered in `step_email`), shopping list (already rendered in `step_finalize`), T-24h reminder, denial notification. `from_email`/`reply_to` config still needs adding to Settings.
|
||||||
2. **Phase 6 — SendGrid.** Replace the `ConsoleEmailBackend` JSONL stub with real SendGrid. Templates: meal proposal, T-24h reminder, confirmation, denial. `from_email`/`reply_to` config still needs adding to Settings.
|
2. **Frontend login UI.** `/api/auth/login` exists and the cookie-based session works, but no UI consumes it. Until this lands, family-facing flows (Pantry, MealDetail, ShoppingList) can only be exercised by tests.
|
||||||
3. **Frontend login UI.** `/api/auth/login` exists and the cookie-based session works, but no UI consumes it. Until this lands, family-facing flows (Pantry, MealDetail, ShoppingList) can only be exercised by tests.
|
3. **Phase 8 — feedback UI.** Close the learning loop into Phase 9. The `feedback` table exists and the schema supports it; nothing reads/writes it from a UI.
|
||||||
4. **Phase 8 — feedback UI.** Close the learning loop into Phase 9. The `feedback` table exists and the schema supports it; nothing reads/writes it from a UI.
|
4. **Phase 11 polish — variety analysis, budget tracking.** APScheduler container is now live.
|
||||||
5. **Phase 11 polish — APScheduler, variety analysis, budget tracking.**
|
5. **Phase 10 — image strategy.**
|
||||||
6. **Phase 10 — image strategy.**
|
|
||||||
|
|
||||||
Brainstorm with the user before committing to non-trivial scope. Use the `superpowers:brainstorming` skill.
|
Brainstorm with the user before committing to non-trivial scope. Use the `superpowers:brainstorming` skill.
|
||||||
|
|
||||||
@@ -165,8 +172,9 @@ Brainstorm with the user before committing to non-trivial scope. Use the `superp
|
|||||||
|
|
||||||
| ID | Subject | Priority |
|
| ID | Subject | Priority |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Phase 5 | Weekly cadence orchestration (scrape → generate → email → vote → finalize) | High |
|
|
||||||
| Phase 6 | Replace `ConsoleEmailBackend` stub with real SendGrid | High |
|
| Phase 6 | Replace `ConsoleEmailBackend` stub with real SendGrid | High |
|
||||||
|
| #P5-a | HTML-escape recipe/ingredient names in step_email / step_finalize email templates | Minor |
|
||||||
|
| #P5-b | Simplify dead `getattr` default in `step_deadline` (line ~179 of steps.py) | Minor |
|
||||||
| #8 | `ScrapeStatus` enum could use a distinct `QUEUED` value | Cosmetic |
|
| #8 | `ScrapeStatus` enum could use a distinct `QUEUED` value | Cosmetic |
|
||||||
|
|
||||||
Other tasks in the recovery session were closed. See `.agent/phase-summaries/` for the detailed write-ups of each phase (R1A test harness, R1B+D auth+paths, R1C async scrape, R2A live scrape, R2B email approval, R3-0 Swiftly ingestion).
|
Other tasks in the recovery session were closed. See `.agent/phase-summaries/` for the detailed write-ups of each phase (R1A test harness, R1B+D auth+paths, R1C async scrape, R2A live scrape, R2B email approval, R3-0 Swiftly ingestion).
|
||||||
@@ -202,10 +210,18 @@ backend/app/
|
|||||||
├── scraper/
|
├── scraper/
|
||||||
│ ├── base.py rate-limited HTTP base
|
│ ├── base.py rate-limited HTTP base
|
||||||
│ └── lucky_ca_scraper.py Swiftly JSON API client; mints via swiftly_auth.get_token()
|
│ └── lucky_ca_scraper.py Swiftly JSON API client; mints via swiftly_auth.get_token()
|
||||||
|
├── scheduler/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── __main__.py APScheduler entry; 5 Friday Pacific jobs
|
||||||
├── services/
|
├── services/
|
||||||
│ ├── approval.py per-voter token issue/verify/consume
|
│ ├── approval.py per-voter token issue/verify/consume
|
||||||
│ ├── email.py Console + SendGrid stub
|
│ ├── email.py Console + SendGrid stub
|
||||||
│ ├── matcher.py ingredient ↔ grocery rapidfuzz scorer
|
│ ├── matcher.py ingredient ↔ grocery rapidfuzz scorer
|
||||||
|
│ ├── orchestrator/ Phase 5 weekly cycle
|
||||||
|
│ │ ├── __init__.py re-exports run_step / run_week
|
||||||
|
│ │ ├── alerts.py send_admin_alert()
|
||||||
|
│ │ ├── runner.py per-family loop; run_step / run_week
|
||||||
|
│ │ └── steps.py step_scrape/generate/email/deadline/finalize
|
||||||
│ ├── planner/ Phase 9 filter / score / select / orchestrator
|
│ ├── planner/ Phase 9 filter / score / select / orchestrator
|
||||||
│ ├── scraper_service.py enqueue + bg runner; runs matcher post-scrape
|
│ ├── scraper_service.py enqueue + bg runner; runs matcher post-scrape
|
||||||
│ └── swiftly_auth.py Firebase REST anon-signUp; process-local JWT cache
|
│ └── swiftly_auth.py Firebase REST anon-signUp; process-local JWT cache
|
||||||
@@ -214,8 +230,8 @@ backend/app/
|
|||||||
├── database.py engine / SessionLocal / get_db
|
├── database.py engine / SessionLocal / get_db
|
||||||
└── models/__init__.py all SQLAlchemy models
|
└── models/__init__.py all SQLAlchemy models
|
||||||
|
|
||||||
backend/alembic/versions/ 0001 → 0007
|
backend/alembic/versions/ 0001 → 0008
|
||||||
backend/tests/ 92 tests (incl. test_swiftly_auth.py)
|
backend/tests/ 115 tests (incl. test_swiftly_auth.py, test_orchestrator.py)
|
||||||
backend/tests/fixtures/lucky_ca/ categories.html, category_meat_seafood.json, weekly_ad.html (R2-A archive)
|
backend/tests/fixtures/lucky_ca/ categories.html, category_meat_seafood.json, weekly_ad.html (R2-A archive)
|
||||||
|
|
||||||
scripts/
|
scripts/
|
||||||
@@ -236,4 +252,4 @@ docs/specs/
|
|||||||
|
|
||||||
Trust the tests. Trust the live runs. Don't trust prose claims that something is "complete" without running the verification gate yourself. The recovery happened because the prior agent did the latter without the former.
|
Trust the tests. Trust the live runs. Don't trust prose claims that something is "complete" without running the verification gate yourself. The recovery happened because the prior agent did the latter without the former.
|
||||||
|
|
||||||
Last updated: 2026-05-06 — Swiftly auto-mint shipped (AM-1..AM-6); operator toil eliminated. Next pickup: Phase 5 weekly orchestration.
|
Last updated: 2026-05-07 — Phase 5 weekly orchestration shipped; 115/115 pytest green. Next pickup: Phase 6 SendGrid.
|
||||||
|
|||||||
+35
-17
@@ -15,13 +15,18 @@ Constraint that drives the design: 3 of 4 members do not like mushrooms; family
|
|||||||
## Architecture (current)
|
## Architecture (current)
|
||||||
|
|
||||||
```
|
```
|
||||||
email ─► SendGrid (R3-C, not yet wired) ──┐
|
email ─► SendGrid (Phase 6, not yet wired) ──┐
|
||||||
web ─► nginx :80/:443 ─► React/Vite ────┼─► FastAPI ──► PostgreSQL 15
|
web ─► nginx :80/:443 ─► React/Vite ───────┼─► FastAPI ──► PostgreSQL 15
|
||||||
│ │
|
│ │
|
||||||
│ └─► Swiftly JSON API
|
│ └─► Swiftly JSON API
|
||||||
│ (prod.swiftlyapi.net)
|
│ (prod.swiftlyapi.net)
|
||||||
│
|
│
|
||||||
└─► /api/admin/scrape (BackgroundTasks)
|
└─► /api/admin/scrape (BackgroundTasks)
|
||||||
|
|
||||||
|
scheduler ─► APScheduler (America/Los_Angeles)
|
||||||
|
Fri 02:00 scrape → 05:00 generate → 06:00 email
|
||||||
|
Fri 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`
|
- **backend** (FastAPI 0.109, SQLAlchemy 2.0, Alembic) — internal only, `expose: 8000`
|
||||||
@@ -31,7 +36,7 @@ web ─► nginx :80/:443 ─► React/Vite ────┼─► FastAPI ─
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase status (2026-05-06)
|
## Phase status (2026-05-07)
|
||||||
|
|
||||||
| # | Phase | Status |
|
| # | Phase | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -39,7 +44,7 @@ web ─► nginx :80/:443 ─► React/Vite ────┼─► FastAPI ─
|
|||||||
| 2 | DB & models (Alembic, Pydantic schemas, API endpoints) | **Complete** (real, verified) |
|
| 2 | DB & models (Alembic, Pydantic schemas, API endpoints) | **Complete** (real, verified) |
|
||||||
| 3 | Lucky California ingestion (Swiftly JSON API) | **Complete** — 17 categories, ~10k products live |
|
| 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). |
|
| 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 |
|
| 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) | Stub only — `app/services/email.py::SendGridEmailBackend` raises NotImplementedError |
|
| 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) |
|
| 7 | Web UI core (Dashboard / Meal Detail / Pantry / Shopping List) | **Complete** (no auth UI yet) |
|
||||||
| 8 | Web UI feedback portal | Not started |
|
| 8 | Web UI feedback portal | Not started |
|
||||||
@@ -47,7 +52,7 @@ web ─► nginx :80/:443 ─► React/Vite ────┼─► FastAPI ─
|
|||||||
| 10 | Image strategy (scraped + AI fallback) | Not started |
|
| 10 | Image strategy (scraped + AI fallback) | Not started |
|
||||||
| 11 | Polish (variety analysis, budget tracking, APScheduler) | 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.
|
Verification gate (current, 2026-05-07): **115/115 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; scheduler container starts with all 5 Friday jobs registered.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -69,7 +74,7 @@ Conventions: UUID PKs everywhere, `TIMESTAMPTZ`, Postgres ENUMs (with `values_ca
|
|||||||
|
|
||||||
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.
|
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.
|
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`).
|
||||||
|
|
||||||
Full schema: `docs/database-schema.md`.
|
Full schema: `docs/database-schema.md`.
|
||||||
|
|
||||||
@@ -89,7 +94,11 @@ SESSION_PASSWORD=... # family-shared password for /api/auth/login
|
|||||||
|
|
||||||
# Email
|
# Email
|
||||||
EMAIL_BACKEND=console # 'console' (default) or 'sendgrid'
|
EMAIL_BACKEND=console # 'console' (default) or 'sendgrid'
|
||||||
SENDGRID_API_KEY=... # only when EMAIL_BACKEND=sendgrid (R3-C)
|
SENDGRID_API_KEY=... # only when EMAIL_BACKEND=sendgrid (Phase 6)
|
||||||
|
|
||||||
|
# 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 / Swiftly
|
||||||
LUCKY_STORE_ID=757 # Lucky California — San Pablo
|
LUCKY_STORE_ID=757 # Lucky California — San Pablo
|
||||||
@@ -110,10 +119,18 @@ The Swiftly bearer JWT is auto-minted at request time via Firebase REST anon-sig
|
|||||||
## Verification commands
|
## Verification commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Full local stack
|
# Full local stack (include scheduler for Phase 5)
|
||||||
docker compose --env-file .env.test up -d db backend
|
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 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
|
docker compose --env-file .env.test exec -e TEST_DATABASE_URL=postgresql://mealplanner:${POSTGRES_PASSWORD}@db:5432/mealplanner backend pytest -q tests/ # → 115 passed
|
||||||
|
|
||||||
|
# Verify scheduler
|
||||||
|
docker compose --env-file .env.test logs scheduler | grep Registered
|
||||||
|
# Expected: 5 lines — scrape/generate/email/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
|
# Frontend
|
||||||
cd frontend && npm ci && npm run build
|
cd frontend && npm ci && npm run build
|
||||||
@@ -132,7 +149,7 @@ A `.env.test` template lives in the repo root (gitignored) for local stack runs.
|
|||||||
- API paths: no trailing slash, no `/list`/`/planned` suffixes.
|
- API paths: no trailing slash, no `/list`/`/planned` suffixes.
|
||||||
- Tests: pytest; `requires_postgres` marker auto-skips locally without `TEST_DATABASE_URL`.
|
- Tests: pytest; `requires_postgres` marker auto-skips locally without `TEST_DATABASE_URL`.
|
||||||
- Migrations: Alembic only. Never `Base.metadata.create_all()` at runtime.
|
- Migrations: Alembic only. Never `Base.metadata.create_all()` at runtime.
|
||||||
- Background work: FastAPI `BackgroundTasks` (current). APScheduler with `--workers 1` planned for Phase 11.
|
- Background work: FastAPI `BackgroundTasks` for on-demand scrape. APScheduler `BlockingScheduler` in the `scheduler` container for the weekly cycle (Phase 5 complete).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -146,9 +163,10 @@ A `.env.test` template lives in the repo root (gitignored) for local stack runs.
|
|||||||
- `docs/RUNNING.md` — local dev workflow.
|
- `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-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/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).
|
||||||
- `.agent/plan.md`, `.agent/context.md`, `.agent/phase-summaries/` — recovery decisions and per-phase summaries from the R1+R2+R3-0 work.
|
- `.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.
|
- `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.
|
Last updated: 2026-05-07 — Phase 5 weekly orchestration shipped; 115/115 pytest green; scheduler container live with 5 Friday Pacific jobs. Next pickup: Phase 6 SendGrid.
|
||||||
|
|||||||
Reference in New Issue
Block a user