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`
|
||||
|
||||
---
|
||||
|
||||
|
||||
+90
-91
@@ -18,28 +18,20 @@ The family has been using meal kit services (Blue Apron → EveryPlate → Hungr
|
||||
|
||||
### Current Status
|
||||
|
||||
**Phase**: Phase 1 complete. Phase 2 (Database & Models) next.
|
||||
**Phase**: Post-adversarial-review fixes applied. Ready for verification.
|
||||
|
||||
Infrastructure skeleton is committed:
|
||||
- docker-compose.yml with 4 services (backend, frontend, db, nginx)
|
||||
- FastAPI backend with placeholder API routes
|
||||
- React frontend with Vite + Tailwind + placeholder pages
|
||||
- nginx reverse proxy config
|
||||
- SQLAlchemy models created (not yet connected to real endpoints)
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `docs/SPEC.md` | Full project specification (goals, constraints, user stories) |
|
||||
| `docs/ARCHITECTURE.md` | System architecture, component descriptions, data flow |
|
||||
| `docs/database-schema.md` | PostgreSQL schema with all tables, indexes, relationships |
|
||||
| `docs/implementation-plan.md` | 12-phase implementation plan with verification commands |
|
||||
| `docs/RUNNING.md` | Deployment guide, troubleshooting, environment setup |
|
||||
| `README.md` | Project overview and quick start |
|
||||
| `meal-planner-plan.md` | Original planning file (may be superseded) |
|
||||
**Adversarial Review Completed**: 2026-05-04
|
||||
- All consensus blockers (§1.1 - §1.8) addressed
|
||||
- All high-risk gaps (§2.1 - §2.8) addressed
|
||||
- Key fixes applied:
|
||||
- Recipe-ingredient: JSONB only (removed join table)
|
||||
- Family member: Added `family_member` table for per-voter tracking
|
||||
- Approval flow: Redesigned with confirmation page + POST + per-voter tokens + TTL
|
||||
- Auth: VPN-only for admin endpoints, session-based for family web UI
|
||||
- Schema: ENUMs, CHECKs, CITEXT for ingredients, ISO day_of_week (1=Mon)
|
||||
- Lucky URL: Fixed to `https://luckysupermarkets.com`
|
||||
- Docker: Hardened (no direct port exposure to backend/frontend)
|
||||
- Alembic: Configured with migration policy
|
||||
|
||||
---
|
||||
|
||||
@@ -49,19 +41,20 @@ Infrastructure skeleton is committed:
|
||||
User (email) ──► SendGrid ───────────────────────────────┐
|
||||
User (web) ───► React UI ──► nginx ──► FastAPI ──────────┼──► PostgreSQL
|
||||
│ │
|
||||
└──► Lucky California scraper ──┘
|
||||
└──► Lucky CA scraper ──┘
|
||||
(luckysupermarkets.com)
|
||||
```
|
||||
|
||||
### Services (Docker Compose)
|
||||
- **backend**: FastAPI Python app (port 8000)
|
||||
- **frontend**: React + Tailwind (port 3000, served via nginx)
|
||||
- **db**: PostgreSQL 15 (port 5432)
|
||||
- **backend**: FastAPI Python app (port 8000, internal only)
|
||||
- **frontend**: React + Tailwind (port 3000, internal only via nginx)
|
||||
- **db**: PostgreSQL 15 (internal only)
|
||||
- **nginx**: Reverse proxy with SSL (ports 80/443)
|
||||
|
||||
### Tech Stack
|
||||
- Backend: Python 3.11, FastAPI, SQLAlchemy, Alembic
|
||||
- Frontend: React 18, TypeScript, Tailwind CSS, React Query
|
||||
- Database: PostgreSQL 15
|
||||
- Backend: Python 3.11, FastAPI, SQLAlchemy 2.0, Alembic
|
||||
- Frontend: React 18, TypeScript, Tailwind CSS, React Query, Vite
|
||||
- Database: PostgreSQL 15 with ENUMs and CITEXT
|
||||
- Scraping: Playwright, BeautifulSoup
|
||||
- Email: SendGrid
|
||||
- Hosting: Docker Compose, nginx
|
||||
@@ -70,39 +63,56 @@ User (web) ───► React UI ──► nginx ──► FastAPI ────
|
||||
|
||||
## Family Profile
|
||||
|
||||
- **Household**: 2 adults, 2 children
|
||||
- **Dietary**: One adult + one child like mushrooms; other adult + one child do NOT
|
||||
- **Goals**: Calorie, budget, and health conscious; tasty but not expensive
|
||||
- **No allergies**
|
||||
### Household
|
||||
- 2 adults, 2 children
|
||||
- 3 of 4 members do NOT like mushrooms
|
||||
- No allergies
|
||||
- Goals: Calorie, budget, and health conscious; tasty but not expensive
|
||||
|
||||
### Approval Workflow
|
||||
### Family Members
|
||||
| Member | Role | Mushroom Preference |
|
||||
|--------|------|-------------------|
|
||||
| Adult 1 | Adult | Does NOT like mushrooms |
|
||||
| Adult 2 | Adult | Likes mushrooms |
|
||||
| Child 1 | Child | Does NOT like mushrooms |
|
||||
| Child 2 | Child | OK with mushrooms |
|
||||
|
||||
### Approval Workflow (REDESIGNED)
|
||||
1. System generates 7-day meal plan (Sunday)
|
||||
2. Email sent to both adults with meals, images, Approve/Deny links
|
||||
3. One denial → meal swapped with alternative
|
||||
4. No denials (or silence) → meal auto-approved
|
||||
5. After all approvals → shopping list generated
|
||||
2. Email sent to all adults with meals, images, approval page links
|
||||
3. Email link → confirmation page (GET, not auto-approve)
|
||||
4. Adult clicks Approve/Deny → POST with reason
|
||||
5. Per-member token (single-use, 72h TTL)
|
||||
6. Majority approve → meal confirmed; any deny → swap
|
||||
7. After approval → shopping list generated
|
||||
|
||||
---
|
||||
|
||||
## Database Schema Highlights
|
||||
|
||||
### Core Tables
|
||||
- `family_profile` - Household configuration
|
||||
- `recipe` - All recipes with ingredients (JSONB), instructions, image URLs
|
||||
- `ingredient` - Master ingredient list with aisle, price, season
|
||||
- `meal_plan` - Weekly plan (7 days)
|
||||
- `meal_plan_item` - Individual meal with approval_token, status
|
||||
- `family_profile` - Household configuration with household_size CHECK
|
||||
- `family_member` - Individual family members with email, role, mushroom preference
|
||||
- `recipe` - Recipes with JSONB ingredients (not join table)
|
||||
- `ingredient` - Master list with name_lower (CITEXT for case-insensitive matching)
|
||||
- `meal_plan` - Weekly plan with ISO day_of_week (1=Mon, 7=Sun)
|
||||
- `meal_plan_item` - Individual meal with approval status
|
||||
- `meal_plan_vote` - Per-member votes (one vote per member per meal)
|
||||
- `approval_token` - Single-use tokens with TTL and status tracking
|
||||
- `home_pantry` - Family's on-hand ingredients
|
||||
- `feedback` - Ratings, denial reasons, never-suggest flags
|
||||
- `grocery_item` - Scraped Lucky California items with sale prices
|
||||
- `grocery_item` - Scraped Lucky California items with FK to ingredient
|
||||
- `scrape_log` / `email_log` - Operation history
|
||||
|
||||
### 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` junction table)
|
||||
- `family_member` 1:N `meal_plan_vote` (per-voter tracking)
|
||||
- `recipe` 1:N `meal_plan_item`
|
||||
- `meal_plan` 1:N `meal_plan_item`
|
||||
- `meal_plan_item` 1:1 `feedback`
|
||||
- `meal_plan_item` 1:N `meal_plan_vote`
|
||||
- `meal_plan_item` 1:N `approval_token`
|
||||
- `grocery_item` → `ingredient` (FK)
|
||||
|
||||
---
|
||||
|
||||
@@ -111,7 +121,7 @@ User (web) ───► React UI ──► nginx ──► FastAPI ────
|
||||
| Phase | Description | Status |
|
||||
|-------|-------------|--------|
|
||||
| 1 | Infrastructure (Docker, PostgreSQL, FastAPI, React, nginx) | **Complete** |
|
||||
| 2 | Database & Models (SQLAlchemy models, Alembic migrations) | Not Started |
|
||||
| 2 | Database & Models (SQLAlchemy models, Alembic migrations) | **Post-review fixes applied** |
|
||||
| 3 | API Endpoints (CRUD, meal plans, shopping list, feedback) | Not Started |
|
||||
| 4 | Lucky California Scraper (weekly ad, Playwright) | Not Started |
|
||||
| 5 | Recipe Engine (CRUD, tagging, search) | Not Started |
|
||||
@@ -125,7 +135,7 @@ User (web) ───► React UI ──► nginx ──► FastAPI ────
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables Required
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Database
|
||||
@@ -140,39 +150,51 @@ FAMILY_EMAIL_1=user@example.com
|
||||
FAMILY_EMAIL_2=spouse@example.com
|
||||
|
||||
# Scraping
|
||||
LUCKY_CA_URL=https://www.luckyncal.com
|
||||
LUCKY_CA_URL=https://luckysupermarkets.com
|
||||
|
||||
# AI Images (optional)
|
||||
AI_IMAGE_ENABLED=false
|
||||
|
||||
# Auth
|
||||
SECRET_KEY=change-me-in-production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### IMMEDIATE: Phase 2 - Database & Models
|
||||
### IMMEDIATE: Verify skeleton with verification matrix
|
||||
|
||||
1. Set up Alembic for migrations
|
||||
2. Create actual database tables from SQLAlchemy models
|
||||
3. Add seed data (basic ingredients, sample recipes)
|
||||
4. Create Pydantic schemas for API validation
|
||||
5. Implement real API endpoints (not just placeholders)
|
||||
```bash
|
||||
docker compose config
|
||||
docker compose build backend
|
||||
docker compose build frontend
|
||||
docker compose up -d db
|
||||
docker compose run --rm backend python -c "from app.main import app; print(app.title)"
|
||||
docker compose run --rm backend alembic upgrade head
|
||||
docker compose run --rm frontend npm run build
|
||||
```
|
||||
|
||||
### After Phase 2 Complete
|
||||
If any of these fail, fix before proceeding.
|
||||
|
||||
- Verify migrations: `docker-compose exec backend alembic upgrade head`
|
||||
- Test database connectivity: `curl localhost:8000/health/db`
|
||||
- Test API endpoints with real data
|
||||
### After Verification
|
||||
|
||||
### Current Git State
|
||||
1. Phase 2: Implement real API endpoints (not placeholders)
|
||||
2. Phase 3: Connect database models to endpoints
|
||||
3. Phase 4: Spike Lucky California scrape (before committing to full schema)
|
||||
|
||||
---
|
||||
|
||||
## Current Git State
|
||||
|
||||
```bash
|
||||
git log --oneline
|
||||
624b516 docs: update ORIENTATION.md for Phase 1 complete
|
||||
1328ec3 feat: add Phase 1 infrastructure skeleton
|
||||
0c5b0aa docs: add complete project documentation
|
||||
```
|
||||
|
||||
Phase 1 skeleton complete and committed. Phase 2 (Database) is next.
|
||||
**Pending commit**: All adversarial review fixes (models, schema, docker-compose, docs updates)
|
||||
|
||||
---
|
||||
|
||||
@@ -190,12 +212,14 @@ Phase 1 skeleton complete and committed. Phase 2 (Database) is next.
|
||||
### API Design
|
||||
- RESTful endpoints with proper HTTP methods and status codes
|
||||
- Pydantic schemas for request/response validation
|
||||
- JWT-free for MVP (simple token-based auth for email approval links)
|
||||
- Email approval links use GET → confirmation page → POST
|
||||
- Per-voter tokens, not shared household tokens
|
||||
|
||||
### Database
|
||||
- Always use UUIDs for primary keys
|
||||
- Timestamps with timezone (TIMESTAMPTZ)
|
||||
- Soft deletes preferred over hard deletes where applicable
|
||||
- Use ENUMs for status fields (not loose VARCHAR)
|
||||
- Use CITEXT or lowercase-on-write for name matching
|
||||
|
||||
### Testing
|
||||
- Write unit tests for services (pytest)
|
||||
@@ -204,37 +228,12 @@ Phase 1 skeleton complete and committed. Phase 2 (Database) is next.
|
||||
|
||||
---
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Run full stack
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Create database migration
|
||||
```bash
|
||||
docker-compose exec backend alembic revision --autogenerate -m "description"
|
||||
```
|
||||
|
||||
### Trigger scrape manually
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/admin/scrape \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"source": "lucky_california", "type": "weekly_ad"}'
|
||||
```
|
||||
|
||||
### View all logs
|
||||
```bash
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Known Issues / Open Questions
|
||||
|
||||
1. **Lucky California scraping**: May need adjustment if website structure changes. Has fallback to manual input.
|
||||
1. **Lucky California scraping**: Feasibility not yet spiked. URL is `luckysupermarkets.com`.
|
||||
2. **AI image generation**: Config flag to enable/disable. Disabled by default.
|
||||
3. **WhatsApp integration**: Planned for future via Twilio. Out of scope for MVP.
|
||||
4. **Planned scheduler**: APScheduler with `--workers 1` to avoid duplicate fires.
|
||||
|
||||
---
|
||||
|
||||
@@ -244,4 +243,4 @@ docker-compose logs -f
|
||||
- **Wife**: Non-technical, will use web UI and email
|
||||
- **Children**: 2, eating habits vary (one OK with mushrooms)
|
||||
|
||||
Last updated: 2026-05-04 (Phase 1 complete, Phase 2 next)
|
||||
Last updated: 2026-05-04 (post adversarial review fixes)
|
||||
|
||||
+2
-2
@@ -36,7 +36,7 @@ FAMILY_EMAIL_1=you@example.com
|
||||
FAMILY_EMAIL_2=spouse@example.com
|
||||
|
||||
# Lucky California (for scraping)
|
||||
LUCKY_CA_URL=https://www.luckyncal.com
|
||||
LUCKY_CA_URL=https://luckysupermarkets.com
|
||||
|
||||
# AI Images (optional)
|
||||
AI_IMAGE_ENABLED=false
|
||||
@@ -217,7 +217,7 @@ docker-compose exec backend python -c "from app.database import SessionLocal; pr
|
||||
|
||||
```bash
|
||||
# Check Lucky California is accessible
|
||||
curl -I https://www.luckyncal.com
|
||||
curl -I https://luckysupermarkets.com
|
||||
|
||||
# Verify Playwright browser installed
|
||||
docker-compose exec backend python -c "from playwright.sync_api import sync_playwright; print('OK')"
|
||||
|
||||
+75
-21
@@ -31,7 +31,7 @@ Current meal kit services (Blue Apron → EveryPlate → HungryRoot → Sunbaske
|
||||
### Primary Goals
|
||||
1. **Weekly Meal Planning**: Automatically generate a 7-day meal plan each week
|
||||
2. **Grocery Integration**: Scrape Lucky California weekly ads and product catalog for sales/in-season items
|
||||
3. **Family Approval Workflow**: Send email to both adults with meal proposal, image, and details; one denial swaps the meal
|
||||
3. **Family Approval Workflow**: Send email to adults with meal proposal, image, and details; one denial swaps the meal
|
||||
4. **Shopping List Generation**: Create weekly shopping list grouped by Lucky California aisles, highlighting sales
|
||||
5. **Pantry Integration**: Allow users to specify items they have at home to incorporate into meal suggestions
|
||||
6. **Web UI**: Modern interface for non-technical family members to interact with meals, feedback, and recipes
|
||||
@@ -81,15 +81,21 @@ Current meal kit services (Blue Apron → EveryPlate → HungryRoot → Sunbaske
|
||||
- Calorie, budget, and health conscious eating
|
||||
- Food should be tasty but not overly expensive
|
||||
|
||||
### Family Members
|
||||
| Member | Role | Mushroom Preference |
|
||||
|--------|------|-------------------|
|
||||
| Adult 1 | Adult | Does NOT like mushrooms |
|
||||
| Adult 2 | Adult | Likes mushrooms |
|
||||
| Child 1 | Child | Does NOT like mushrooms |
|
||||
| Child 2 | Child | OK with mushrooms |
|
||||
|
||||
### Dietary Constraints
|
||||
- One adult likes mushrooms
|
||||
- One child is OK with mushrooms
|
||||
- Two adults and one child do NOT like mushrooms
|
||||
- 3 of 4 family members do NOT like mushrooms
|
||||
- No allergies
|
||||
|
||||
### Preference Signals
|
||||
- "Never suggest this ingredient" flags
|
||||
- Per-meal ratings (1-5 stars)
|
||||
- "Never suggest this ingredient" flags (per family, not per member)
|
||||
- Per-meal ratings (1-5 stars, per member)
|
||||
- Denial reasons (too expensive, looks boring, contains disliked ingredient, etc.)
|
||||
- Home pantry items to incorporate
|
||||
|
||||
@@ -105,31 +111,53 @@ Current meal kit services (Blue Apron → EveryPlate → HungryRoot → Sunbaske
|
||||
- Variety requirements (avoid sauce/ingredient repetition)
|
||||
- Home pantry items to use
|
||||
|
||||
2. Email sent to both adults containing:
|
||||
2. Email sent to all adult family members containing:
|
||||
- All 7 meals listed with images
|
||||
- Each meal has: Approve / Deny buttons (via email links or web UI)
|
||||
- Denial requires a reason selection or free-text
|
||||
- Each meal has: "View & Vote" link to web approval page
|
||||
- Email does NOT auto-approve on link click
|
||||
|
||||
3. Approval handling:
|
||||
- If both approve OR no response → meal confirmed
|
||||
- If either denies → meal swapped with alternative suggestion
|
||||
- Denied meals logged for learning
|
||||
3. Approval page flow (Web UI):
|
||||
- Adult clicks email link → lands on confirmation page
|
||||
- Page shows meal details, image, ingredients, estimated cost
|
||||
- Adult clicks "Approve" or "Deny"
|
||||
- Denial requires selecting a reason
|
||||
- Vote is recorded per-member (not per-household)
|
||||
|
||||
4. After approval deadline:
|
||||
4. Approval handling:
|
||||
- If majority of adults approve → meal confirmed
|
||||
- If any adult denies → meal swapped with alternative suggestion
|
||||
- Denial reason is recorded for learning
|
||||
- Explicit deadline: 48 hours from email send
|
||||
- After deadline: meals with insufficient responses auto-expire and are excluded
|
||||
|
||||
5. After approval period:
|
||||
- Final meal plan locked
|
||||
- Shopping list generated
|
||||
- Recipes made available in web UI
|
||||
|
||||
### Approval Token Security
|
||||
- Each email contains a unique, single-use token per family member
|
||||
- Tokens expire after 72 hours
|
||||
- Tokens can only be used once (marked USED after voting)
|
||||
- Email links lead to a confirmation page; actual vote is a POST
|
||||
|
||||
---
|
||||
|
||||
## 8. Technical Constraints
|
||||
|
||||
### Self-Hosting Requirements
|
||||
- Must run on local infrastructure (homelab, NUC, Synology, etc.)
|
||||
- Remote access via reverse proxy (Caddy or nginx)
|
||||
- No external cloud services except SendGrid for email
|
||||
- Remote access via reverse proxy with VPN or TLS
|
||||
- Primary access: local network only (VPN required for remote)
|
||||
|
||||
### Authentication & Authorization
|
||||
- **Admin endpoints** (`/api/admin/*`): VPN-only access
|
||||
- **Family web UI**: Session-based authentication (simple username/password)
|
||||
- **Email approval links**: Token-based, single-use, time-limited
|
||||
- No JWT; no OAuth
|
||||
|
||||
### Lucky California Integration
|
||||
- **URL**: https://luckysupermarkets.com (verified)
|
||||
- Primary: Scrape weekly ad and product catalog
|
||||
- Store scraped data locally
|
||||
- Respect robots.txt and rate limiting
|
||||
@@ -139,15 +167,41 @@ Current meal kit services (Blue Apron → EveryPlate → HungryRoot → Sunbaske
|
||||
- SendGrid for transactional email
|
||||
- HTML email templates with meal images
|
||||
- Plain text fallback for email clients that block images
|
||||
- Accessible: includes alt text for images, works with screen readers
|
||||
|
||||
### External Dependencies
|
||||
| Service | Purpose | Required |
|
||||
|---------|---------|----------|
|
||||
| SendGrid | Transactional email | Yes |
|
||||
| luckysupermarkets.com | Grocery scraping | Yes |
|
||||
| Recipe websites | Recipe images | Yes |
|
||||
| AI Image API (optional) | Fallback image generation | No |
|
||||
|
||||
---
|
||||
|
||||
## 9. Data Retention
|
||||
## 9. Accessibility (WCAG 2.1 AA)
|
||||
|
||||
### Web UI
|
||||
- All interactive elements keyboard accessible
|
||||
- Color contrast ratio ≥ 4.5:1 for normal text
|
||||
- Form inputs have visible labels
|
||||
- Error messages are descriptive and associated with inputs
|
||||
- Skip navigation links provided
|
||||
|
||||
### Email
|
||||
- HTML emails include meaningful alt text for all images
|
||||
- Plain text version provided as fallback
|
||||
- Links are descriptive (not "click here")
|
||||
- Font sizes are readable (minimum 14px equivalent)
|
||||
|
||||
---
|
||||
|
||||
## 10. Data Retention
|
||||
|
||||
### Stored Data
|
||||
- All recipes (scraped and manually added)
|
||||
- Meal plans (weekly history)
|
||||
- Approval/denial history with reasons
|
||||
- Per-member votes and denial history
|
||||
- Feedback (ratings, flags, pantry items)
|
||||
- Scraped grocery data (weekly refresh)
|
||||
|
||||
@@ -158,17 +212,17 @@ Current meal kit services (Blue Apron → EveryPlate → HungryRoot → Sunbaske
|
||||
|
||||
---
|
||||
|
||||
## 10. Success Metrics
|
||||
## 11. Success Metrics
|
||||
|
||||
1. **Adoption**: Family consistently uses the system weekly
|
||||
2. **Meal variety**: No more than 2 meals/week share the same sauce or primary protein
|
||||
3. **Cost efficiency**: Average cost per serving within 150% of equivalent grocery-store meal
|
||||
4. **Approval rate**: >80% of proposed meals approved without changes
|
||||
4. **Explicit approval rate**: >80% of proposed meals receive explicit approval (not silence)
|
||||
5. **Learning**: After 4 weeks, system should not propose previously denied meals
|
||||
|
||||
---
|
||||
|
||||
## 11. Future Considerations
|
||||
## 12. Future Considerations
|
||||
|
||||
- Twilio WhatsApp integration for wife who prefers messaging
|
||||
- Direct Lucky California online ordering
|
||||
|
||||
+158
-61
@@ -4,13 +4,29 @@
|
||||
|
||||
PostgreSQL 15+ with the following extensions:
|
||||
- `uuid-ossp` for UUID generation
|
||||
- `pg_trgm` for fuzzy text search (if needed)
|
||||
- `CITEXT` for case-insensitive text (ingredient names)
|
||||
|
||||
---
|
||||
|
||||
## 2. Tables
|
||||
## 2. Enums
|
||||
|
||||
### 2.1 `family_profile`
|
||||
```sql
|
||||
CREATE TYPE family_member_role_enum AS ENUM ('adult', 'child');
|
||||
CREATE TYPE meal_type_enum AS ENUM ('breakfast', 'lunch', 'dinner');
|
||||
CREATE TYPE meal_plan_status_enum AS ENUM ('draft', 'pending_approval', 'approved', 'locked');
|
||||
CREATE TYPE meal_plan_item_status_enum AS ENUM ('pending', 'approved', 'denied', 'swapped');
|
||||
CREATE TYPE approval_token_status_enum AS ENUM ('active', 'used', 'expired');
|
||||
CREATE TYPE denial_reason_enum AS ENUM ('too_expensive', 'boring', 'disliked_ingredient', 'cultural', 'other');
|
||||
CREATE TYPE never_suggest_reason_enum AS ENUM ('allergy', 'dislike', 'tried_too_much', 'other');
|
||||
CREATE TYPE scrape_status_enum AS ENUM ('started', 'success', 'failed');
|
||||
CREATE TYPE email_status_enum AS ENUM ('sent', 'delivered', 'failed', 'bounced');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Tables
|
||||
|
||||
### 3.1 `family_profile`
|
||||
|
||||
Primary household configuration.
|
||||
|
||||
@@ -23,18 +39,39 @@ Primary household configuration.
|
||||
| child_count | INTEGER | NOT NULL | Number of children |
|
||||
| dietary_notes | TEXT | | Free-text dietary notes |
|
||||
| budget_per_meal | NUMERIC(10,2) | DEFAULT 50.00 | Budget target per meal (in dollars) |
|
||||
| calorie_target | INTEGER | | Daily calorie target per adult |
|
||||
| calorie_target | INTEGER | | Daily calorie target per adult (aspirational) |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
### 2.2 `ingredient`
|
||||
**Constraints**:
|
||||
- `CHECK (adult_count + child_count = household_size)`
|
||||
|
||||
Master list of all ingredients.
|
||||
### 3.2 `family_member`
|
||||
|
||||
Individual family members for per-person voting and preferences.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| name | VARCHAR(200) | NOT NULL, UNIQUE | Ingredient name |
|
||||
| family_profile_id | UUID | FK → family_profile(id) ON DELETE CASCADE | |
|
||||
| name | VARCHAR(100) | NOT NULL | Member name |
|
||||
| email | VARCHAR(300) | UNIQUE per family | Email for notifications |
|
||||
| role | family_member_role_enum | NOT NULL | 'adult' or 'child' |
|
||||
| likes_mushrooms | BOOLEAN | DEFAULT FALSE | Dietary preference |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
**Unique constraint**: `(family_profile_id, email)`
|
||||
|
||||
### 3.3 `ingredient`
|
||||
|
||||
Master list of all ingredients. `name_lower` is used for case-insensitive matching.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| name | VARCHAR(200) | NOT NULL | Display name (e.g., "Carrots") |
|
||||
| name_lower | VARCHAR(200) | NOT NULL, UNIQUE | Lowercase for matching (e.g., "carrots") |
|
||||
| plural_name | VARCHAR(200) | | For shopping list grouping |
|
||||
| aisle | VARCHAR(100) | | Lucky California aisle |
|
||||
| typical_price | NUMERIC(10,2) | | Price per unit |
|
||||
@@ -42,9 +79,9 @@ Master list of all ingredients.
|
||||
| season_months | INTEGER[] | | Array of month numbers 1-12 |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
### 2.3 `recipe`
|
||||
### 3.4 `recipe`
|
||||
|
||||
All recipes in the system.
|
||||
All recipes in the system. Ingredients stored as JSONB for flexibility.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
@@ -56,14 +93,13 @@ All recipes in the system.
|
||||
| image_source | VARCHAR(50) | | 'scraped', 'ai_generated', 'manual' |
|
||||
| prep_time_minutes | INTEGER | | Prep time |
|
||||
| cook_time_minutes | INTEGER | | Cook time |
|
||||
| total_time_minutes | INTEGER | | Computed: prep + cook |
|
||||
| servings | INTEGER | NOT NULL | |
|
||||
| servings_scaled | INTEGER | | For scaling recipes |
|
||||
| servings | INTEGER | NOT NULL | Default servings |
|
||||
| servings_scaled | INTEGER | | Current scaled servings |
|
||||
| cuisine_tags | VARCHAR(50)[] | | Array: 'italian', 'asian', etc. |
|
||||
| dietary_tags | VARCHAR(50)[] | | Array: 'vegetarian', 'gluten_free', etc. |
|
||||
| protein_type | VARCHAR(50) | | 'chicken', 'beef', 'vegetarian', 'seafood' |
|
||||
| spice_level | INTEGER | CHECK (spice_level BETWEEN 1 AND 5) | 1=mild, 5=very spicy |
|
||||
| ingredients | JSONB | NOT NULL | [{ingredient_id, quantity, unit, is_optional}] |
|
||||
| ingredients | JSONB | NOT NULL | [{ingredient_id, name, quantity, unit, is_optional}] |
|
||||
| instructions | TEXT[] | NOT NULL | Array of step strings |
|
||||
| source_url | TEXT | | Original recipe URL if scraped |
|
||||
| scraped_at | TIMESTAMPTZ | | When originally scraped |
|
||||
@@ -71,7 +107,9 @@ All recipes in the system.
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
### 2.4 `meal_plan`
|
||||
**Note**: `total_time_minutes` is computed as `prep_time_minutes + cook_time_minutes` (not stored).
|
||||
|
||||
### 3.5 `meal_plan`
|
||||
|
||||
A generated weekly meal plan.
|
||||
|
||||
@@ -79,8 +117,8 @@ A generated weekly meal plan.
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| family_profile_id | UUID | FK → family_profile(id) | |
|
||||
| week_start_date | DATE | NOT NULL | Monday of the week |
|
||||
| status | VARCHAR(20) | NOT NULL, DEFAULT 'draft' | 'draft', 'pending_approval', 'approved', 'locked' |
|
||||
| week_start_date | DATE | NOT NULL | Monday of the week (ISO: 1=Mon, 7=Sun) |
|
||||
| status | meal_plan_status_enum | NOT NULL, DEFAULT 'draft' | |
|
||||
| approval_deadline | TIMESTAMPTZ | | When approval period ends |
|
||||
| total_estimated_cost | NUMERIC(10,2) | | Sum of all meal costs |
|
||||
| notes | TEXT | | Admin notes |
|
||||
@@ -89,21 +127,19 @@ A generated weekly meal plan.
|
||||
|
||||
**Unique constraint**: `(family_profile_id, week_start_date)`
|
||||
|
||||
### 2.5 `meal_plan_item`
|
||||
### 3.6 `meal_plan_item`
|
||||
|
||||
Individual meal within a plan.
|
||||
Individual meal within a plan. `day_of_week` uses ISO-8601 convention (1=Monday through 7=Sunday).
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| meal_plan_id | UUID | FK → meal_plan(id) ON DELETE CASCADE | |
|
||||
| recipe_id | UUID | FK → recipe(id) | |
|
||||
| day_of_week | INTEGER | NOT NULL, CHECK (day_of_week BETWEEN 0 AND 6) | 0=Monday, 6=Sunday |
|
||||
| meal_type | VARCHAR(20) | NOT NULL | 'breakfast', 'lunch', 'dinner' |
|
||||
| approval_status | VARCHAR(20) | DEFAULT 'pending' | 'pending', 'approved', 'denied', 'swapped' |
|
||||
| approval_token | UUID | UNIQUE, DEFAULT uuid_generate_v4() | Token for email approval links |
|
||||
| approval_token_expires | TIMESTAMPTZ | | |
|
||||
| denial_reason | VARCHAR(50) | | 'too_expensive', 'boring', 'disliked_ingredient', 'other' |
|
||||
| day_of_week | INTEGER | NOT NULL, CHECK (day_of_week BETWEEN 1 AND 7) | 1=Mon, 7=Sun (ISO-8601) |
|
||||
| meal_type | meal_type_enum | NOT NULL | 'breakfast', 'lunch', 'dinner' |
|
||||
| approval_status | meal_plan_item_status_enum | DEFAULT 'pending' | |
|
||||
| denial_reason | denial_reason_enum | | If denied |
|
||||
| denial_details | TEXT | | Free-text explanation |
|
||||
| estimated_cost | NUMERIC(10,2) | | Per serving cost |
|
||||
| used_pantry_items | UUID[] | | Home pantry items used |
|
||||
@@ -112,7 +148,38 @@ Individual meal within a plan.
|
||||
|
||||
**Unique constraint**: `(meal_plan_id, day_of_week, meal_type)`
|
||||
|
||||
### 2.6 `home_pantry`
|
||||
### 3.7 `meal_plan_vote`
|
||||
|
||||
Per-member votes on meal plan items. This is the key to the redesigned approval flow.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| meal_plan_item_id | UUID | FK → meal_plan_item(id) ON DELETE CASCADE | |
|
||||
| family_member_id | UUID | FK → family_member(id) ON DELETE CASCADE | |
|
||||
| vote | BOOLEAN | NOT NULL | TRUE=approve, FALSE=deny |
|
||||
| voted_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
**Unique constraint**: `(meal_plan_item_id, family_member_id)` — one vote per member per meal
|
||||
|
||||
### 3.8 `approval_token`
|
||||
|
||||
Single-use tokens for email approval links.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| meal_plan_item_id | UUID | FK → meal_plan_item(id) ON DELETE CASCADE | |
|
||||
| family_member_id | UUID | FK → family_member(id) ON DELETE CASCADE | |
|
||||
| token | VARCHAR(64) | NOT NULL, UNIQUE | Secure random token |
|
||||
| status | approval_token_status_enum | DEFAULT 'active' | |
|
||||
| expires_at | TIMESTAMPTZ | NOT NULL | Token expiration (72h from send) |
|
||||
| used_at | TIMESTAMPTZ | | When token was used |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
**Unique constraint**: `(meal_plan_item_id, family_member_id)`
|
||||
|
||||
### 3.9 `home_pantry`
|
||||
|
||||
Items the family has at home.
|
||||
|
||||
@@ -120,7 +187,7 @@ Items the family has at home.
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| family_profile_id | UUID | FK → family_profile(id) ON DELETE CASCADE | |
|
||||
| ingredient_id | UUID | FK → ingredient(id) | |
|
||||
| ingredient_id | UUID | FK → ingredient(id) ON DELETE SET NULL | |
|
||||
| quantity | NUMERIC(10,2) | | How much on hand |
|
||||
| unit | VARCHAR(50) | | e.g., "cans", "lb" |
|
||||
| expires_at | DATE | | Perishable expiry date |
|
||||
@@ -129,24 +196,25 @@ Items the family has at home.
|
||||
|
||||
**Unique constraint**: `(family_profile_id, ingredient_id)`
|
||||
|
||||
### 2.7 `feedback`
|
||||
### 3.10 `feedback`
|
||||
|
||||
Meal feedback and ratings.
|
||||
Meal feedback and ratings. One feedback row per meal, can be associated with a specific member.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| family_profile_id | UUID | FK → family_profile(id) | |
|
||||
| family_member_id | UUID | FK → family_member(id) ON DELETE SET NULL | |
|
||||
| meal_plan_item_id | UUID | FK → meal_plan_item(id) ON DELETE CASCADE | |
|
||||
| rating | INTEGER | CHECK (rating BETWEEN 1 AND 5) | 1-5 stars |
|
||||
| never_suggest | BOOLEAN | DEFAULT FALSE | Add to "never suggest" list |
|
||||
| denial_reason | VARCHAR(50) | | Same as meal_plan_item |
|
||||
| denial_reason | denial_reason_enum | | Same as meal_plan_item |
|
||||
| feedback_text | TEXT | | Free-text feedback |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
**Unique constraint**: `(meal_plan_item_id)` — one feedback per meal
|
||||
**Note**: Multiple feedback rows can exist for the same meal (one per family member).
|
||||
|
||||
### 2.8 `never_suggest`
|
||||
### 3.11 `never_suggest`
|
||||
|
||||
Global "never suggest" flags per family.
|
||||
|
||||
@@ -154,19 +222,20 @@ Global "never suggest" flags per family.
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| family_profile_id | UUID | FK → family_profile(id) ON DELETE CASCADE | |
|
||||
| ingredient_id | UUID | FK → ingredient(id) | Optionally block specific ingredients |
|
||||
| recipe_id | UUID | FK → recipe(id) | Or block entire recipes |
|
||||
| reason | VARCHAR(50) | | 'allergy', 'dislike', 'tried_too_much', 'other' |
|
||||
| ingredient_id | UUID | FK → ingredient(id) ON DELETE CASCADE | Optionally block specific ingredients |
|
||||
| recipe_id | UUID | FK → recipe(id) ON DELETE CASCADE | Or block entire recipes |
|
||||
| reason | never_suggest_reason_enum | | |
|
||||
| notes | TEXT | | |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
### 2.9 `grocery_item`
|
||||
### 3.12 `grocery_item`
|
||||
|
||||
Scraped items from Lucky California.
|
||||
Scraped items from Lucky California. Links to `ingredient` table.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| ingredient_id | UUID | FK → ingredient(id) ON DELETE SET NULL | Links to master ingredient list |
|
||||
| name | VARCHAR(300) | NOT NULL | Product name |
|
||||
| brand | VARCHAR(200) | | Brand name |
|
||||
| current_price | NUMERIC(10,2) | | Current sale price |
|
||||
@@ -182,9 +251,7 @@ Scraped items from Lucky California.
|
||||
| scraped_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
| scraped_url | TEXT | | Source URL |
|
||||
|
||||
**Index**: `CREATE INDEX idx_grocery_item_is_on_sale ON grocery_item(is_on_sale) WHERE is_on_sale = TRUE`
|
||||
|
||||
### 2.10 `scrape_log`
|
||||
### 3.13 `scrape_log`
|
||||
|
||||
Scraping operation history.
|
||||
|
||||
@@ -193,14 +260,14 @@ Scraping operation history.
|
||||
| id | UUID | PK | |
|
||||
| source | VARCHAR(50) | NOT NULL | 'lucky_california', 'recipe_site' |
|
||||
| scrape_type | VARCHAR(50) | NOT NULL | 'weekly_ad', 'product_catalog', 'recipe' |
|
||||
| status | VARCHAR(20) | NOT NULL | 'started', 'success', 'failed' |
|
||||
| status | scrape_status_enum | NOT NULL | |
|
||||
| items_scraped | INTEGER | DEFAULT 0 | |
|
||||
| error_message | TEXT | | |
|
||||
| started_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
| completed_at | TIMESTAMPTZ | | |
|
||||
| duration_seconds | INTEGER | | |
|
||||
|
||||
### 2.11 `email_log`
|
||||
### 3.14 `email_log`
|
||||
|
||||
Email sending history.
|
||||
|
||||
@@ -213,71 +280,101 @@ Email sending history.
|
||||
| meal_plan_id | UUID | FK → meal_plan(id) | Related meal plan |
|
||||
| meal_plan_item_id | UUID | FK → meal_plan_item(id) | Optional: specific meal |
|
||||
| sendgrid_message_id | VARCHAR(100) | | SendGrid message ID |
|
||||
| status | VARCHAR(20) | NOT NULL | 'sent', 'delivered', 'failed', 'bounced' |
|
||||
| status | email_status_enum | NOT NULL | |
|
||||
| error_message | TEXT | | |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
| delivered_at | TIMESTAMPTZ | | |
|
||||
|
||||
---
|
||||
|
||||
## 3. Indexes
|
||||
## 4. Indexes
|
||||
|
||||
### 3.1 Primary Indexes
|
||||
### 4.1 Primary Indexes
|
||||
All PKs have default B-tree indexes.
|
||||
|
||||
### 3.2 Foreign Key Indexes
|
||||
### 4.2 Foreign Key Indexes
|
||||
```sql
|
||||
CREATE INDEX idx_family_member_profile ON family_member(family_profile_id);
|
||||
CREATE INDEX idx_recipe_family_profile ON recipe(family_profile_id);
|
||||
CREATE INDEX idx_meal_plan_family_profile ON meal_plan(family_profile_id);
|
||||
CREATE INDEX idx_meal_plan_item_meal_plan ON meal_plan_item(meal_plan_id);
|
||||
CREATE INDEX idx_meal_plan_item_recipe ON meal_plan_item(recipe_id);
|
||||
CREATE INDEX idx_meal_plan_vote_item ON meal_plan_vote(meal_plan_item_id);
|
||||
CREATE INDEX idx_meal_plan_vote_member ON meal_plan_vote(family_member_id);
|
||||
CREATE INDEX idx_approval_token_item ON approval_token(meal_plan_item_id);
|
||||
CREATE INDEX idx_approval_token_member ON approval_token(family_member_id);
|
||||
CREATE INDEX idx_home_pantry_family_profile ON home_pantry(family_profile_id);
|
||||
CREATE INDEX idx_home_pantry_ingredient ON home_pantry(ingredient_id);
|
||||
CREATE INDEX idx_feedback_family_profile ON feedback(family_profile_id);
|
||||
CREATE INDEX idx_feedback_member ON feedback(family_member_id);
|
||||
CREATE INDEX idx_feedback_meal_plan_item ON feedback(meal_plan_item_id);
|
||||
CREATE INDEX idx_never_suggest_family_profile ON never_suggest(family_profile_id);
|
||||
CREATE INDEX idx_grocery_item_sale ON grocery_item(is_on_sale) WHERE is_on_sale = TRUE;
|
||||
CREATE INDEX idx_grocery_item_ingredient ON grocery_item(ingredient_id);
|
||||
CREATE INDEX idx_grocery_item_is_on_sale ON grocery_item(is_on_sale) WHERE is_on_sale = TRUE;
|
||||
```
|
||||
|
||||
### 3.3 Full-Text Search Indexes
|
||||
### 4.3 Full-Text Search Indexes
|
||||
```sql
|
||||
CREATE INDEX idx_ingredient_name_fts ON ingredient USING gin(to_tsvector('english', name));
|
||||
CREATE INDEX idx_ingredient_name_lower_fts ON ingredient USING gin(to_tsvector('english', name_lower));
|
||||
CREATE INDEX idx_recipe_name_fts ON recipe USING gin(to_tsvector('english', name));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Data Migration Strategy
|
||||
## 5. Day of Week Convention
|
||||
|
||||
### 4.1 Initial Schema
|
||||
Use SQLAlchemy or Alembic for schema management.
|
||||
**ISO-8601 standard** (1=Monday through 7=Sunday):
|
||||
- 1 = Monday
|
||||
- 2 = Tuesday
|
||||
- 3 = Wednesday
|
||||
- 4 = Thursday
|
||||
- 5 = Friday
|
||||
- 6 = Saturday
|
||||
- 7 = Sunday
|
||||
|
||||
### 4.2 Future Migrations
|
||||
- Alembic for version-controlled migrations
|
||||
- All migrations must be reversible
|
||||
This applies to `meal_plan_item.day_of_week`.
|
||||
|
||||
### 4.3 Seed Data
|
||||
- Basic ingredient list pre-populated
|
||||
- Sample recipes for initial testing (5-10 meals)
|
||||
- Default family profile template
|
||||
**Note**: PostgreSQL's `EXTRACT(DOW FROM date)` returns 0=Sunday, 6=Saturday. Convert accordingly.
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Retention
|
||||
## 6. Data Migration Strategy
|
||||
|
||||
### 6.1 Initial Schema
|
||||
Use Alembic for version-controlled migrations.
|
||||
|
||||
### 6.2 Migration Policy
|
||||
- All schema changes must go through Alembic migrations
|
||||
- Migrations must be reversible where possible
|
||||
- No `Base.metadata.create_all()` in application startup code
|
||||
|
||||
### 6.3 Seed Data
|
||||
- Enum types created first
|
||||
- Basic ingredients pre-populated (50-100 common items)
|
||||
- Sample recipes for initial testing (5-10 meals)
|
||||
- Default family profile with 2 adult members
|
||||
|
||||
---
|
||||
|
||||
## 7. Data Retention
|
||||
|
||||
| Data Type | Retention | Action After Expiry |
|
||||
|----------|-----------|---------------------|
|
||||
|-----------|-----------|---------------------|
|
||||
| Meal plans | 12 weeks | Archive to JSON, delete rows |
|
||||
| Feedback | Indefinite | Keep for learning |
|
||||
| Scraped grocery items | 2 weeks | Delete old items |
|
||||
| Scrape logs | 30 days | Delete old logs |
|
||||
| Email logs | 90 days | Delete old logs |
|
||||
| Never-suggest | Indefinite | Keep |
|
||||
| Used approval tokens | 7 days | Delete after vote processed |
|
||||
| Expired approval tokens | 30 days | Delete |
|
||||
|
||||
---
|
||||
|
||||
## 6. Row-Level Security (Future)
|
||||
## 8. Row-Level Security (Future)
|
||||
|
||||
If multi-family support is added:
|
||||
- RLS on all tables
|
||||
- Policies based on `family_profile_id`
|
||||
- Backend enforces tenant isolation
|
||||
- Current implementation: single-family, no RLS needed
|
||||
|
||||
Reference in New Issue
Block a user