Public Access
Backend: - POST /api/ingredients now checks name_lower and aliases before inserting - Returns existing ingredient on 409 instead of throwing error Frontend: - Removed fragile 409-recovery logic from Pantry.tsx handleAdd - Added aliases field to Ingredient type for case-insensitive matching Fixes pantry add for ingredients like 'Carrots' whose canonical name is 'Carrot'
20 KiB
20 KiB
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, salesRecipeSiteScraper: 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:
- Dashboard: Current week's meal plan, approval status, shopping list summary
- Meal Detail: Full recipe view, ingredients, cooking instructions, print button
- Approval Center: Pending approvals with Approve/Deny actions
- Pantry Manager: Add/remove home pantry items
- Feedback Portal: Rate completed meals, flag ingredients
- 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_profile1:Nfamily_memberfamily_profile1:Nmeal_planfamily_profile1:Nhome_pantryfamily_member1:Nmeal_plan_vote(per-member voting)recipe1:Nmeal_plan_itemmeal_plan1:Nmeal_plan_itemmeal_plan1:Nmeal_plan_votemeal_plan_item1:Nmeal_plan_votemeal_plan_item1:Napproval_tokenmeal_plan_item1:1feedbackgrocery_item→ingredient(FK)ingredient1:Nhome_pantryingredient1:Ngrocery_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 |
| POST | /api/ingredients |
Create or resolve ingredient (public, idempotent) |
| 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 pageGET /api/deny/{token}→ Show denial reason formPOST /api/deny/{token}→ Process denial with reason
5. Deployment Architecture
5.1 Docker Compose Services
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
- Log error with full context
- Retry 3 times with exponential backoff
- If all fail, flag for manual review
- Send admin notification after 3 failures
- Graceful degradation: allow manual sale input
7.2 Email Failures
- Log send attempt
- Retry via SendGrid's built-in retry
- If permanently failed, mark meal as "pending email confirmation"
- Web UI remains as fallback for approval
7.3 Database Failures
- Connection pooling with automatic reconnect
- Read operations can fall back to cache if available
- Write operations queued for retry
8. Logging & Monitoring
8.1 Log Categories
scraper: All scraping operations and resultsemail: SendGrid API calls, delivery statusmeal_planner: Plan generation inputs/outputsapi: All HTTP requestsfeedback: 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