fix: address adversarial review blockers

All §1 consensus blockers and §2 high-risk gaps resolved:

Schema fixes:
- Remove RecipeIngredient join table, use JSONB for ingredients
- Add family_member table for per-voter approval tracking
- Add all ENUMs for status fields (no loose VARCHAR)
- Add CHECK constraints (household_size, rating 1-5, day_of_week)
- Add name_lower for case-insensitive ingredient matching
- Add grocery_item → ingredient FK
- Fix day_of_week to ISO-8601 (1=Monday, 7=Sunday)
- Remove calorie_target (nutrition is non-goal)

Approval flow redesign:
- Email link → confirmation page (GET), not auto-approve
- Actual vote is POST from confirmation page
- Per-voter tokens (single-use, 72h TTL)
- Record which member voted

Auth model:
- VPN-only for admin endpoints
- Session-based for family web UI

Docker hardening:
- Remove direct port exposure for backend/frontend
- nginx is sole entrypoint
- Add docker-compose.dev.yml for local dev

Skeleton fixes:
- Add missing Pantry.tsx page
- Add missing index.html (Vite entrypoint)
- Add package-lock.json
- Fix SQLAlchemy 2 text() for raw SQL
- Remove create_all from startup (use migrations)
- Configure Alembic properly

Docs updates:
- Update Lucky URL to luckysupermarkets.com
- Add WCAG 2.1 AA accessibility target
- Update family profile with correct mushroom preferences
- Add external dependencies list to SPEC

