Commit Graph
155 Commits
Author SHA1 Message Date
adminandClaude Sonnet 4.6 35f736a052 fix(planner): correct set_size to 3 dinners and switch cost filter to per-serving
- set_size 21→3, top_k 20→10: generate 3 weekly dinners not full 3×7 matrix
- All 3 items assigned MealType.DINNER on Mon/Wed/Fri
- RecipeCost gains servings field + cost_per_serving property
- compute_recipe_cost accepts servings param (default 4)
- filter.py gates on cost_per_serving instead of total_cost
- max_meal_cost 500→50 (now a meaningful $/serving threshold)
- Email displays ~$X/serving instead of inflated raw total
- select_set: candidate_pool uses max(top_k, set_size) to prevent
  combinations(n<set_size) returning empty iterator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 07:20:17 -07:00
admin 16069bfa92 chore: remove graphify artifacts from repo and add to .gitignore 2026-05-18 17:43:40 -07:00
admin 019f9020ad tests: fix suite-wide collection and failures
- config: switch Settings to ConfigDict(extra='ignore') so extra env vars
  (spoonacular_api_key, SWIFTLY_BEARER_TOKEN) don't crash import.
  Remove deprecated class Config.
- email: wrap SendGrid imports in try/except so the module loads without
  the optional dependency. Update test_email_backend to patch Mail/RepyTo.
- planner_select: default PlannerConfig.set_size=21 (3 meals/day × 7) is
  way too large for the unit test assertion that checks 3-recipe diversity.
  Introduced _CFG_3 with set_size=3 and applied to all tests.
- Delete stale test_matcher.py importing removed functions.

Full suite: 46 passed, 74 skipped (Postgres), 0 failed, 120 collected.
2026-05-18 17:43:30 -07:00
admin e92cc3a074 scrapers: drop Swiftly products with unparseable price
Products missing a parseable sale or regular price would previously yield
a GroceryItem with current_price=None. That broke the downstream matcher
(ingredient typical_price is non-null) and cluttered the table.

Added a guard in map_product() to return None when both reg_price and
sale_price are None. Fixes test_map_product_returns_none_for_unparseable.
2026-05-18 17:22:29 -07:00
admin 69acd70188 fix(pantry): make public ingredient endpoint idempotent
CI / backend (pytest + alembic) (push) Has been cancelled
CI / frontend (build) (push) Has been cancelled
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
admin 54b3e785b3 feat: allow adding pantry items by text input with auto-ingredient creation
CI / backend (pytest + alembic) (push) Has been cancelled
CI / frontend (build) (push) Has been cancelled
- backend: expose POST /api/ingredients on public router so frontend can create ingredients without admin token
- frontend/api: point listIngredients and createIngredient to /api/ingredients
- frontend/pantry: replace ingredient dropdown with searchable text input + fuzzy matching + auto-create
2026-05-17 21:31:37 -07:00
admin 986968b93d fix: improve mobile layout on meals page and navigation
- Dashboard: stack days vertically on mobile and suppress empty meal slots
- Nav: allow wrapping on narrow screens
- MealDetail: responsive hero sizing and padding
2026-05-17 21:22:37 -07:00
admin 879768f72b fix: shopping list ingredient UUID parsing for prices/aisles; feat: interactive checkboxes with localStorage persistence 2026-05-17 13:12:18 -07:00
admin 2f6ab006de fix: enrich ingredient names in shopping list API
Same root cause as meal detail: recipe JSONB stores ingredient_id but
not name. Shopping list now looks up names from the Ingredient table
before aggregating quantities, so items show "3 cups onion" instead
of "Unknown".
2026-05-15 14:21:51 -07:00
admin 2708cc4bdd fix: enrich ingredient names in meal detail API
Recipe JSONB stores ingredient_id but not name. GET /api/meals/items/{id}
now queries the Ingredient table and injects names into the response so
the frontend displays "3 cups onion" instead of just "3 cups".
2026-05-15 14:11:36 -07:00
admin 7b5e65087e feat: 21 meals per week (3/day) + approve/deny + generate single meal
Backend:
- Planner config: set_size=21 (was 3), top_k=30 (was 20)
- generate.py: distribute 21 recipes across 7 days × 3 meal types
- meals.py: add POST /items/{id}/approve, /items/{id}/deny
- meals.py: add POST /{plan_id}/generate-item for empty slots

