Commit Graph
100 Commits
Author SHA1 Message Date
admin e90a9d6683 feat(ui): close 3 P2 audit findings + a11y sweep (Sprint 3)
- lib/toast.tsx (renamed from .ts for JSX): new showToast.undo(message,
  onUndo, ms=5000) helper. Inline 'Undo' button dismisses the toast and
  fires onUndo. Note: react-hot-toast 2.6 lacks onClose/onDismiss, so
  expiry is silent — same effective behavior as confirm() declined.

- Dashboard.handleDelete: captures the full MealPlanItem before the
  DELETE so Undo can re-fire meals.generateItem(planId, dayOfWeek,
  mealType) and refill the slot (recipe may differ — see plan R4).

- Pantry.handleRemove: fully reversible — Undo re-fires pantry.add with
  the original ingredient_id, quantity, and unit. New removeId state
  scopes the spinner to the clicked row.

- Both confirm() call sites removed.

- App.tsx Navigation: whitespace-nowrap + px-2 sm:px-3 so all 4 links fit
  on one line down to 360 px. aria-current='page' on the active link.
  <nav aria-label='Primary'>, <main id='main-content'>.

- components/ui/Badge: optional icon and aria-label props. Dashboard
  approval-status Badge passes aria-label='Approval status: approved'
  (or the current value) so screen readers don't rely on color alone.

- ErrorBoundary already mounted at App.tsx:42 — verified, no code change.

- Review/sprint3-verification.md (new) + Review/ui-nielsen-audit.md and
  fix-ui-audit.md updated with Sprint 3 status and deploy steps.

Build: npm run build (tsc + vite) green. tsc 0 errors.
2026-06-03 18:09:35 -07:00
admin f5fb7558c4 fix(migration): simplify aisle migration + add persistent backup script
- Drop the empty batch_alter_table block and the meaningless
  set_config call from migration 0015. Temp tables still persist
  for the migration's session (Alembic's transactional_ddl).
- New backend/scripts/persist_aisle_backup.sql creates
  public.ingredient_aisle_backup_0015 and
  public.grocery_item_aisle_backup_0015 permanent tables for
  operators who want a recoverable record beyond the migration.
- Update Review/sprint2-verification.md, Review/ui-nielsen-audit.md
  and fix-ui-audit.md with the correct container-based deploy
  steps: docker compose exec db psql -U mealplanner -d mealplanner
  -f /dev/stdin < ...sql. Host psql is not available on the
  deployment host; the db runs inside the container.
2026-06-03 17:41:59 -07:00
admin ccc70aaf72 feat(ui): close 6 P1 audit findings + 1 bonus mobile fix (Sprint 2)
- Dashboard MealCard: title truncate -> line-clamp-2, image shrinks to
  40x40 on <md to give the title room (B6).
- MealDetail: hero reworked to normal flow with stronger gradient;
  description runs through new cleanDescription() helper that strips
  14 spoonacular SEO patterns and trims to the last full sentence.
  Raw description moved to a 'Notes from source' disclosure (B7).
- Pantry: free-text aisle/unit replaced with <Select> populated from
  the new PANTRY_AISLES canonical enum; ingredient name field marked
  required. New PANTRY_AISLES export + PantryAisle type in types (B8).
- backend: alembic 0015_normalize_pantry_aisles maps free-text
  ingredient.aisle and grocery_item.aisle to canonical labels in a
  single transaction; downgrade raises (restore from snapshot).
  backend/scripts/dry_run_aisle_migration.sql is the read-only
  preview helper.
- ShoppingList: human-readable AISLE_LABEL map replaces raw snake_case
  aisle keys; 3-col stat grid with compact mobile sizing (B9 + S3.3).
- Pantry table: role/aria-label region and a right-edge white
  gradient hint at mobile horizontal overflow (B10).
- Recipes: pending/applied filter split, Apply and Reset buttons,
  active-count chip on the Filters button, role=region + aria-label
  on the panel (B11).
- Review/sprint2-verification.md and fix-ui-audit.md updated.

Build: npm run build (tsc + vite) green. tsc emits 0 errors.

Co-located audit + plan docs kept in sync: Review/ui-nielsen-audit.md
gains a Sprint 2 status block; fix-ui-audit.md has implementation
notes for each Sprint 2 task.
2026-06-03 17:36:26 -07:00
admin 36038bb9cb docs(review): mark Sprint 1 P0 fixes addressed in commit f3e4a44 2026-06-02 10:56:19 -07:00
admin f3e4a446a3 fix(ui): close 5 P0 audit findings (ingredients, cost, routing, mobile slots)
Sprint 1 of the UI/UX audit (Review/ui-nielsen-audit.md).

