Files
Meal-Planner/docs/ARCHITECTURE.md
T
admin 69acd70188
CI / backend (pytest + alembic) (push) Has been cancelled
CI / frontend (build) (push) Has been cancelled
fix(pantry): make public ingredient endpoint idempotent
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'
2026-05-18 17:16:57 -07:00

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, 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_itemingredient (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
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
  • 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

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