Frontend:
- Dashboard: Approve/Deny buttons on pending meal cards
- Dashboard: Generate button in empty meal slots
- API client: approveItem, denyItem, generateItem methods

Build: TypeScript compiles clean, Python syntax verified.
2026-05-15 11:44:04 -07:00
admin ddcdb962ca fix: atomic SQL swap for meal move endpoint
Replace two-step ORM update with single UPDATE ... CASE statement.
Eliminates IntegrityError from SQLAlchemy flush order violating the
unique constraint (meal_plan_id, day_of_week, meal_type).
2026-05-14 15:56:47 -07:00
admin 301e984336 feat: drag-and-drop meal scheduling + 7-day grid 2026-05-14 15:49:43 -07:00
admin c21741dd56 fix: move orchestrate endpoints out of admin router
- Create backend/app/api/orchestrate.py — new router for workflow steps
  (scrape, generate, email, reminder, deadline, finalize) without admin auth.
- Remove orchestrate endpoints from backend/app/api/admin.py.
- Register orchestrate router in main.py under /api/orchestrate.
- Update frontend api/index.ts to call /orchestrate/{step} instead of
  /admin/orchestrate/{step}.

This lets family members trigger vote emails without an admin bearer token.
2026-05-14 15:36:20 -07:00
admin f61515beff fix: wire up 'Send Vote Email' button to POST /admin/orchestrate/email
- Add triggerOrchestrate() to API client calling POST /admin/orchestrate/{step}
- Replace dead <button> in Dashboard with VoteEmailButton component:
  onClick calls triggerOrchestrate('email'), shows toast spinner + success/error
2026-05-14 13:59:29 -07:00
admin 6323eadc86 fix: test-email actually sends; vote page pre-checks existing votes; lowercase MealPlanItemStatus everywhere
- /api/admin/test-email now calls get_email_backend().send() instead of only logging.
- /api/meals/vote/{id} GET now queries MealPlanVote and renders 'already voted' confirmation if found.
- api/meals.py: fix remaining uppercase MealPlanItemStatus enum ref (DENIED, APPROVED, PENDING).
- Fixes the 'all meals show as pending' status regression and the 'Error: Already voted' bug.
2026-05-14 12:17:44 -07:00
admin 26d9985433 feat: remove login requirements for internal home-network use
- backend/app/security.py: require_session() now auto-authenticates by
  returning the first family_profile_id from the DB. No cookie or password
  needed. Falls back to "bootstrap" sentinel if no FamilyProfile exists.
  Admin routes (require_admin) still protected by bearer token.
- frontend/src/api/index.ts: removed 401→/login redirect interceptor
- frontend/src/App.tsx: removed Sign out button, removed /login route and
  Login page import
- Login page kept on disk (unused) for potential future re-enablement
2026-05-14 11:27:09 -07:00
admin 6a0c9d0c4e feat: full UI redesign with design system, Nielsen heuristics compliance
- Install lucide-react, framer-motion, react-hot-toast, clsx, tailwind-merge
- Custom Tailwind config: semantic color tokens, Inter font, shadow scale,
  border radius scale, custom animations (fadeIn, slideUp, shimmer)
- Shared component library: Button, Badge, Card, Input, Select, Textarea,
  EmptyState, Skeleton, LoadingSpinner
- Global CSS with @layer components (.btn, .card, .input, .badge, .skeleton)
- Toast notification system via react-hot-toast + showToast utility
- ErrorBoundary wrapper for graceful error recovery
- Redesigned navigation: sticky, active state indicators, Lucide icons
- Dashboard: hero header, today highlighting, scrollable week grid,
  redesigned meal cards, empty states, skeleton loading