- RecipeDetail: drop .trim() on ingredient line so unit and name no longer fuse
  ('2 canBlack Beans' -> '2 can Black Beans').
- MealDetail: align ingredient field name to backend ('qty' not 'quantity'),
  add 'ingredient.name' fallback for the missing nested name from API.
- MealDetail: '$N/A per serving' -> '$X.XX' or 'No estimate'.
- App: add /recommended alias to /recipes/recommended, plus a catch-all
  NotFound page so unrecognised URLs no longer render blank.
- Dashboard: remove 'hidden md:*' on empty meal slots so mobile users can
  tap Generate. Bump empty-slot button to 44px min-height (a11y).
- EmptyState: accept an optional 'to' prop for Link-wrapped actions.
- types: extend RecipeIngredient with optional notes and nested ingredient.
2026-06-02 10:55:53 -07:00
admin c364b8b222 feat(backend): recipe enrichment with side dishes & detailed instructions
- Add SideDish/SideDishIngredient schemas and recipe.side_dishes JSONB column
- Add recipe_enrichment.py service using Ollama LLM to:
  - Rewrite vague instructions with specific temps, quantities, timing, sauce breakdowns
  - Suggest 1-2 complementary side dishes with ingredients & prep notes
- Wire enrichment into recipe_ingestion.py discovery pipeline
- Add admin trigger endpoint /api/recipes/{id}/enrich for on-demand enrichment
- Migration 0014: Add side_dishes JSONB to recipe table
- Fix schemas/__init__.py imports: restore RecipeBase/Create/Read exports, add datetime/date for PydanticOptional compatibility
- Deployed to docker-willester and migrated to alembic 0014
2026-05-28 06:42:46 -07:00
admin de51e2e8d3 docs: mark tunable planner weights and score persistence as complete
- implementation-plan.md: check Phase 9.2 tunable weights
- phase-9-planner-algorithm.md: mark tunable weights open item as done
- Note: score persistence was completed in earlier commit e22eae1
2026-05-25 16:04:39 -07:00
admin 98d611d7b3 feat(backend): tunable planner weights via family profile config
- Add planner_config JSONB to family_profile model + migration
- Add PlannerConfig.merge(overrides) + to_dict() for family-level override merging
- generate_meal_plan merges family.planner_config into DEFAULT before filtering/scoring/selection
- New endpoints on /api/profile:
  - GET /planner-config — returns merged effective config
  - PUT /planner-config — partial override validation + merge
  - DELETE /planner-config — reset to system defaults
- Schemas: PlannerConfigOverride, PlannerConfigResponse, PlannerConfigUpdateRequest
  with weight-sum validation (0.999–1.001)
- Export RecipeBase/Create/Read/Update from schemas/__init__ to resolve forward refs
2026-05-25 16:03:46 -07:00
admin 86164e6dd3 feat(frontend): add recipe browser + recommended + detail pages
- Recipes.tsx: search, tag/protein/cuisine filters, ingredient search, family blocklist
- Recommended.tsx: feedback-driven recipe recommendations
- RecipeDetail.tsx: recipe display with ingredients, instructions, quick stats
- App.tsx: add /recipes, /recipes/recommended, /recipes/:id routes
- API client: list, recommended, get recipe methods
- Types: add Recipe fields (external_source, external_id, discovery_reason, calories_per_serving, qty)
2026-05-24 21:44:16 -07:00
admin e22eae1ecd feat(backend): persist plan scores on MealPlanItem
- Migration 0012 adds score (float) and components (jsonb) to meal_plan_item
- generate.py: populates score and components at create time
- schemas/MealPlanItemResponse: include score + components fields
- GET /api/meal-plans/{id}: returns persisted values instead of zeros
2026-05-24 20:20:11 -07:00
admin c96b41ec26 feat(api): R3-A recipe engine — search, tags, family scope, recommendations
- GET /api/recipes: added cuisine, protein, dietary, ingredient, max_time,
  spice_max, calorie_max query params
