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 │ │ descriptioncreated_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`
---