- Meal Detail: hero image with gradient overlay, metadata row with icons,
  Lucide star rating, edit-existing-feedback flow
- Pantry: inline add form, search/filter, visual quantity badges,
  expiry warnings, confirmation dialogs
- Shopping List: gradient summary cards, aisle grouping with badges,
  sale strikethrough pricing, empty state
- Login: centered card with icon, Input component, Button component
- All old gray/blue utility classes migrated to new surface/primary tokens
- TypeScript clean, production build passes
2026-05-14 10:26:53 -07:00
admin f7ed10651b feat: Phase 8 Feedback UI + API endpoints
- New backend/app/api/feedback.py: GET/POST for meal_plan_item feedback
- MealDetail.tsx: star rating, never-suggest checkbox, reason dropdown,
  free-text comments, displays saved feedback
- frontend/src/api/index.ts + types: feedback API + TypeScript interface
- backend/app/schemas/__init__.py: model_validator maps qty→quantity for
  RecipeIngredient (fixes Pydantic validation on recipe JSONB)
- docs/HANDOFF.md: mark Phase 8 complete, update file map and date
2026-05-14 09:54:25 -07:00
adminandClaude Sonnet 4.6 e618b2bd5a docs: update HANDOFF for 2026-05-12 session
Spoonacular enrichment, LLM matcher, plural normalization, email confirmed polished.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 11:43:34 -07:00
adminandClaude Sonnet 4.6 b52276056c fix: cast qty/unit to str before html.escape in vote email shopping preview
Recipe ingredient qty fields in JSONB are stored as floats (e.g. 1.5),
not strings. html.escape() requires str input — AttributeError otherwise.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 11:40:55 -07:00
adminandClaude Sonnet 4.6 dbc26bcc30 feat: LLM-powered second-pass ingredient matcher + matcher improvements
Matcher improvements (matcher.py):
- Plural normalization: 'tortillas'→'tortilla', 'thighs'→'thigh' so
  subset recall check works without stemmer
- Precision floor lowered 0.45→0.30: allows 'Bacon'→'Wright Brand Bacon'
  (1/3=0.33) while exclusion words still block category contaminants
- _EXCLUSION_WORDS now normalized through same singularizer for consistency

LLM second-pass (llm_matcher.py):
- run_llm_match_job(): for each still-unmatched ingredient, collects top-12
  candidates from grocery catalog ranked by fuzzy×precision (same metric as
  AUTO matcher), then asks Ollama to pick the best match
- Candidate scoring: combined = (partial_token_sort_ratio/100) × precision
  ensures "McCormick Black Pepper" outranks "Dr Pepper" for 'Black Pepper'
- Stores picks as source='auto_llm' (confidence=0.750)
- Ollama Cloud endpoint: https://ollama.com/v1, model: kimi-k2.6:cloud

Migration 0010: adds 'auto_llm' to ingredient_match_source_enum

Config: OLLAMA_BASE_URL / OLLAMA_API_KEY / OLLAMA_MODEL settings
Docker-compose: wires all three Ollama + Spoonacular env vars to backend/scheduler
Scraper service: calls run_llm_match_job after run_match_job on every scrape

Results: AUTO matcher went from 36→25 unmatched (plural normalization fix),
LLM added 3 more (Black Pepper, Zucchini, Chicken Thighs).
Remaining 22 are genuine Lucky CA catalog gaps (standalone olive oil,
dried spices, etc. not in Swiftly weekly ad).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:37:58 -07:00
adminandClaude Sonnet 4.6 b03e7f8070 feat: Spoonacular recipe enrichment — images + descriptions for 25/30 recipes
- Added SPOONACULAR_API_KEY to docker-compose.yml (backend + scheduler)
- scripts/enrich_recipes_spoonacular.py: searches Spoonacular by recipe name,
  updates image_url and description; 402 quota guard exits cleanly
