Files
Meal-Planner/docs/ARCHITECTURE.md
T
admin a0b16f7418 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
2026-05-04 20:11:05 -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
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