Commit graph

28 commits

Author SHA1 Message Date
2547f80893 feat(streaming): add StreamTokenRequest/Response schemas 2026-04-24 10:19:18 -07:00
c3e7dc1ea4 feat: time-first recipe entry (kiwi#52)
- Add max_total_min to RecipeRequest schema and TypeScript interface
- Add _within_time() helper to recipe_engine using parse_time_effort()
  with graceful degradation (empty directions or no signals -> pass)
- Wire max_total_min filter into suggest() loop after max_time_min
- Add time_first_layout to allowed settings keys
- Add timeFirstLayout ref to settings store (preserves sensoryPreferences)
- Add maxTotalMin ref to recipes store, wired into _buildRequest()
- Add time bucket selector UI (15/30/45/60/90 min) in RecipesView
  Find tab, gated by timeFirstLayout != 'normal'
- Add time-first layout selector section in SettingsView
- Add 5 _within_time unit tests and 2 settings key tests
2026-04-24 10:15:58 -07:00
f1d35dd1ac feat(recipes): 'Not today' per-session ingredient exclusions
Users often have ingredients they want to avoid today (out of stock, not feeling it)
that aren't true allergies. The new 'Not today' filter lets them exclude specific
ingredients per session without permanently modifying their allergy list.

- recipe.py schema: exclude_ingredients field (list[str], default [])
- recipe_engine.py: filters corpus results when any ingredient is in exclude_set
- llm_recipe.py: injects exclusions into both prompt templates so LLM-generated
  recipes respect the constraint at generation time
- RecipesView.vue: tag-chip UI with Enter/comma input, removes on × click
- stores/recipes.ts: excludeIngredients reactive list (not persisted to localStorage)
2026-04-21 15:05:16 -07:00
ed4595d960 feat(recipes): async L3/L4 recipe job queue with poll endpoint
Adds the recipe_jobs table and background task pipeline for level 3/4
recipe generation. POST ?async=true returns 202 with job_id; clients
poll GET /recipes/jobs/{job_id} until status=done.

Key fix: _enqueue_recipe_job now calls scheduler.enqueue() after
insert_task() to wake the in-memory work queue immediately. Without
this, tasks sat in 'queued' until the scheduler's 30s idle cycle or
an API restart triggered _load_queued_tasks().

- Migration 034: recipe_jobs table (job_id, user_id, status, request,
  result, error) with indexes on job_id and user_id/created_at
- Store: create/get/update_running/complete/fail recipe job methods
- runner.py: recipe_llm task type + _run_recipe_llm handler; MUST
  call fail_recipe_job() before re-raising so status stays consistent
- CLOUD_MODE guard: falls back to sync generation (scheduler only
  polls shared settings DB, not per-user DBs)
- L4 wildcard is covered by the same req.level in (3, 4) dispatch
2026-04-19 21:44:27 -07:00
b2c546e86a feat: wire secondary-use window hints into recipe engine (#83)
Secondary-state items (stale bread, overripe bananas, day-old rice, etc.)
are now surfaced to the recipe engine so relevant recipes get matched even
when the ingredient is phrased differently in the corpus (e.g. "day-old
rice" vs. "rice").

Backend:
- Add rice and tortillas entries to SECONDARY_WINDOW in expiration_predictor
- Add secondary_pantry_items: dict[str, str] field to RecipeRequest schema
  (maps product_name → secondary_state label, e.g. {"Bread": "stale"})
- Add _SECONDARY_STATE_SYNONYMS lookup in recipe_engine — keyed by
  (category, state_label), returns corpus-matching ingredient phrases
- Update _expand_pantry_set() to accept secondary_pantry_items and inject
  synonym terms into the expanded pantry set used for FTS matching

Frontend:
- Add secondary_pantry_items to RecipeRequest interface in api.ts
- Add secondaryPantryItems param to _buildRequest / suggest / loadMore
  in the recipes store
- Add secondaryPantryItems computed to RecipesView — reads secondary_state
  from inventory items (expired but still in secondary window) and builds
  the product_name → state_label map
- Pass secondaryPantryItems into handleSuggest and handleLoadMore

Closes #83
2026-04-18 19:06:53 -07:00
01aae2eec8 fix: recipe enrichment backfill, main_ingredient browser domain, bug batch
Some checks failed
CI / Backend (Python) (push) Has been cancelled
CI / Frontend (Vue) (push) Has been cancelled
CI / Backend (Python) (pull_request) Has been cancelled
CI / Frontend (Vue) (pull_request) Has been cancelled
Recipe corpus (#108):
- Add _MAIN_INGREDIENT_SIGNALS to tag_inferrer.py (Chicken/Beef/Pork/Fish/Pasta/
  Vegetables/Eggs/Legumes/Grains/Cheese) — infers main:* tags from ingredient names
- Update browser_domains.py main_ingredient categories to use main:* tag queries
  instead of raw food terms; recipe_browser_fts now has full 3.19M row coverage
  (was ~1.2K before backfill)

Bug fixes:
- Fix community posts response shape (#96): add total/page/page_size fields
- Fix export endpoint arg types (#92)
- Fix household invite store leak (#93)
- Fix receipts endpoint issues
- Fix saved_recipes endpoint
- Add session endpoint (app/api/endpoints/session.py)

Shopping list:
- Add migration 033_shopping_list.sql
- Add shopping schemas (app/models/schemas/shopping.py)
- Add ShoppingView.vue, ShoppingItemRow.vue, shopping.ts store

Frontend:
- InventoryList, RecipesView, RecipeDetailPanel polish
- App.vue routing updates for shopping view

Docs:
- Add user-facing docs under docs/ (getting-started, user-guide, reference)
- Add screenshots
2026-04-18 15:38:56 -07:00
890216a1f0 fix: wire recipe corpus to cloud per-user DBs via SQLite ATTACH (#102)
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
Cloud mode: attach shared read-only corpus DB (RECIPE_DB_PATH env var)
as "corpus" schema so per-user SQLite DBs can access 3.19M recipes.
All corpus table references now use self._cp prefix ("corpus." in cloud,
"" in local). FTS5 pseudo-column kept unqualified per SQLite spec.
compose.cloud.yml: bind-mount /Library/Assets/kiwi/kiwi.db read-only.

Also fix batch of audit issues:
- #101: OCR approval used source="receipt_ocr" for inventory_items — use "receipt"
- #89/#100: Shopping confirm-purchase used source="shopping_list" — use "manual"
- #103: Frontend inventory filter sent ?status= but API expects ?item_status=
- #104: InventoryItemUpdate schema missing purchase_date field; store.py allowed set also missing it
- #105: Guest cookie Secure flag tied to CLOUD_MODE instead of X-Forwarded-Proto; broke HTTP direct-port access
2026-04-18 14:21:56 -07:00
e745ce4375 feat: wire meal planner slot editor and meal type picker
Slot click now opens an inline editor panel:
- Pick from saved recipes via dropdown (pre-loaded on mount)
- Or type a custom label
- Clear slot button when a slot is already filled
- Save/Cancel with loading state

Add meal type opens a chip picker showing the types not yet active
(breakfast / lunch / snack minus whatever is already on the plan).
Selecting one calls the new PATCH /meal-plans/{plan_id} endpoint.

Backend:
- PATCH /meal-plans/{plan_id} with UpdatePlanRequest(meal_types)
- store.update_meal_plan_types() UPDATE ... RETURNING *
- 409 on IntegrityError in create_plan (already in place)
2026-04-16 14:23:38 -07:00
200a6ef87b feat(recipes): complexity badges, time hints, Surprise Me, Just Pick One
#55 — Complexity rating on recipe cards:
  - Derived from direction text via _classify_method_complexity()
  - Badge displayed on every card: easy (green), moderate (amber), involved (red)
  - Filterable via complexity filter chips in the results bar

#58 — Cooking time + difficulty as filter domains:
  - estimated_time_min derived from step count + complexity
  - Time hint (~Nm) shown on every card
  - complexity_filter and max_time_min fields in RecipeRequest
  - Both applied in the engine before suggestions are built

#53 — Surprise Me: picks a random suggestion from the filtered pool,
  avoids repeating the last pick. Shown in a spotlight card.

#57 — Just Pick One: surfaces the top-matched suggestion in the same
  spotlight card. One tap to commit to cooking it.

Closes #55, #58, #53, #57
2026-04-16 09:27:34 -07:00
2ad71f2636 feat(recipes): pantry match floor filter — 'can make now' toggle
Adds pantry_match_only flag to RecipeRequest. When enabled, any recipe
with one or more missing ingredients (after swaps) is excluded from
results. Swapped ingredients count as covered.

Frontend: toggle checkbox in recipe settings panel, disabled when
shopping mode is active (the two modes are mutually exclusive). Hides
the max-missing input when pantry-match-only is on (irrelevant there).

Closes #63
2026-04-16 09:12:24 -07:00
0de6182f48 feat(scan): barcode miss fallback chain — Open Beauty Facts + Open Products Facts
When a barcode is not found in Open Food Facts, the service now tries
Open Beauty Facts and Open Products Facts before giving up. All three
share the same API format; only the host URL differs.

When all databases miss, the scan endpoint sets needs_manual_entry=true
in the result. The frontend detects this, shows a calm informational
message, and switches to manual entry mode automatically.

Also fixes a latent bug where not-found scans showed 'Added: item to
pantry' due to the success condition checking barcodes_found (always 1)
instead of added_to_inventory.

Closes #65
2026-04-16 08:30:49 -07:00
fb18a9c78c feat: partial consumption tracking and waste/disposal logging (#12 #60)
#12 — partial consume:
- POST /inventory/items/{id}/consume now accepts optional {quantity}
  body; decrements by that amount and only marks status=consumed when
  quantity reaches zero (store.partial_consume_item)
- OFFs barcode scan pre-fills sub-unit quantity when product data
  includes a pack size (number_of_units or 'N x ...' quantity string)
- Consume button shows quantity-aware label and opens ActionDialog
  with number input for multi-unit items ('use some or all')
- consumeItem() in api.ts now returns InventoryItem and accepts
  optional quantity param

#60 — disposal logging:
- Migration 031: adds disposal_reason TEXT column to inventory_items
  (status='discarded' was already in the CHECK constraint)
- POST /inventory/items/{id}/discard endpoint with optional DiscardRequest
  body (free text or preset reason)
- Calm framing: 'item not used' not 'wasted'; reason presets avoid
  blame language ('went bad before I could use it', 'too much — had excess')
- Muted discard button (X icon, tertiary color) — not alarming

Shared:
- New ActionDialog.vue component for dialogs with inline inputs
  (quantity stepper or reason dropdown); keeps ConfirmDialog simple
- disposal_reason field added to InventoryItemResponse

Closes #12
Closes #60
2026-04-16 07:28:21 -07:00
64a0abebe3 feat: pantry intel cluster — #61 expiry display, #64 cook log, #66 scaling, #59 open-package tracking
#61: expiry badge now shows relative + calendar date ("5d · Apr 15") with
tooltip "Expires in 5 days (Apr 15)"; traffic-light colors already in place

#64: RecipeDetailPanel.handleCook() calls recipesStore.logCook(); SavedRecipesPanel
shows "Last made: X ago" below each card using cookLog entries

#66: Serving multiplier (1x/2x/3x/4x) in RecipeDetailPanel scales ingredient
quantities using regex; handles integers, decimals, fractions (1/2, 3/4),
mixed numbers (1 1/2), and ranges (2-3); leaves unrecognised strings unchanged

#59: migration 030 adds opened_date column; ExpirationPredictor gains
SHELF_LIFE_AFTER_OPENING table + days_after_opening(); POST /inventory/items/{id}/open
sets opened_date=today and returns computed opened_expiry_date; InventoryList
shows lock-open button for unopened items and an "📂 5d · Apr 15" badge once opened
2026-04-16 06:01:25 -07:00
76516abd62 feat: metric/imperial unit preference (#81)
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
- Settings: add unit_system key (metric | imperial, default metric)
- Recipe LLM prompts: inject unit instruction into L3 and L4 prompts
  so generated recipes use the user's preferred units throughout
- Frontend: new utils/units.ts converter (mirrors Python units.py)
- Inventory list: display quantities converted to preferred units
- Settings view: metric/imperial toggle with save button
- Settings store: load/save unit_system alongside cooking_equipment

Closes #81
2026-04-15 23:04:29 -07:00
9941227fae chore: merge main into feature/meal-planner
Resolves three conflicts:
- app/api/routes.py: fixed saved_recipes-before-recipes ordering from main;
  meal_plans and community_router from feature branch
- app/db/store.py: meal plan/prep session methods (feature) + community
  pseudonym methods (main) -- both additive
- app/tiers.py: KIWI_BYOK_UNLOCKABLE includes meal_plan_llm,
  meal_plan_llm_timing (feature) and community_fork_adapt (main)
2026-04-14 14:53:52 -07:00
b4f031e87d feat(kiwi): add orch_fallback field to RecipeResult 2026-04-14 14:38:37 -07:00
65ef65bb4c feat: add Pydantic schemas for Build Your Own tab endpoints 2026-04-14 09:45:05 -07:00
482666907b fix(meal-planner): validate meal_type path param, enforce store whitelist safety, add week_start date validation, make PrepTask frozen
- upsert_slot: raise 422 immediately if meal_type not in VALID_MEAL_TYPES
- update_prep_task: assert whitelist safety contract after dict comprehension
- CreatePlanRequest: week_start typed as date with must_be_monday validator; str() cast at call site
- PrepTask: frozen=True; build_prep_tasks rewired to use (priority, kwargs) tuples so frozen instances are built with correct sequence_order in one pass (no post-construction mutation)
- Move deferred import json to file-level in meal_plans.py
- Fix test dates: "2026-04-14" was a Tuesday; updated request bodies to "2026-04-13" (Monday)
2026-04-12 13:51:50 -07:00
067b0821af feat(schemas): add meal plan Pydantic models 2026-04-12 13:12:41 -07:00
793df1b5cf feat: saved recipes, recipe browser, and recipe detail panel
- Saved recipes: save/unsave, star rating, notes, tags, collections (migrations 018-020)
- Recipe browser: domain/category browsing with pantry match badges, pagination
- Recipe detail panel: full directions, ingredient checklist, swap candidates, prep notes
- Grocery links: affiliate links for missing ingredients
- Nutrition filters and display chips on recipe cards
- Bookmark toggle persisted to saved_recipes table
- Tier gates on saved recipes (paid) and collections (premium)
- Browser telemetry for domain/category click tracking
- Cloud compose: CLOUD_DATA_ROOT volume mount for per-user SQLite trees
- manage.sh: cf-orch agent sidecar in local stack
- README: updated feature list and stack description
2026-04-08 14:35:02 -07:00
c7861344b7 feat: add MessageResponse schema; wire response_model on leave + remove-member endpoints 2026-04-04 22:47:39 -07:00
dce8d05a09 refactor: use str | None + Field(default_factory=list) in household schemas 2026-04-04 22:41:53 -07:00
2db4de6d8f feat: add household Pydantic schemas 2026-04-04 22:40:30 -07:00
1a493e0ad9 feat: recipe engine — assembly templates, prep notes, FTS fixes, texture backfill
- Assembly template system (13 templates: burrito, fried rice, omelette, stir fry,
  pasta, sandwich, grain bowl, soup/stew, casserole, pancakes, porridge, pie, pudding)
  with role-based matching, whole-word single-keyword guard, deterministic titles
  via MD5 pantry hash
- Prep-state stripping: strips 'melted butter' → 'butter' for coverage checks;
  reconstructs actionable states as 'Before you start:' cooking instructions
  (NutritionPanel prep_notes field + RecipesView.vue display block)
- FTS5 fixes: always double-quote all terms; strip apostrophes to prevent
  syntax errors on brands like "Stouffer's"; 'plant-based' → bare 'based' crash
- Bidirectional synonym expansion: alt-meat, alt-chicken, alt-beef, alt-pork
  mapped to canonical texture class; pantry expansion covers 'hamburger' from
  'burger patties' etc.
- Texture profile backfill script (378K ingredient_profiles rows) with macro-derived
  classification in priority order (fatty → creamy → starchy → firm → fibrous →
  tender → liquid → neutral); oats/legumes starchy-first fix
- LLM prompt: ban flavoured/sweetened ingredients (vanilla yoghurt) from savoury
- Migrations 014 (nutrition macros) + 015 (recipe FTS index)
- Nutrition estimation pipeline script
- gitignore MagicMock sqlite test artifacts
2026-04-02 22:12:35 -07:00
9371df1c95 feat: recipe engine Phase 3 — StyleAdapter, LLM levels 3-4, user settings
Task 13: StyleAdapter with 5 cuisine templates (Italian, Latin, East Asian,
Eastern European, Mediterranean). Each template includes weighted method_bias
(sums to 1.0), element-filtered aromatics/depth/structure helpers, and
seasoning/finishing-fat vectors. StyleTemplate is a fully immutable frozen
dataclass with tuple fields.

Task 14: LLMRecipeGenerator for Levels 3 and 4. Level 3 builds a structured
element-scaffold prompt; Level 4 generates a minimal wildcard prompt (<1500
chars). Allergy hard-exclusion wired through RecipeRequest.allergies into
both prompt builders and the generate() call path. Parsed LLM response
(title, ingredients, directions, notes) fully propagated to RecipeSuggestion.

Task 15: User settings key-value store. Migration 012 adds user_settings
table. Store.get_setting / set_setting with upsert. GET/PUT /settings/{key}
endpoints with Pydantic SettingBody, key allowlist, get_session dependency.
RecipeEngine reads cooking_equipment from settings when hard_day_mode=True.

55 tests passing.
2026-03-31 14:15:18 -07:00
e8fb57f6a2 feat: RecipeEngine Level 1-2 — grocery links + affiliate deeplink builder
Add GroceryLink schema model and grocery_links field to RecipeResult.
Introduce GroceryLinkBuilder service (Amazon Fresh, Walmart, Instacart)
using env-var affiliate tags; no links emitted when tags are absent.
Wire link builder into RecipeEngine.suggest() for levels 1-2.
Add test_grocery_links_free_tier to verify structure contract.

35 tests passing.
2026-03-31 12:23:07 -07:00
37737b06de feat: RecipeEngine Level 1-2 — corpus match, substitution, grocery list, hard day mode 2026-03-31 11:50:28 -07:00
8cbde774e5 chore: initial commit — kiwi Phase 2 complete
Pantry tracker app with:
- FastAPI backend + Vue 3 SPA frontend
- SQLite via circuitforge-core (migrations 001-005)
- Inventory CRUD, barcode scan, receipt OCR pipeline
- Expiry prediction (deterministic + LLM fallback)
- CF-core tier system integration
- Cloud session support (menagerie)
2026-03-30 22:20:48 -07:00