- 25 of 30 null recipes now enriched; 5 remain (quota exhausted for today)
- Remaining: Caprese Pasta, Creamy Tuscan Chicken, Sheet-Pan Chicken Thighs,
  Loaded Veggie Quesadillas, Zucchini and Spinach Frittata

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 06:56:14 -07:00
adminandClaude Sonnet 4.6 a98f0dc1cb docs: update HANDOFF.md for 2026-05-10 session
Covers all changes from this session:
- MVP login + auth gating (Login page, 401 interceptor, Sign out)
- nginx DNS resolver fix + port 8081
- Vote email: ingredient list + collapsible cooking steps
- Shopping list: per-meal sections, current_price fix
- Matcher full rewrite: ingredient-centric, precision×recall scoring,
  exclusion words, min precision floor, exact-name fast path, limit 100
- Scraper: save priceless produce items (garlic, lime, etc.)
- Infrastructure notes: DB user, module caching, admin API auth header
- Next moves: Spoonacular enrichment + Ollama LLM matcher

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-10 20:55:27 -07:00
adminandClaude Sonnet 4.6 2373883fe7 fix: exact-name fast path in matcher + save priceless produce in scraper
Scraper: remove price guard in map_product so produce items without a
catalog price (e.g. Fresh Garlic, Lime sold by weight) are saved to
grocery_item with current_price=NULL rather than skipped.

Matcher:
- Add exact-name fast path: build a lowercase-trimmed name→index map
  and skip fuzzy search entirely when the ingredient name matches a
  grocery item exactly. Lime → Lime (confidence 1.0), Garlic → Fresh
  Garlic from fuzzy (confidence 1.0).
- Add exclusion words: juice, gelatin to prevent beverage/dessert
  products from matching cooking ingredients.
- Increase fuzzy candidate limit 20→100 so exact-name items buried in
  large tie groups are not missed.
- Add 'juice' to exclusion: prevents '100% Lime Juice' from winning
  over plain 'Lime'.

Result: all recipe ingredients now match correct Lucky CA products or
show '—' (no match); zero category cross-contamination remaining.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-10 20:17:03 -07:00
adminandClaude Sonnet 4.6 d7a3f5c081 fix: matcher exclusion words + precision floor for clean grocery matching
- Add exclusion words: soda, rotisserie, tuna/tonno/salmon/sardine/anchovy
  to prevent beverages, prepared poultry, and seafood-in-oil from matching
  raw cooking ingredients
- Add min precision floor (0.45): grocery sig-word count must be ≤ 2× the
  ingredient's sig-word count, catching long branded products that pass
  the word-overlap recall check but are clearly wrong category matches
  (e.g. "Garlic Herb Rotisserie Chicken" precision=0.25 now rejected)

Result: all previously wrong matches now show '—' (no match) rather than
a wrong product; correct matches unchanged

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-10 12:00:19 -07:00
adminandClaude Sonnet 4.6 ac2b575f6b fix: rewrite matcher as ingredient-centric with precision×recall scoring
- Flip matching direction: iterate ingredients, search grocery items
  (previously: iterate grocery items → false positives from partial word
  overlap, e.g. Pampers Wipes matched Ginger Fresh via the word "Fresh")
- Score = partial_token_sort_ratio × (ingredient_sig / grocery_sig_words)
  — precision term penalises long branded products where the ingredient
  word appears incidentally ("Vermicelli, Garlic & Olive Oil" now scores
  lower than a pure olive oil SKU)
- 100% recall guard: every significant ingredient word must appear in the
  grocery name (eliminates cross-category noise completely)
- Stop-word list strips generic qualifiers so "boneless skinless" in an
  ingredient name doesn't block "Chicken Thighs Boneless" in the grocery
- ON CONFLICT DO NOTHING preserves manual matches on re-run

