Public Access
feat: phase r1+r2 recovery + r3-0 swiftly api ingestion
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>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# R2-B blockers — pre-existing defects discovered by the spike
|
||||
|
||||
The R2-B email + per-voter approval round-trip cannot complete the
|
||||
end-to-end DB-backed proof until both of the following pre-existing bugs
|
||||
are fixed. Both are OUT OF SCOPE for R2-B per the task brief.
|
||||
|
||||
## Blocker 1: model relationship bug — `MealPlan.votes`
|
||||
|
||||
**File:** `backend/app/models/__init__.py:201`
|
||||
|
||||
```python
|
||||
class MealPlan(Base):
|
||||
...
|
||||
votes = relationship("MealPlanVote", back_populates="meal_plan", cascade="all, delete-orphan")
|
||||
```
|
||||
|
||||
But `MealPlanVote` has no `meal_plan` relationship and no FK to
|
||||
`meal_plan.id` — only to `meal_plan_item.id` (line 235).
|
||||
|
||||
**Symptom:** `sqlalchemy.exc.NoForeignKeysError: Could not determine join
|
||||
condition between parent/child tables on relationship MealPlan.votes`
|
||||
fires the moment ANY mapper is configured (i.e. on the first ORM use of
|
||||
any model). Blocks every flow, not just the vote flow.
|
||||
|
||||
**Repro (with schema in place):**
|
||||
|
||||
```bash
|
||||
DATABASE_URL='postgresql://...' python -c "
|
||||
from app import models
|
||||
from sqlalchemy.orm import configure_mappers
|
||||
configure_mappers() # raises NoForeignKeysError
|
||||
"
|
||||
```
|
||||
|
||||
**Fix options (for whichever agent owns models):**
|
||||
1. Remove `MealPlan.votes` (votes are reachable via `MealPlan.items[*].votes`).
|
||||
2. Add `meal_plan_id` FK to `MealPlanVote` and a back-ref. Requires migration.
|
||||
3. Specify `primaryjoin="MealPlan.id == foreign(remote(MealPlanVote.meal_plan_item_id))"` via `MealPlanItem` — viewonly only.
|
||||
|
||||
Option 1 is least invasive and matches existing usage in `meals.py` (no
|
||||
code reads `MealPlan.votes`).
|
||||
|
||||
## Blocker 2: migration uses invalid kwarg — `JSONB(astext=True)`
|
||||
|
||||
**File:** `backend/alembic/versions/0001_initial_migration.py:101`
|
||||
|
||||
```python
|
||||
sa.Column('ingredients', postgresql.JSONB(astext=True), nullable=False),
|
||||
```
|
||||
|
||||
`astext` is not a valid `JSONB.__init__` kwarg in SQLAlchemy 2.x.
|
||||
|
||||
**Symptom:** `alembic upgrade head` fails with:
|
||||
`TypeError: JSON.__init__() got an unexpected keyword argument 'astext'`.
|
||||
|
||||
**Impact:** `backend/tests/test_alembic.py::test_alembic_upgrade_head_roundtrip`
|
||||
fails. `backend/tests/conftest.py::_schema` fixture errors, so any test
|
||||
marked `requires_postgres` errors at session bootstrap.
|
||||
|
||||
**Fix:** drop the kwarg. `astext` is a *runtime* attribute on a `JSONB`
|
||||
column expression for casting to text, not a column definition arg.
|
||||
|
||||
```python
|
||||
sa.Column('ingredients', postgresql.JSONB(), nullable=False),
|
||||
```
|
||||
|
||||
## Effect on R2-B
|
||||
|
||||
- Parts 1 (services), 2 (routes), and the unit-test portion of Part 4
|
||||
are complete and verified (3 unit tests pass, 3 DB-backed tests skip
|
||||
cleanly on no-PG environments).
|
||||
- Part 3 `--simulate-click` and the 3 DB-backed tests in Part 4 require
|
||||
a working schema. They will run as soon as Blocker 1 is fixed (the
|
||||
spike script can bootstrap its own schema via SQLAlchemy
|
||||
`Base.metadata.create_all` once the relationship resolves).
|
||||
|
||||
## Sandbox cleanup note
|
||||
|
||||
While diagnosing Blocker 1, this agent ran `Base.metadata.create_all`
|
||||
against `mealplanner-db-1` (192.168.144.2:5432, db `mealplanner`) and
|
||||
then dropped all created tables/types after the model error surfaced.
|
||||
Final state: only `alembic_version` (matches pre-spike state).
|
||||
Reference in New Issue
Block a user