admin 8ad4ef67a9 feat(ui): bulk pantry add + plan-the-week button (Sprint 6 F3+F4)
F3 — Bulk 'add checked to pantry' on ShoppingList (the audit's F3 /
H7 finding). ShoppingList already had a 'checked' Set keyed on
ingredient_id and persisted to localStorage — that selection state
is the natural substrate for a bulk action.

Backend (POST /api/pantry/bulk):
- New endpoint that accepts {items: HomePantryCreate[]} and returns
  HomePantryBulkResult with per-item status (added / updated /
  skipped) and totals. Each item follows the same upsert semantics
  as POST /api/pantry (insert or overwrite qty/unit/expires_at).
- Items with an unknown ingredient id are reported as 'skipped'
  with reason='Unknown ingredient' rather than aborting the batch.
  Per-item failure is the chosen model (partial-success) so the
  user gets a precise count of what actually went in.
- New Pydantic schemas: HomePantryBulkCreate, HomePantryBulkResult,
  HomePantryBulkResultItem.

Frontend:
- mealPlannerApi.pantry.addBulk(items) is the API binding.
- ShoppingList gets a new 'Add N to pantry' primary button (next
  to the existing Reset button) that appears when checked.size > 0.
  Click → POST /api/pantry/bulk → toast shows 'added X, updated Y,
  skipped Z' counts. On success, only the items that actually
  landed in the pantry are removed from the checked set; skipped
  items stay checked so the user can see what failed.
- Disabled state with 'Adding…' label while the request is in
  flight; button text shows the count dynamically (matches the
  F4 design language: tell the user what they're about to do).

F4 — Plan the whole week (the audit's F4 / H7 finding).

Backend (POST /api/meals/{id}/fill-empty-slots):
- New endpoint that takes {meal_types: [str, ...]} and fills every
  empty slot in the plan whose meal_type is in the request. Per-day
  iteration (1-7) per meal_type, skipping already-occupied slots.
  Recipe selection: prefer un-used, fall back to any (same as the
  existing generate-item).
- Per-slot failure model: never aborts mid-batch. Returns
  FillEmptySlotsResult { filled: [{day, meal_type, item}],
  failed: [{day, meal_type, reason}] }. Invalid meal_types
  (e.g. 'brunch') return immediately with a single FailedSlot
  explaining why.
- Same approval_status=pending semantics as generate-item.

Frontend:
- mealPlannerApi.meals.fillEmptySlots(planId, mealTypes) is the
  API binding.
- New 'Plan the week' button on the Dashboard header (next to the
  week-nav control from Sprint 5). Primary color, Sparkles icon,
  ChevronDown caret indicates a dropdown. Disabled + spinner
  ('Planning…') while the request runs.
- Dropdown has two options: 'Dinners only' (sends
  meal_types=['dinner']) and 'All meals' (sends
  meal_types=['breakfast','lunch','dinner']). Each option has a
  one-line secondary label explaining the action.
- Toast on success: 'Planned N meal slots' (full) or 'Planned N
  of M meal slots — X failed (e.g. <reason>)' (partial). The
  query is then invalidated so the new slots show up.

Files: backend/app/api/meals.py, backend/app/api/pantry.py,
backend/app/schemas/__init__.py, frontend/src/api/index.ts,
frontend/src/pages/Dashboard.tsx, frontend/src/pages/ShoppingList.tsx.

Build: tsc 0 errors, vite 0 errors. Bundle +3.6KB (the new code
fits in the existing chunk).
Curl smoke on local dev DB confirms both new endpoints behave as
designed: /api/pantry/bulk returns proper skipped count for
unknown ingredients, /api/meals/{id}/fill-empty-slots returns
the partial-success result for the dinners-only call.
2026-06-04 14:00:31 -07:00

Meal Planner

Self-hosted meal planning system that integrates with Lucky California grocery store, sends weekly meal proposals via email to family members, generates shopping lists, and learns from feedback.

Background

This project was born out of frustration with meal kit services (Blue Apron → EveryPlate → HungryRoot → Sunbasket) that:

  • Escalate costs to 3x ingredient markup
  • Fall into repetitive meal rhythms
  • Force users to log into apps to manage selections
  • Don't integrate with home pantry items

Features

  • Grocery Integration: Scrapes Lucky California weekly ads and sales
  • Family Approval Workflow: Email proposals with approve/deny; one denial swaps the meal
  • Shopping List Generation: Weekly list grouped by store aisles, highlighting sales, with interactive checkboxes to track purchased items
  • Pantry Integration: Specify home items to incorporate into suggestions
  • Web UI: Modern interface for the whole family
  • Learning: Feedback-based meal recommendations, with weekly auto-discovery of new recipes from external APIs when family preferences are signaled
  • Recipe Images: Scraped from public recipe sites, AI fallback available
  • Unit Conversion: Converts recipe quantities (cups, tbsp, lb) to grocery units for accurate cost estimates

Architecture

  • Backend: Python/FastAPI
  • Database: PostgreSQL
  • Frontend: React + Tailwind CSS
  • Email: SendGrid
  • Hosting: Docker Compose with nginx reverse proxy

Documentation

Quick Start

# Clone and start
docker-compose up -d

# View logs
docker-compose logs -f

# Stop
docker-compose down

Family Profile

Household: 2 adults, 2 children

  • One adult likes mushrooms, one child OK with them
  • Three family members do NOT like mushrooms
  • No allergies
  • Calorie, budget, and health conscious eating

Approval Workflow

  1. System generates 7-day meal plan based on sales, dietary constraints, budget, variety
  2. Email sent to both adults with meal previews
  3. One denial = meal swapped; no denials = auto-approved
  4. Shopping list generated after approval

Tech Stack

Component Technology
Backend Python 3.11, FastAPI
Database PostgreSQL 15
Frontend React 18, TypeScript, Tailwind
Scraping Playwright, BeautifulSoup
Email SendGrid
Hosting Docker Compose, nginx
S
Description
No description provided
Readme
46 MiB
Languages
Python 72.5%
TypeScript 25.9%
JavaScript 0.5%
PLpgSQL 0.4%
CSS 0.3%
Other 0.2%