Commit Graph
100 Commits
Author SHA1 Message Date
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
admin c8382b37e7 feat: add IngredientCreate/Update/Read and IngredientGroceryMatchRead schemas 2026-05-05 20:48:49 -07:00
admin 8ac27d843d feat: add IngredientGroceryMatch model and Ingredient.aliases / Recipe.calories_per_serving 2026-05-05 20:47:31 -07:00
admin a2e3ba6190 feat: migration 0006 - ingredient.aliases, recipe.calories_per_serving, ingredient_grocery_match 2026-05-05 20:45:54 -07:00
admin 2fe609ea9f chore: add rapidfuzz==3.6.1 for ingredient matching 2026-05-05 20:42:11 -07:00
adminandClaude Opus 4.7 5c9ec9faef docs: phase 9 planner algorithm implementation plan
8-task TDD plan covering planner package (config, types, cost,
filter, score, select, generate orchestrator), generate/regenerate/
get endpoints, and end-to-end smoke test against seeded data.

Builds on the thin phase 4 plan; uses NeverSuggest for blocklists,
ingredient_grocery_match for sale-aware cost estimation, top-K=20
set enumeration with diversity penalty for protein/cuisine repeats.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:19:33 -07:00
adminandClaude Opus 4.7 0f08966583 docs: thin phase 4 implementation plan
15-task TDD plan covering migration 0006 (ingredient.aliases,
recipe.calories_per_serving, ingredient_grocery_match), CRUD endpoints
for ingredient/recipe/never-suggest, rapidfuzz matcher service hooked
into scrape success, manual match override, resolve-ingredient assist,
and migration 0007 seeding 30 starter recipes.

Reuses existing NeverSuggest table for ingredient + recipe blocking
rather than adding a new family_ingredient_block table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:15:12 -07:00
adminandClaude Opus 4.7 71e2317462 docs: phase 9 + thin phase 4 design spec
Captures brainstorm decisions for the meal-planner algorithm and the
minimum recipe-engine surface needed to feed it:

- 6 hard constraints (blocklist, never_suggest, recency N=4, calories
  +/-20%, time <=45min, cost <=$30)
- Top-K=20 set enumeration with diversity penalty for protein/cuisine
- Ingredient<->grocery_item matching as a cacheable layer (rapidfuzz +
  manual override) rather than per-run fuzzy work
- Thin phase 4: recipe CRUD, ingredient CRUD, resolve-ingredient
  assist, manual match override, match job, 30-recipe seed
- Recipe ingestion source pros/cons (TheMealDB + Spoonacular + manual)
  documented; decision deferred until phase 4 + phase 9 work end-to-end

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:03:18 -07:00
adminandClaude Opus 4.7 c953111395 docs: refresh ORIENTATION + HANDOFF after r1+r2 recovery and r3-0
Updates ORIENTATION.md and HANDOFF.md to reflect actual state as of
commit 8e89f79: phases 1/2/3/7 complete (verified, not just claimed),
phases 4/5/6/8/9/10/11 not started. Documents the auth model, the
bootstrap login hatch, the SWIFTLY_BEARER_TOKEN expiration handling,
the Swiftly JSON API ingestion path that replaced Playwright, the
canonicalized API paths, and the verification commands to reproduce
the 31/31 pytest gate locally and in CI.

HANDOFF.md is intended for fresh agents and points at .agent/
phase-summaries for the per-phase write-ups. Surfaces 10 caveats and
traps the next agent will hit if they skim, and recommends Phase 9
(meal-planner generation algorithm) as the next move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:41:19 -07:00
adminandClaude Opus 4.7 8e89f793d5 feat: phase r1+r2 recovery + r3-0 swiftly api ingestion
R1 stabilization: pytest harness with transactional db fixture, smoke
+ alembic + auth + scrape + approval + swiftly tests, github actions
ci yaml. Bearer-token admin auth + signed-cookie session for family
ui mutations. Async POST /api/admin/scrape (BackgroundTasks, returns
202). Path canonicalization (no /list, /planned suffixes). DATABASE_URL
fail-fast on empty.

R2 deferred-risk spikes: live lucky california fetch (R2-A), full
email+per-voter approval click round trip with single-use enforcement
(R2-B, console email backend, sendgrid stub).

R3-0 phase 3 redesign: replaced playwright html scraper with requests
based swiftly json api client. 17 categories, ~10k products per scrape,
upsert by (source, external_id). 401 surfaces actionable token-refresh
message via ScrapeLog.error_message.

Pre-existing defects fixed: shopping_list.py syntax error blocking app
import, MealPlan.votes orphan relationship, JSONB(astext=True) invalid
kwarg, missing requests dep, calorie_target schema drift, every SQLEnum
needed values_callable, 0001 had empty downgrade(), seed had duplicate
ingredient rows.

Migrations added: 0003 grocery_item.description, 0004 family_profile.
calorie_target, 0005 grocery_item.external_id + source + composite index.

Verified: 31/31 pytest green, alembic upgrade->downgrade->upgrade clean,
frontend npm run build clean, live scrape 9,960 grocery_item rows in 36s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:08:19 -07:00
admin b9434967ed docs: add HANDOFF.md for new agent takeover 2026-05-04 20:59:27 -07:00
admin 9458edfb45 docs: update ORIENTATION.md with Phase 3/7 progress and current git state 2026-05-04 20:57:09 -07:00
admin 27bca0e940 docs: update ORIENTATION.md phase table 2026-05-04 20:55:16 -07:00
admin 08e196b0ab feat: implement frontend Web UI pages
- Add types for all API models (MealPlan, Recipe, Ingredient, etc.)
- Add API client with mealPlannerApi wrapper for all endpoints
- Implement Dashboard with weekly meal plan grid view
- Implement Pantry page with add/remove functionality
- Implement MealDetail page with recipe display
- Implement ShoppingList page with aisle grouping
- Add ShoppingList route to App.tsx
- Add vite-env.d.ts for Vite env type support
2026-05-04 20:54:54 -07:00
admin 933a0cc9db feat: implement Lucky California scraper with Playwright + BeautifulSoup
- Add BaseScraper with rate limiting, retries, session management
- Add LuckyCaliforniaScraper with Playwright for dynamic content
- Add ScraperService to save scraped items to grocery_item table
- Connect /api/admin/scrape to ScraperService
- Update ORIENTATION.md phase table
2026-05-04 20:50:27 -07:00