- GET /api/recipes?family_profile_id=… hides never-suggest blocklist recipes
- GET /api/recipes/recommended: returns feedback-driven recipe suggestions
- Update docs: remove completed open items from planner-algorithm.md
2026-05-24 19:45:46 -07:00
admin ae32e650ce feat(backend): wire exclude_recipe_ids, verify MealPlan votes schema, add image generation service
- api/meal_plans.py: /regenerate now passes exclude_recipe_ids into generate_meal_plan
- planner/generate.py: filter recipe_dicts by exclude_recipe_ids set
- image_generation.py: OpenAI gpt-image-1 client with prompt building, b64_json handling
- main.py: StaticFiles mount at /static for generated images
- admin.py: POST /api/admin/trigger-images endpoint for batch generation
- scripts/generate_images.py: CLI for batch image generation
- docker-compose.yml + nginx: volume mounts for static/images persistence
- Verify MealPlanItem.votes ↔ MealPlanVote relationship is correct; no model bug exists
2026-05-24 19:31:26 -07:00
admin dcdb88595e docs: document unit conversion implementation
- Update implementation-plan.md: mark unit conversion complete
- Update HANDOFF.md: add session notes for 2026-05-24 unit conversion
- Update README.md: list Unit Conversion as a feature
2026-05-24 15:34:16 -07:00
admin fd8ba3c4d2 feat(backend): implement unit conversion for cost calculation
- Add UnitConverter (normalization, within-family, density tables)
- Update cost.py to convert recipe qty to grocery price unit
- Update generate.py _load_match_index to fetch ingredient name + unit
- Fix orchestrator email/shopping-list cost loops to use conversion
- Fix missing Ingredient import in generate.py
- Add 19 unit tests
2026-05-24 13:46:50 -07:00
admin 3885d7d0dc feat: feedback-driven recipe discovery (auto-ingest via Spoonacular)
CI / backend (pytest + alembic) (push) Has been cancelled
CI / frontend (build) (push) Has been cancelled
2026-05-24 13:17:39 -07:00
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
adminandClaude Sonnet 4.6 914fdbdb51 fix: close DB session in run_step finally block
Wraps the SessionLocal body in try/finally so db.close() is always
called, preventing connection leaks on exception. Updates the
test monkeypatch to use a no-op close() proxy so the transactional
fixture stays live after run_step returns.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 06:42:37 -07:00
adminandClaude Sonnet 4.6 e3ca8b7a96 feat: orchestrator runner — run_step / run_week per-family loop
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 06:40:36 -07:00
adminandClaude Sonnet 4.6 0a097da18e feat: orchestrator step_generate
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 06:30:50 -07:00
adminandClaude Sonnet 4.6 b2cbdd1533 feat: orchestrator step_scrape with retry + stale-data fallback
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 06:27:27 -07:00
admin 0135b74c03 feat: orchestrator package + alerts.send_admin_alert 2026-05-07 06:24:55 -07:00
admin f8ab448cbd feat: add ADMIN_EMAIL and APP_BASE_URL settings 2026-05-07 06:23:10 -07:00
adminandClaude Sonnet 4.6 03ced28ead feat: WeeklyRun model + FamilyProfile.pending_approval_policy
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 06:21:40 -07:00
admin ea34b9384d feat: migration 0008 — weekly_run table + pending_approval_policy 2026-05-07 06:18:30 -07:00
admin 8ec5cbb58e docs: Phase 5 orchestration implementation plan 2026-05-07 06:13:50 -07:00
adminandClaude Opus 4.7 dc01a0fefb docs: refresh HANDOFF + ORIENTATION for fresh-agent pickup
Bring docs in line with the post-AM-6 state so a fresh agent can pick
up cleanly without first reconciling stale numbers:

- pytest count: 88/59/31 → 92 across all references
- live scrape: 9,960 rows / 36s → 9,980 rows / 44s (latest run, 2026-05-06)
- migrations applied: 0001-0005 → 0001-0007 in both docs
- verification gate updated with auto-minted JWT detail and the 29,779
  ingredient_grocery_match rows produced post-scrape
- "What is real" / Backend: added swiftly_auth.py bullet describing
  get_token() / mint_anonymous_token() / cache semantics + the
  10,928-item live verification
- file map: added services/swiftly_auth.py, services/matcher.py,
  services/planner/, mentioned scraper_service runs matcher post-scrape
- file map: alembic versions 0001 → 0007, tests/ count = 92, marked
  config.py as no-longer-carrying SWIFTLY_BEARER_TOKEN
- spec map line for swiftly-token-auto-mint: "next-up" → "Implemented"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:50:14 -07:00
adminandClaude Opus 4.7 95b8e0c1b5 feat: AM-3..AM-6 strip SWIFTLY_BEARER_TOKEN env var, delete superseded script, refresh docs
AM-3: SWIFTLY_BEARER_TOKEN removed from .env.example, .env.test (local),
docker-compose.yml service env, and Settings (backend/app/config.py).
The scraper docstring is updated to reflect the auto-mint path.

