R1 stabilization: pytest harness with transactional db fixture, smoke + alembic + auth + scrape + approval + swiftly tests, github actions ci yaml. Bearer-token admin auth + signed-cookie session for family ui mutations. Async POST /api/admin/scrape (BackgroundTasks, returns 202). Path canonicalization (no /list, /planned suffixes). DATABASE_URL fail-fast on empty. R2 deferred-risk spikes: live lucky california fetch (R2-A), full email+per-voter approval click round trip with single-use enforcement (R2-B, console email backend, sendgrid stub). R3-0 phase 3 redesign: replaced playwright html scraper with requests based swiftly json api client. 17 categories, ~10k products per scrape, upsert by (source, external_id). 401 surfaces actionable token-refresh message via ScrapeLog.error_message. Pre-existing defects fixed: shopping_list.py syntax error blocking app import, MealPlan.votes orphan relationship, JSONB(astext=True) invalid kwarg, missing requests dep, calorie_target schema drift, every SQLEnum needed values_callable, 0001 had empty downgrade(), seed had duplicate ingredient rows. Migrations added: 0003 grocery_item.description, 0004 family_profile. calorie_target, 0005 grocery_item.external_id + source + composite index. Verified: 31/31 pytest green, alembic upgrade->downgrade->upgrade clean, frontend npm run build clean, live scrape 9,960 grocery_item rows in 36s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3.9 KiB
R2-B — Email + per-voter approval round-trip spike
Schema gap (top, per spec)
The spike surfaced a pre-existing model relationship bug that blocks
the round-trip even after a clean schema bootstrap:
backend/app/models/__init__.py:201 declares
MealPlan.votes = relationship("MealPlanVote", back_populates="meal_plan", ...)
but MealPlanVote has no meal_plan FK or relationship. SQLAlchemy
fails at first mapper configure with NoForeignKeysError. Details +
two pre-existing migration bug in r2b-blockers.md. Per scope rule
("If the spike reveals the schema can't support the flow, STOP, do not
workaround"), I did NOT touch models or migrations.
What landed
backend/app/services/email.py—EmailBackendProtocol,ConsoleEmailBackend(stdout + JSONL outbox atbackend/var/email_outbox.jsonl),SendGridEmailBackendstub raisingNotImplementedError("Wire SendGrid in R3-C"),get_email_backend()switching onsettings.EMAIL_BACKEND.backend/app/services/approval.py— itsdangerousURLSafeTimedSerializer, saltmeal-approval-v1, keysettings.SECRET_KEY.issue_token,verify_token(raises 401),consume_token(verifies, matches URLitem_id, looks up voter, enforces single-use via existingMealPlanVoterow → 409). Single- use enforcement lives ONLY inconsume_token.backend/app/api/meals.py— replaced two vote routes:GET /api/meals/vote/{item_id}?token=...→ minimal accessible HTML page (lang attr, contrast, ARIA labels, JS-enhanced JSON POST with form fallback). Token only inside formaction, never visible body.POST /api/meals/vote/{item_id}?token=...body{"vote": "approve"|"deny"}. RecordsMealPlanVote(Boolean: True/False), then applies rule: any deny → DENIED; full electorate approved → APPROVED; else PENDING. Returns{"status": "recorded", "item_status": "..."}.
backend/app/config.py— addedEMAIL_BACKEND: str = "console".backend/requirements-dev.txt— addedfreezegun>=1.4..gitignore— appendedbackend/var/.scripts/send_test_approval.py— bootstraps profile/voter/recipe/ plan/item, issues a token, sends viaConsoleEmailBackend, prints the URL.--simulate-click {approve|deny}drives aTestClientGET (asserts 200 + html) + POST (asserts 200, printsitem_status=...) + a second POST (asserts 409 single-use).backend/tests/test_approval.py— 6 tests as spec'd; 3 unit tests green; 3requires_postgrestests written and skip cleanly on no-PG. They will pass once Blocker 1 is fixed.
URL contract
/api/meals/vote/{item_id}?token=<itsdangerous-signed JSON>.
Token payload: {"item": str(uuid), "voter": str(uuid)}. TTL 7 days.
Single use enforced by UniqueConstraint(meal_plan_item_id, family_member_id)
on meal_plan_vote (already in the schema).
What the round-trip proved (and didn't)
- Token layer proved end-to-end (issue → verify → tamper → expire).
- Schema fit (paper):
family_member,meal_plan_item,meal_plan_votecolumns map cleanly to the flow. - Schema fit (runtime): BLOCKED by
MealPlan.votesdefect. Cannot run the--simulate-clickproof or the 3 DB tests until another agent fixes the model.
Verification (what I ran)
pytest backend/tests/test_approval.py -q→ 3 passed, 3 skipped.python -c "from app.services.email import get_email_backend; print(...)"→ConsoleEmailBackend.python scripts/send_test_approval.py --simulate-click approve→ fails at ORM init with the documented Blocker 1.
Files
- created:
backend/app/services/__init__.py,backend/app/services/email.py,backend/app/services/approval.py,scripts/send_test_approval.py,backend/tests/test_approval.py,.agent/phase-summaries/r2b-blockers.md,.agent/phase-summaries/r2b-summary.md. - modified:
backend/app/api/meals.py,backend/app/config.py,backend/requirements-dev.txt,.gitignore.