Public Access
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:
+87
-51
@@ -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`
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user