AM-4: scripts/refresh_swiftly_token.py (commit ccfb38a, seleniumbase
click-through capture) deleted; superseded by swiftly_auth.py.

AM-5: docs refreshed.
- spec status header → "Implemented 2026-05-06" with live-verification
  evidence
- HANDOFF.md TL;DR + caveats #2/#3 collapsed; replaced with the
  auto-mint failure-modes caveat; "Suggested next move" rewritten
  pointing to Phase 5 orchestration; file-map and last-updated touched
- ORIENTATION.md env-var section updated (no bearer var) + footer

AM-6 verification gate (run 2026-05-06):
- pytest -q tests/ → 92/92 green (88 prior + 4 new swiftly_auth)
- POST /api/admin/scrape → status=success, items_scraped=10928 in 44s
- grocery_item rows: 9980 (after dedup-by external_id)
- ingredient_grocery_match rows: 29779 (matcher post-hook populated)
- Container env confirmed clean of SWIFTLY_BEARER_TOKEN

The system now scrapes, matches, and generates plans without any
operator-managed credential. Live JWT lifecycle: Firebase REST anon
signUp → cache for ~55min → re-mint as needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:47:15 -07:00
adminandClaude Opus 4.7 dfd79a9d08 feat: AM-2 wire swiftly_auth.get_token() into LuckyCaliforniaScraper
fetch_category() now calls swiftly_auth.get_token() to mint a fresh
Firebase JWT on demand when no explicit bearer_token override is
pinned by tests. The cache short-circuit means the per-call mint
overhead is ~zero in the steady state.

- Removed the empty-token short-circuit; auto-mint makes it moot
- Updated _AUTH_ERROR_MESSAGE: 401-after-mint now points at the spec
  (Lucky tightening anon-auth) rather than asking for manual capture
- Replaced test_swiftly_auth_error_when_token_missing with a positive
  test that verifies fetch_category mints when bearer_token is None
- bearer_token constructor arg preserved for the 401-path test

Full suite: 92/92 green. Live verification via
scripts/spike_swiftly_ingest.py --confirm-live deferred to next step
per HANDOFF AM-2 halt boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:27:37 -07:00
adminandClaude Opus 4.7 5e0a49e4ae feat: AM-1 swiftly_auth module — Firebase REST anon-signUp + process cache
Replaces the static SWIFTLY_BEARER_TOKEN env-var lookup with a JIT
mint via the Firebase Identity Toolkit signUp endpoint, gated by the
firebaseApiKey published in luckysupermarkets.com/config.json.

- get_token(): returns cached JWT if exp > now+300s, else mints
- mint_anonymous_token(): fetches API key, posts signUp with
  Origin/Referer headers, validates iss + exp on the returned JWT
- SwiftlyAuthMintError surfaces verbatim to ScrapeLog.error_message
- Process-local cache only; threading.Lock around mutate

Tests: 4 unit tests covering fresh mint, cache hit, near-expiry
re-mint, and Firebase non-200. Full suite: 92/92 green.

Spec: docs/specs/2026-05-06-swiftly-token-auto-mint.md
Wiring into lucky_ca_scraper deferred to AM-2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:19:30 -07:00
adminandClaude Opus 4.7 b4d92c07af docs: embed AM-1..AM-6 task list in HANDOFF.md for fresh-agent pickup
Breaks the auto-mint spec into 6 ordered, sized tasks with an explicit
halt-for-approval boundary at AM-2 (live scrape verification before
removing the env var). Restates the verified prerequisites (config.json
publicly readable; Firebase signUp returns valid JWT with proper headers;
Swiftly accepts the minted token) so a fresh agent doesn't have to
re-discover them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:08:53 -07:00
adminandClaude Opus 4.7 b523c58e77 docs: design Swiftly token auto-mint and queue it as next implementation pass
Discovery: luckysupermarkets.com/config.json is publicly readable and
exposes firebaseApiKey. With proper Origin/Referer headers, Firebase
Identity Toolkit's anonymous-signup REST endpoint mints the same JWT
shape (iss=swiftly-lu-prod, aud=swiftly-lu-prod, anon provider, 3600s
TTL) that Swiftly accepts. Verified end-to-end on 2026-05-06.

This eliminates the manual hourly token-capture toil and supersedes
the seleniumbase-based scripts/refresh_swiftly_token.py (commit
ccfb38a) which had partial UI selector issues.