Benchmark on today's Lucky CA weekly ad (10,965 items):
  Before: ~25% correct (Pampers→Ginger, Red Wine→Bell Pepper, etc.)
  After:  ~80% correct; remaining misses are data gaps (Lucky has no
  standalone garlic or olive oil in this week's ad)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-10 11:38:59 -07:00
adminandClaude Sonnet 4.6 aeed2a4dc0 feat: add cooking instructions to vote email; shopping list grouped by meal; fix current_price field
- Vote email: recipe cards now include a collapsible <details> block with
  numbered cooking steps (recipe.instructions ARRAY)
- Shopping list email: ingredients now grouped under each meal heading
  instead of a flat deduplicated list
- step_finalize: fix grocery price lookup (.price -> .current_price)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-10 09:55:52 -07:00
admin 59a15a2c21 fix: nginx DNS resolver, port 8081, seed script with real family data 2026-05-09 20:50:20 -07:00
adminandClaude Sonnet 4.6 bc85b9b617 fix: step_finalize — resolve ingredient names and prices in shopping list email
Preloads Ingredient names from the DB (ingredients JSONB has no name field),
deduplicates by ingredient name, looks up top-confidence IngredientGroceryMatch
per ingredient, and renders a rich 4-column HTML table (Ingredient | Qty | Unit |
At Lucky | Price) with an estimated total.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 20:48:46 -07:00
adminandClaude Sonnet 4.6 a03152e51a fix: resolve ingredient names from Ingredient table in email template
recipe.ingredients JSONB has ingredient_id but no name field; preload
names in a single bulk query before the per-member loop so ing_rows,
shopping preview, and cost lookup all render real ingredient names.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 14:47:48 -07:00
adminandClaude Sonnet 4.6 0739a688dd feat: enrich proposal email with ingredients, cost, shopping preview
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 13:40:05 -07:00
admin 43ee581931 Merge branch 'feature/mvp-login' 2026-05-09 12:48:31 -07:00
admin 76638016c1 feat: one-time family profile seed script 2026-05-09 12:47:25 -07:00
admin fe64f0ead4 feat: login page, 401 interceptor, nav sign-out 2026-05-09 12:36:40 -07:00
admin 458c0361dc docs: MVP login implementation plan 2026-05-09 11:46:31 -07:00
admin 0d70fb118d docs: refresh HANDOFF + ORIENTATION for Phase 6 completion 2026-05-08 21:46:04 -07:00
admin aea47d8365 fix: escape member.name in step_email; add all-voted reminder test 2026-05-08 21:40:00 -07:00
adminandClaude Sonnet 4.6 3b28ad0e1c fix: html-escape recipe/ingredient names in email templates (#P5-a, #P5-b)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 21:37:19 -07:00
admin 9a484d39c3 feat: wire step_reminder into runner, admin, and scheduler (Fri 16:00 PT) 2026-05-08 21:29:53 -07:00
adminandClaude Sonnet 4.6 d002485c10 feat: step_reminder — 1-hour pre-deadline nudge for non-voters
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 18:00:16 -07:00
adminandClaude Sonnet 4.6 5a41402644 feat: wire SendGridEmailBackend with from_email/reply_to settings
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 12:12:05 -07:00
admin 63ad306a61 feat: migration 0009 — add reminded_at to weekly_run 2026-05-08 06:48:36 -07:00
admin 61a5578b71 chore: ignore .worktrees directory 2026-05-08 06:12:10 -07:00
admin 7d34b57812 docs: Phase 6 SendGrid implementation plan 2026-05-07 13:12:31 -07:00
admin a68f0be414 docs: Phase 6 SendGrid design spec 2026-05-07 12:50:38 -07:00
admin b551707267 docs: refresh HANDOFF + ORIENTATION for Phase 5 completion 2026-05-07 11:09:41 -07:00
adminandClaude Sonnet 4.6 cc8ece22b3 feat: add scheduler container to docker-compose
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 06:47:00 -07:00
adminandClaude Sonnet 4.6 7972ecb948 feat: APScheduler entry point — 5-job weekly cadence (Pacific)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 06:46:39 -07:00
adminandClaude Sonnet 4.6 14a0ed1ced feat: admin orchestrate endpoints — run-week, per-step, status
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 06:44:41 -07:00