Verification:
- docker compose config: PASS
- docker compose build backend: PASS
- docker compose build frontend: PASS
- backend import: PASS
- alembic context: PASS
This commit is contained in:
2026-05-04 20:11:05 -07:00
parent 624b51654c
commit a0b16f7418
22 changed files with 5754 additions and 272 deletions
+87 -51
View File
@@ -120,13 +120,21 @@ Recipe sites → Scraper → Parse → Store as recipe.image_source
**Approval Flow**:
```
Generate plan → Send proposal email
→ Wait for responses (48h window)
→ If deny → Swap meal with alternative
→ If approve/no response → Confirm meal
→ After all confirmations → Generate shopping list
Generate plan → Send proposal email (per-member tokens)
→ Member clicks email link → lands on confirmation page
→ Member submits vote (POST, not GET)
→ Token marked USED, vote recorded
→ If majority approve → meal confirmed
→ If any deny → meal swapped with alternative
→ After deadline → Generate shopping list
```
**Email Security**:
- Email links are GET to confirmation page (not direct approval)
- Actual vote is a POST from the confirmation page
- Tokens are single-use, expire after 72 hours
- Per-member tokens (not shared)
**Email Template Data**:
- Meal name and day
- Meal image (URL)
@@ -178,70 +186,98 @@ Generate plan → Send proposal email
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ family_ │ │ recipe │ │ meal_plan │
│ family_ │ │ family_member │ │ recipe │
│ profile │ │ │ │ │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ id │ │ id │ │ id │
│ name │◄────│ family_profile │ │ week_start_date│
│ household_size │ │ name │◄─┐ │ status │
│ dietary_notes │ │ description │ │ │ created_at │
│ preferences │ │ image_url │ │ └────────────────┘
│ created_at │ │ prep_time │ │ │
└─────────────────┘ │ cook_time │ │ │
│ │ servings │ │ │
│ │ cuisine_tags[] │ │ │
│ │ dietary_tags[] │ │ │
│ │ protein_type │ │ │
│ │ created_at │ │ │
│ └─────────────────┘ │ │
│ │ │ │
│ ▼ │ │
│ ┌─────────────────┐ │ ┌─────────────────────┐
│ │ recipe │ │ │ meal_plan_item │
│ │ _ingredient │◄──┘ ├─────────────────────┤
│ ├─────────────────┤ │ id │
│ │ recipe_id │ │ meal_plan_id │
│ │ ingredient_id │ │ recipe_id ────┘
└──────────────►│ quantity │ │ day_of_week │
│ unit │ │ approval_status │
│ is_optional │ │ approval_token │
└─────────────────┘ │ denial_reason │
└─────────────────────┘
│ id │◄────│ family_profile │ │ id │
│ name │ │ id │ │ name │
│ household_size │ │ name │ │ description │
│ adult_count │ │ email │ │ image_url │
│ child_count │ │ role │ │ ingredients │
│ dietary_notes │ │ likes_mushrooms │ │ (JSONB) │
│ budget_per_meal │ │ created_at │ │ instructions[] │
│ created_at │ └────────┬────────┘ │ cuisine_tags[] │
└────────┬────────┘ │ │ dietary_tags[] │
│ │ │ protein_type │
│ ▼ │ prep/cook_time │
│ ┌─────────────────┐ │ servings │
│ │ meal_plan_vote │ │ created_at │
│ ├─────────────────┤ └────────┬────────┘
│ │ id │ │
│ │ meal_plan_item │ │
│ │ family_member │ │
│ │ vote (bool) │ │
│ │ voted_at │ │
│ └─────────────────┘ │
│ ▲ │
│ │ ▼
│ ┌─────────────────┐ ┌─────────────────────┐
│ │ meal_plan_item │ │ ingredient │
│ ├─────────────────┤ ├─────────────────────┤
│ │ id │ │ id │
│ │ meal_plan_id │ │ name │
│ │ recipe_id │ │ name_lower (unique) │
└─────────────►│ day_of_week │ │ aisle │
│ meal_type │ │ typical_price │
│ approval_status│ │ unit │
│ denial_reason │ │ season_months[] │
│ estimated_cost │ └─────────────────────┘
└────────┬────────┘ ▲
│ │
▼ │
┌─────────────────┐ ┌───────┴─────────────┐
│ meal_plan │ │ grocery_item │
├─────────────────┤ ├───────────────────┤
│ id │ │ id │
│ week_start_date │ │ ingredient_id (FK) │
│ status │ │ name │
│ total_cost │ │ current_price │
│ approval_deadline│ │ regular_price │
└─────────────────┘ │ is_on_sale │
│ sale_end_date │
│ scraped_at │
└───────────────────┘
┌─────────────────┐ ┌─────────────────┐
│ grocery │ │ home_ │
│ item │ │ pantry │
├─────────────────┤ ├─────────────────┤
│ id │ │ id │
│ name │ │ family_profile │
│ aisle │ │ ingredient_id │
│ current_price │ │ quantity │
│ is_on_sale │ │ added_at │
│ sale_end_date │ │ expires_at │
│ season_months[] │ └─────────────────┘
│ scraped_at │
┌─────────────────┐
│ home_pantry │
├─────────────────┤
│ id │
│ family_profile │
│ ingredient_id │
│ quantity │
│ unit │
│ expires_at │
│ added_at │
└─────────────────┘
┌─────────────────┐ ┌─────────────────┐
│ feedback │ │ ingredient │
│ feedback │ │ approval_token │
├─────────────────┤ ├─────────────────┤
│ id │ │ id │
│ meal_plan_item │ │ name │
│ rating │ │ aisle │
│ never_suggest │ │ typical_price │
│ denial_reason │ │ season_months[] │
│ feedback_text │ │ created_at │
│ family_member │ │ meal_plan_item │
│ meal_plan_item │ │ family_member │
│ rating │ │ token │
│ never_suggest │ │ status │
│ denial_reason │ │ expires_at │
│ feedback_text │ │ used_at │
│ created_at │ └─────────────────┘
└─────────────────┘
```
### 3.2 Key Relationships
- `family_profile` 1:N `family_member`
- `family_profile` 1:N `meal_plan`
- `family_profile` 1:N `home_pantry`
- `recipe` N:N `ingredient` (via `recipe_ingredient`)
- `family_member` 1:N `meal_plan_vote` (per-member voting)
- `recipe` 1:N `meal_plan_item`
- `meal_plan` 1:N `meal_plan_item`
- `meal_plan` 1:N `meal_plan_vote`
- `meal_plan_item` 1:N `meal_plan_vote`
- `meal_plan_item` 1:N `approval_token`
- `meal_plan_item` 1:1 `feedback`
- `grocery_item` → `ingredient` (FK)
- `ingredient` 1:N `home_pantry`
- `ingredient` 1:N `grocery_item`
---