- New spec: docs/specs/2026-05-06-swiftly-token-auto-mint.md
- HANDOFF.md TL;DR refreshed (Phase 9 shipped); caveat #2 + #3
  rewritten to point to the auto-mint redesign; suggested-next-move
  reordered to put the redesign first
- ORIENTATION.md env-var section flags SWIFTLY_BEARER_TOKEN as
  scheduled-for-removal; "Where to look" lists both specs;
  last-updated footer refreshed

Implementation deferred — this commit captures the design and routing
only. Estimated 2-3 hours of focused work to ship per the spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 10:54:01 -07:00
adminandClaude Opus 4.7 ccfb38a34e feat: scripts/refresh_swiftly_token.py - capture fresh Swiftly token via seleniumbase
Drives luckysupermarkets.com in stealth CDP mode: opens the store
locator, types the zip code, clicks the target store, then triggers a
category page navigation. A fetch + XHR interceptor (installed via JS)
captures the first Authorization header sent to a Swiftly host. The
captured JWT is validated (iss + exp), then written into the env file.

Runs on the host (not docker) since seleniumbase needs a real Chrome.
Defaults to .env.test, headless, zip 94806, store 757. Flags:
  --debug          visible Chrome window
  --restart-backend  rerun docker compose to pick up the new token
  --env-file PATH  override target env file
  --zip / --store  override location

Selectors are intentionally JS-based and tolerant of UI changes
(querySelectorAll fallthrough by attribute heuristics + textContent
substring match) so first-attempt failures degrade to clear errors
rather than silent breakage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 10:42:48 -07:00
admin 479f2f3b45 docs: phase 9 complete - planner algorithm shipped 2026-05-06 09:17:15 -07:00
admin 75e4bdb7a6 feat: POST /api/admin/meal-plans/generate + regenerate + get endpoints 2026-05-06 09:15:16 -07:00
admin 3f92e1f641 feat: planner orchestrator - load, filter, score, select, persist 2026-05-06 09:11:53 -07:00
admin 63e292a995 feat: planner top-K set enumeration with protein/cuisine diversity penalty 2026-05-06 06:48:26 -07:00
admin 77813cc7d3 feat: planner per-recipe scoring with 5 weighted signals 2026-05-06 06:44:58 -07:00
admin 95396137c6 feat: planner hard-constraint filter for the 6 spec constraints 2026-05-06 06:42:49 -07:00
admin bf0a327561 feat: planner cost+savings estimator against ingredient_grocery_match 2026-05-06 06:40:33 -07:00
admin c86321908c feat: planner config (weights, thresholds, K) and shared types 2026-05-06 06:38:26 -07:00
admin bd0e34d7e1 docs: thin phase 4 complete; refresh ORIENTATION + HANDOFF 2026-05-06 06:36:36 -07:00
admin 07ddec792c feat: seed 30 starter recipes spanning chicken/beef/pork/fish/vegetarian 2026-05-06 06:33:55 -07:00
admin ed3255adb6 feat: migration 0007 - seed canonical ingredients with aliases (recipes follow in P4-14) 2026-05-06 06:29:33 -07:00
admin 1f7b9bac23 feat: never-suggest CRUD endpoints (ingredient and recipe blocklist) 2026-05-06 06:26:16 -07:00
admin 3d5f0c2668 feat: manual match pin/unpin endpoints 2026-05-06 06:24:17 -07:00
admin db4b01337e feat: run matcher after successful scrape; failures don't flip scrape status 2026-05-06 06:21:54 -07:00
admin 6dfb84310f feat: rapidfuzz-based ingredient<->grocery matcher with manual-pin preservation 2026-05-06 06:18:18 -07:00
admin 489ee03574 feat: POST /api/admin/recipes/resolve-ingredient with rapidfuzz top-3 2026-05-06 06:16:29 -07:00
adminandClaude Opus 4.7 f16a2f8710 feat: recipe CRUD endpoints with canonical ingredient validation
POST/PATCH validate every ingredient_id against the ingredient table
and return 422 with the missing list when refs don't resolve. Replaces
the prior recipes.py stub. Public read routes + admin write routes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 06:14:37 -07:00
admin b1ea011d49 feat: ingredient CRUD endpoints with admin gating 2026-05-05 20:54:31 -07:00
admin be7f698779 feat: add RecipeCreate/Update/Read schemas with canonical ingredient refs 2026-05-05 20:50:07 -07:00