Public Access
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
441 lines
20 KiB
Markdown
441 lines
20 KiB
Markdown
# Meal Planner - Architecture
|
|
|
|
## 1. System Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ USER FACING │
|
|
│ │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
|
|
│ │ Email │ │ Web UI │ │ Future: WhatsApp │ │
|
|
│ │ (SendGrid) │ │ (React) │ │ (Twilio) │ │
|
|
│ └──────┬───────┘ └──────┬───────┘ └───────────┬──────────────┘ │
|
|
└─────────┼────────────────────┼───────────────────────┼─────────────────┘
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ BACKEND (FastAPI) │
|
|
│ │
|
|
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │
|
|
│ │ Scraper │ │ Recipe │ │ Meal │ │ Notification │ │
|
|
│ │ Service │ │ Engine │ │ Planner │ │ Service │ │
|
|
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────────┬──────────┘ │
|
|
│ │ │ │ │ │
|
|
│ ┌─────┴───────────────┴───────────────┴────────────────────┴────────┐ │
|
|
│ │ SERVICE LAYER │ │
|
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │
|
|
│ │ │ Grocery │ │ Recipe │ │ MealPlan │ │ Feedback │ │ │
|
|
│ │ │ Service │ │ Service │ │ Service │ │ Service │ │ │
|
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ │
|
|
│ └────────────────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ DATA LAYER (PostgreSQL) │ │
|
|
│ └────────────────────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ EXTERNAL SERVICES │
|
|
│ │
|
|
│ ┌───────────────┐ ┌───────────────┐ ┌────────────────────────────┐ │
|
|
│ │ Lucky │ │ Recipe Sites │ │ AI Image Service │ │
|
|
│ │ California │ │ (scraping) │ │ (fallback only) │ │
|
|
│ │ (scrape) │ │ │ │ │ │
|
|
│ └───────────────┘ └───────────────┘ └────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Component Descriptions
|
|
|
|
### 2.1 Scraper Service
|
|
**Responsibility**: Fetch and parse data from Lucky California and public recipe sites
|
|
|
|
**Modules**:
|
|
- `LuckyCaliforniaScraper`: Scrapes weekly ad, product catalog, sales
|
|
- `RecipeSiteScraper`: Scrapes recipe images and metadata from public sites
|
|
|
|
**Technology**: Playwright + BeautifulSoup
|
|
|
|
**Data Flow**:
|
|
```
|
|
Lucky California website → Scraper → Parse → Store in PostgreSQL
|
|
Recipe sites → Scraper → Parse → Store as recipe.image_source
|
|
```
|
|
|
|
**Error Handling**:
|
|
- Retry with exponential backoff (3 attempts)
|
|
- Log failures for admin review
|
|
- Graceful degradation to manual input
|
|
|
|
---
|
|
|
|
### 2.2 Recipe Engine
|
|
**Responsibility**: Store, tag, and retrieve recipes
|
|
|
|
**Capabilities**:
|
|
- CRUD operations for recipes
|
|
- Tag-based filtering (cuisine, protein, dietary, season)
|
|
- Ingredient matching against home pantry
|
|
- Image URL storage (scraped primary, AI fallback)
|
|
|
|
**Schema**:
|
|
- Recipe: id, name, description, ingredients[], instructions, image_url, cuisine_tags[], dietary_tags[], protein_type, prep_time, cook_time, servings, created_at
|
|
- Ingredient: id, name, aisle, typical_price_range, season_months[]
|
|
|
|
---
|
|
|
|
### 2.3 Meal Planner Service
|
|
**Responsibility**: Generate weekly meal plans based on constraints
|
|
|
|
**Algorithm Inputs**:
|
|
- Family profile (size, dietary constraints, preferences)
|
|
- Scraped grocery data (sales, in-season)
|
|
- Home pantry items
|
|
- Feedback history (avoid denied meals, prioritize liked ingredients)
|
|
- Variety constraints (max 2 meals with same sauce/protein)
|
|
|
|
**Algorithm Output**:
|
|
- 7-day meal plan with one meal per day
|
|
- Each meal includes: recipe, adjusted ingredients (substituting sale items), estimated cost per serving
|
|
|
|
**Learning Mechanism**:
|
|
- Weight denied meals at 0 (never re-suggest)
|
|
- Weight high-rated meals higher for future weeks
|
|
- Track ingredient frequency to enforce variety
|
|
|
|
---
|
|
|
|
### 2.4 Notification Service
|
|
**Responsibility**: Send emails and manage approval workflow
|
|
|
|
**Email Triggers**:
|
|
- Weekly meal proposal (to both adults)
|
|
- Reminder email (2 days before shipping deadline)
|
|
- Final meal plan confirmation
|
|
- Shopping list ready notification
|
|
|
|
**Approval Flow**:
|
|
```
|
|
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)
|
|
- Ingredient list with pricing
|
|
- Approve/Deny links (tokenized URLs)
|
|
- Estimated total cost
|
|
|
|
---
|
|
|
|
### 2.5 Feedback Service
|
|
**Responsibility**: Collect and process family feedback
|
|
|
|
**Feedback Types**:
|
|
- **Rating**: 1-5 stars per meal
|
|
- **Denial Reason**: too_expensive, boring, disliked_ingredient, cultural, other
|
|
- **Never-Suggest Flag**: per ingredient or per recipe
|
|
- **Home Pantry Update**: items currently available
|
|
|
|
**Learning Integration**:
|
|
- Feedback stored with timestamps
|
|
- Aggregated weekly to adjust meal planner weights
|
|
- "Never-suggest" ingredients filtered from all future proposals
|
|
- Denial reasons used to improve substitutions
|
|
|
|
---
|
|
|
|
### 2.6 Web UI (React + Tailwind)
|
|
**Responsibility**: Interactive interface for family members
|
|
|
|
**Pages**:
|
|
1. **Dashboard**: Current week's meal plan, approval status, shopping list summary
|
|
2. **Meal Detail**: Full recipe view, ingredients, cooking instructions, print button
|
|
3. **Approval Center**: Pending approvals with Approve/Deny actions
|
|
4. **Pantry Manager**: Add/remove home pantry items
|
|
5. **Feedback Portal**: Rate completed meals, flag ingredients
|
|
6. **Admin/Settings**: Family profile, scraper status, email history
|
|
|
|
**Technology**:
|
|
- React 18+ with TypeScript
|
|
- Tailwind CSS for styling
|
|
- React Query for data fetching
|
|
- React Router for navigation
|
|
|
|
---
|
|
|
|
## 3. Data Architecture
|
|
|
|
### 3.1 PostgreSQL Schema Overview
|
|
|
|
```
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
│ family_ │ │ family_member │ │ recipe │
|
|
│ profile │ │ │ │ │
|
|
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
|
|
│ 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 │
|
|
└───────────────────┘
|
|
|
|
┌─────────────────┐
|
|
│ home_pantry │
|
|
├─────────────────┤
|
|
│ id │
|
|
│ family_profile │
|
|
│ ingredient_id │
|
|
│ quantity │
|
|
│ unit │
|
|
│ expires_at │
|
|
│ added_at │
|
|
└─────────────────┘
|
|
|
|
┌─────────────────┐ ┌─────────────────┐
|
|
│ feedback │ │ approval_token │
|
|
├─────────────────┤ ├─────────────────┤
|
|
│ id │ │ id │
|
|
│ 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`
|
|
- `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`
|
|
|
|
---
|
|
|
|
## 4. API Design
|
|
|
|
### 4.1 Core Endpoints
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| GET | `/api/profile` | Get family profile |
|
|
| PUT | `/api/profile` | Update family profile |
|
|
| GET | `/api/meals/planned` | Get current week's meal plan |
|
|
| GET | `/api/meals/{id}` | Get meal details with recipe |
|
|
| POST | `/api/meals/{id}/approve` | Approve a meal |
|
|
| POST | `/api/meals/{id}/deny` | Deny a meal with reason |
|
|
| GET | `/api/shopping-list` | Get current week's shopping list |
|
|
| GET | `/api/shopping-list/print` | Get printable shopping list |
|
|
| GET | `/api/pantry` | Get home pantry items |
|
|
| POST | `/api/pantry` | Add item to pantry |
|
|
| DELETE | `/api/pantry/{id}` | Remove item from pantry |
|
|
| POST | `/api/feedback` | Submit meal feedback |
|
|
| GET | `/api/recipes` | Search recipes |
|
|
| POST | `/api/admin/scrape` | Trigger grocery scrape (admin) |
|
|
| GET | `/api/admin/logs` | Get scraper logs |
|
|
|
|
### 4.2 Email Approval Links
|
|
- `GET /api/approve/{token}` → Mark meal approved, redirect to confirmation page
|
|
- `GET /api/deny/{token}` → Show denial reason form
|
|
- `POST /api/deny/{token}` → Process denial with reason
|
|
|
|
---
|
|
|
|
## 5. Deployment Architecture
|
|
|
|
### 5.1 Docker Compose Services
|
|
|
|
```yaml
|
|
services:
|
|
backend:
|
|
build: ./backend
|
|
ports:
|
|
- "8000:8000"
|
|
environment:
|
|
- DATABASE_URL=postgresql://user:pass@db:5432/mealplanner
|
|
- SENDGRID_API_KEY=${SENDGRID_API_KEY}
|
|
depends_on:
|
|
- db
|
|
restart: unless-stopped
|
|
|
|
frontend:
|
|
build: ./frontend
|
|
ports:
|
|
- "3000:80" # nginx serves built React app
|
|
depends_on:
|
|
- backend
|
|
restart: unless-stopped
|
|
|
|
db:
|
|
image: postgres:15-alpine
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
environment:
|
|
- POSTGRES_USER=mealplanner
|
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
|
- POSTGRES_DB=mealplanner
|
|
restart: unless-stopped
|
|
|
|
nginx:
|
|
image: nginx:alpine
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
volumes:
|
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
|
- ./ssl:/etc/nginx/ssl
|
|
depends_on:
|
|
- frontend
|
|
- backend
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
postgres_data:
|
|
```
|
|
|
|
### 5.2 Reverse Proxy (Nginx)
|
|
|
|
Nginx handles:
|
|
- SSL termination for remote access
|
|
- Routing `/api/*` to backend
|
|
- Routing `/*` to frontend
|
|
- Rate limiting on approval endpoints
|
|
|
|
### 5.3 Remote Access Security
|
|
- Caddy or Nginx with Let's Encrypt
|
|
- VPN option for additional security
|
|
- IP whitelist capability
|
|
|
|
---
|
|
|
|
## 6. Image Strategy
|
|
|
|
### 6.1 Primary: Scraped Images
|
|
- Recipe images scraped from public recipe sites
|
|
- Lucky California product images where available
|
|
- Stored as URLs (not downloaded), hotlink protection handled gracefully
|
|
|
|
### 6.2 Fallback: AI Generation
|
|
- Triggered only when scraped image unavailable
|
|
- Uses configured AI service (DALL-E, Anthropic, etc.)
|
|
- Generated images cached in database
|
|
- Config flag to enable/disable
|
|
|
|
### 6.3 Email Image Handling
|
|
- Images embedded via CDN or inline base64
|
|
- Alt text provided for email clients that block images
|
|
- Link to web UI for full image gallery
|
|
|
|
---
|
|
|
|
## 7. Error Handling Strategy
|
|
|
|
### 7.1 Scraper Failures
|
|
1. Log error with full context
|
|
2. Retry 3 times with exponential backoff
|
|
3. If all fail, flag for manual review
|
|
4. Send admin notification after 3 failures
|
|
5. Graceful degradation: allow manual sale input
|
|
|
|
### 7.2 Email Failures
|
|
1. Log send attempt
|
|
2. Retry via SendGrid's built-in retry
|
|
3. If permanently failed, mark meal as "pending email confirmation"
|
|
4. Web UI remains as fallback for approval
|
|
|
|
### 7.3 Database Failures
|
|
1. Connection pooling with automatic reconnect
|
|
2. Read operations can fall back to cache if available
|
|
3. Write operations queued for retry
|
|
|
|
---
|
|
|
|
## 8. Logging & Monitoring
|
|
|
|
### 8.1 Log Categories
|
|
- `scraper`: All scraping operations and results
|
|
- `email`: SendGrid API calls, delivery status
|
|
- `meal_planner`: Plan generation inputs/outputs
|
|
- `api`: All HTTP requests
|
|
- `feedback`: Feedback submissions
|
|
|
|
### 8.2 Log Storage
|
|
- JSON logs to stdout (Docker log driver)
|
|
- Centralized logging optional (Papertrail, Datadog)
|
|
- Log rotation: 7 days local
|
|
|
|
### 8.3 Monitoring (Future)
|
|
- Uptime monitoring
|
|
- Scraping success rate
|
|
- Email delivery rate
|
|
- Approval response rate
|