Commit graph

252 commits

Author SHA1 Message Date
b4624fba84 feat(ask): add POST /recipes/ask endpoint for natural-language recipe search
Free tier: keyword extraction + FTS ingredient search + title probe search.
Paid tier / BYOK: same search, then LLM synthesis of a conversational answer
(8s timeout so an unresponsive model degrades gracefully to recipe list only).

- AskRequest / AskRecipeHit / AskResponse schemas in recipe.py
- _extract_ask_keywords(): tokenize question, strip stopwords
- _ask_in_thread(): two-pronged search (ingredient FTS + title LIKE)
  merges by ID, computes pantry match_pct when pantry_items provided
- Endpoint registered before /{recipe_id} to avoid integer coercion on /ask
- LLM synthesis gated to paid/premium/ultra only (not "local" dev tier)

Closes #134 (backend)
2026-05-11 13:07:53 -07:00
667daf939e feat(streaming): replace raw <pre> with skeleton + progressive reveal (closes #133)
Parses the streamed LLM output (Title / Ingredients / Directions / Notes
plain-text format) on the fly as tokens arrive. Shows a shimmer skeleton
for each section while that section has not yet arrived, then swaps in
real content as the parse succeeds — title first, then ingredients, then
numbered steps, then notes on completion.

parsedStream computed: matches Title, Ingredients (comma-split), numbered
step lines, and Notes sections from the accumulating streamChunks string.

Skeleton shimmer is CSS-only (no JS); respects prefers-reduced-motion by
falling back to a static placeholder color. The stream-output <pre> block
is removed from the template entirely — raw tokens never reach the user.
2026-05-11 12:46:27 -07:00
4e50661483 feat(find): invert flow — auto-suggest on tab open, collapsible Refine panel (closes #132)
Auto-suggest (L1/L2 only):
  When the Find tab is activated with a non-empty pantry and no existing
  results, suggestion fires immediately without user action. L3/L4 are
  excluded to avoid unintended VRAM allocation and AI quota charges.
  After the first auto-suggest completes, the Refine panel collapses so
  the results are the first thing the user sees.

Live re-suggest (L1/L2 only):
  A single filterKey computed wraps all filter state as JSON. Any filter
  change while on the Find tab with existing results triggers a debounced
  (1.2s) re-suggest, keeping the result list live without button clicks.

Refine collapsible:
  Time budget, Dietary preferences, and Nutrition/Advanced filters are
  wrapped in a v-show panel controlled by filtersOpen (persisted to
  localStorage under kiwi:find_filters_open, default open). Level
  selector, Hard Day Mode, and the Suggest button remain always visible.
  Toggle button shows active filter count badge when any filter is set.
2026-05-11 12:41:58 -07:00
ac4eda2047 fix(build): remove unused settingsStore import after time-budget change
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
2026-05-11 12:37:24 -07:00
3f4b756fc6 feat(find): surface time budget inline, always visible (closes #131)
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
The time budget selector (hands-on and total time chips) was previously
gated behind the time_first_layout Settings preference. Removed the v-if
guard so both rows are always visible in the Find tab without requiring
a Settings change.

Added "No limit" clear buttons that appear next to the chip row when a
time limit is active, so users can reset a time filter in one tap without
needing to find the active chip and re-tap it.

The time_first_layout setting in Settings remains for users who want
control over the layout.
2026-05-11 12:11:06 -07:00
973c76a4c8 feat(browse): add breadcrumb nav above recipe grid (closes #130)
Renders domain › category › subcategory above the recipe grid whenever
a domain and category are active. Each ancestor crumb is a button that
navigates back up the hierarchy (selectDomain / selectCategory). The
leaf node is a plain span with aria-current="page". The nav has
aria-label="Browse location" for screen reader context.
2026-05-11 11:58:49 -07:00
92fab94ae0 feat(find): active-filter bar with clear-all (closes #129)
Adds a summary bar that appears at the top of the Find Recipes panel
whenever any filter is active. Shows a count ("3 filters active") and
a Clear all button that resets all Find-tab filters in one tap:
  constraints, allergies, excluded ingredients, shopping mode,
  pantry-match-only, hard day mode, time budgets (active + total),
  max missing, style, category, and all four nutrition limits.

Local input refs (constraintInput, allergyInput, etc.) are also cleared
so the text fields don't show stale uncommitted values after a clear.
2026-05-11 11:57:10 -07:00
30f5620fd5 feat(settings): autosave on change, remove Save buttons (closes #128)
Each setting now saves via a debounced (600ms) individual API call when
its value changes. A hydration guard (_hydrated flag + nextTick) prevents
watchers from firing during the initial load() fetch, ensuring the first
API round-trip does not generate spurious write calls.

Removed: five explicit Save buttons across Equipment, Sensory, Units,
Shopping Region, and Recipe Search Layout sections.
Added: "Changes save automatically." subtitle + fixed bottom-right toast
  that appears for 2s after any successful save, with enter/leave
  transitions that respect prefers-reduced-motion via the theme.

The full save() and saveSensory() actions are kept as internal fallbacks.
2026-05-11 11:55:09 -07:00
0ef57618bf fix(a11y): add aria-pressed and aria-label to Browse panel buttons (WCAG 2.1)
Screen readers had no way to determine which domain, category, subcategory,
or sort button was selected — the active CSS class is invisible to assistive
technology.

  - aria-pressed on all toggle buttons (domain, category, subcategory, sort)
  - aria-label="Previous page" / "Next page" on pagination buttons
  - aria-live="polite" on results count span — announces filter result changes
  - Equipment chip-remove: "Remove" → "Remove equipment: {item}"

Addresses WCAG 2.1 AA criteria 4.1.2 (Name, Role, Value) and 1.3.1
(Info and Relationships). Part of kiwi UX audit (2026-05-11).
2026-05-11 11:33:10 -07:00
8c765b7da2 fix(barcode): look up product info before checking auto_add_to_inventory
Previously, get_or_create_product was only called when auto_add was true,
so scan responses with auto_add=false returned no product details. Now the
DB lookup always runs when product_info is available; inventory insertion
is still conditional on auto_add_to_inventory. Fixes preview-only barcode
scans returning empty product fields.
2026-05-11 11:33:02 -07:00
e57f46f4b6 feat(streaming): add native SSE fallback for L3/L4 recipe generation (closes #126)
Two-phase streaming architecture:
  Phase 1 (sync thread): IngredientClassifier builds element profiles +
    gap list from SQLite — thread-safe, no async context needed
  Phase 2 (async): LLMRecipeGenerator.stream_generate() yields tokens via
    cf-orch warm vllm (existing /stream-token path) or AsyncOpenAI against
    Ollama if the coordinator is unavailable

Backend (app/services/recipe/llm_recipe.py):
  - stream_generate() async generator; _try_alloc_for_stream() sync helper
  - _stream_openai_compat() static method handles __auto__ model resolution
  - LLMRecipeGenerator(None) is safe for streaming (store not used)

Endpoint (app/api/endpoints/recipes.py):
  - ?stream=true on POST /recipes/suggest returns StreamingResponse
  - X-Accel-Buffering: no prevents nginx buffering without nginx.conf edits

Frontend (api.ts, recipes.ts, RecipesView.vue):
  - suggestRecipeStream() uses fetch + ReadableStream (POST; EventSource
    only supports GET)
  - streamSuggest() action in recipes store builds request internally
  - RecipesView.streamRecipe() silently falls back to native SSE when
    cf-orch token fetch fails rather than surfacing an error
2026-05-11 11:32:54 -07:00
04dbdddbad feat(mcp): add Kiwi MCP server for corpus DB access (closes #124)
Exposes four read-only tools to Claude Code:
  kiwi_query_corpus   — parameterised SELECT against kiwi.db (200-row cap)
  kiwi_count_fts      — FTS5 MATCH hit count for keyword coverage audits
  kiwi_sample_tags    — tag frequency distribution by prefix
  kiwi_browse_preview — first-page results from the live browse API

DB opened in SQLite URI read-only mode (mode=ro); any write statement is
rejected at the driver level. Configure via KIWI_DB_PATH and KIWI_API_URL
env vars (see module docstring for settings.json snippet).
2026-05-11 11:32:40 -07:00
e83bb0415a feat(manage): add update and cloud-update commands (closes #127)
Adds `update` (local stack) and `cloud-update` (menagerie) subcommands
to manage.sh. Both pull HEAD and rebuild/restart the Docker stack in one
step — required for post-merge deployment without manual compose commands.
2026-05-11 11:32:30 -07:00
e62d69d099 docs(readme): landing page rewrite — feature table, quick start, tier table, Forgejo-primary, split license
Some checks failed
CI / Backend (Python) (push) Has been cancelled
CI / Frontend (Vue) (push) Has been cancelled
Mirror / mirror (push) Has been cancelled
2026-05-06 08:51:38 -07:00
7498995092 feat(filters): split time filter into hands-on and total time (kiwi#52)
Some checks failed
CI / Backend (Python) (push) Has been cancelled
CI / Frontend (Vue) (push) Has been cancelled
Mirror / mirror (push) Has been cancelled
Adds max_active_min request field and backend filter. Active time uses
parse_time_effort().active_min (passive waits excluded). Recipes with
no parsed active time signal are not excluded (avoid hiding unlabelled
results). Total and active limits are AND'd when both set.

UI: two pill rows — "Hands-on time" (15/30/45/1hr) and "Total time"
(30m/1hr/90m/2hr/3hr/4+hr). Replaces single row capped at 90 min.
2026-04-27 16:03:27 -07:00
640fcefa9e fix(ui): compact recipe cards, batch ingredient classifier queries
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
Recipe cards were rendering full directions, all nutrition chips,
prep notes, swap candidates, and grocery links inline in the grid —
making each card tall enough to push the second row below the fold at
3-column widths. Cards now show title, match/complexity/time badges,
up to 4 pantry ingredient chips, missing count, and calorie hint.
Full detail remains in RecipeDetailPanel on "Make this".

ElementClassifier.classify_batch() was issuing N separate DB queries
(one per pantry item). Replaced with a single WHERE name IN (...)
query + heuristic fallback for misses — same result, one round-trip.
2026-04-27 14:56:00 -07:00
d5a4b14400 chore(pipeline): add fast targeted meal-tag backfill script
Some checks failed
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
backfill_meal_tags.py merges meal: tags from title-only matching
into existing inferred_tags without re-deriving all other signals.

~10x faster than infer_recipe_tags.py --force for meal-tag-only
updates: 3.19M recipes in ~5-10min vs ~2.5h for full re-derivation.
2026-04-27 13:00:58 -07:00
7fd92d5179 feat(tags): add meal type inference from recipe titles (#125)
Adds _MEAL_SIGNALS table to tag_inferrer with title-only matching for:
  meal:Breakfast — pancakes, waffles, frittata, oatmeal, granola, etc.
  meal:Dessert   — cake, cookie, brownie, pudding, ice cream, tart, etc.
  meal:Snack     — dip, chips, popcorn, nachos, energy balls, etc.
  meal:Beverage  — smoothie, cocktail, juice, lemonade, etc.
  meal:Lunch     — sandwich, wrap, panini, grilled cheese, etc.
  meal:Bread     — bread, sourdough, focaccia, dinner roll, etc.

Uses word-boundary + optional-plural regex (\bWORD(?:s|es)?\b) so:
- "pancakes" matches the "pancake" pattern but "pancake" != "cake"
- "tartare" does not match "tart" (no word boundary after tart in tartare)
- "dipping" does not match "dip" (extra chars prevent boundary)

Title-only matching (not ingredient text) avoids false positives from
ingredient names like "cake flour" or "sandwich bread".

Estimated browse impact after backfill (--force on 3.19M recipes):
  Breakfast: 43 → ~70k
  Dessert:   372 → ~350k  (real desserts, not flavor:Sweet)
  Snack:     57  → ~60k
  Beverage:  43  → ~36k
  Lunch:     69  → ~26k
2026-04-27 12:24:31 -07:00
6f097cd43d fix: wire browse domains to inferred_tag vocabulary, fix can_be leak in dietary
- Dinner: replace non-matching text keywords with main:X protein inferred tags (0 -> 815k results)
- All meal_type categories: add meal:X structured tag phrases
- Dietary: switch to dietary:X-only phrases; bare text keywords matched can_be:X
  tags (nearly all recipes), inflating counts to 1.3M+ falsely
- Cuisine: add cuisine:X structured tag phrases to Italian, Mexican, Asian,
  Indian, Mediterranean, American, BBQ, European, Latin American
- Side Dish: use main:Vegetables + main:Grains as proxy (no meal:Side Dish tag exists)
- Dessert: remove 'sweet' keyword (matched flavor:Sweet on all recipes)
- New dietary categories: Low-Sodium, Paleo

Closes #122. Partial progress on #123.
Follow-up: #125 (expand meal: tag inferrer coverage)
2026-04-27 11:38:37 -07:00
46778d62e3 fix: tab bar horizontal scroll on mobile, shorten Build Your Own label 2026-04-27 10:58:23 -07:00
896b4e048c feat: recipe scanner — photo to structured recipe (kiwi#9)
New feature: photograph a recipe card, cookbook page, or handwritten
note and have it extracted into a structured, editable recipe.

Backend:
- POST /recipes/scan: accept 1-4 photos, run VLM extraction, return
  structured JSON for review (not auto-saved)
- POST /recipes/scan/save: persist a reviewed/edited recipe
- GET/DELETE /recipes/user: user-created recipe CRUD
- Vision backend priority: cf-orch -> local Qwen2.5-VL -> Anthropic BYOK
- 503 with clear config hint when no vision backend available
- Multi-photo support: facing pages (ingredients/directions) sent together
- Pantry cross-reference: marks which ingredients are already on hand
- migration 041: user_recipes table (title, servings, cook_time, steps,
  ingredients JSON, source, pantry_match_pct)
- Tier gate: recipe_scan -> paid, BYOK-unlockable

Frontend:
- "Scan" button in the Recipes tab bar (camera icon)
- RecipeScanModal: upload step (drag-drop + file picker, up to 4 photos,
  live previews), processing step (spinner), review/edit step (all
  fields inline-editable before save), pantry match badge, warning banner
  for low-confidence or incomplete scans

Tests: 35 new tests (23 unit + 12 API), 404 total passing
2026-04-27 08:23:01 -07:00
c9fcfde694 feat(browse): active time estimation, prep scaling, required-ingredient filter
Some checks failed
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
Time effort (time_effort.py):
- Passive defaults per cooking technique (bake 30 min, slow cook 300 min, etc.)
- Prep action detection with n^0.75 quantity scaling for prep-needing ingredients
- Cross-reference ingredients/ingredient_names arrays to distribute quantity across steps
- Effort label now time-based (quick ≤20 min, moderate ≤45 min, involved >45 min)
- prep_min field added to StepAnalysis schema and Pydantic model
- All parse_time_effort call sites updated to pass ingredients + ingredient_names

Browse required-ingredient filter:
- New required_ingredient query param on GET /recipes/browse/{domain}/{category}
- Enter-to-commit input in RecipeBrowserPanel with auto-clear-on-empty watch
- Substring match via FTS5 ingredient_names column prefix filter
- FTS5 replaces LIKE '%X%' throughout browse_recipes and _browse_by_match
- _all + required_ingredient: 8.4s → 74ms; category + required_ingredient: 2s → 35ms
- _ingredient_fts_term() helper builds 'ingredient_names : "X"*' prefix queries
- Combined keywords + ingredient into single FTS MATCH to avoid secondary scans

Tests: 369/369 passing
2026-04-27 07:13:12 -07:00
e05bfe86f5 feat(recipes): orbital cadence — last-cooked chip and sort on saved recipes (#120)
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
2026-04-26 09:09:27 -07:00
95e76edaea feat(community): complete Layer A subcategory tagging (#118)
Some checks failed
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
- RecipeBrowserPanel: fix onTagSearchInput using '_all' domain slug
  (backend validates domain — was silently returning empty results)
- RecipeDetailPanel: fetch and display accepted community category tags
  on recipe open; accepted tags shown with accent chip + checkmark,
  pending tags shown in muted style
- browserAPI.listRecipeTags() was already in api.ts but not consumed —
  now wired into RecipeDetailPanel onMounted as a background fetch
2026-04-25 23:31:30 -07:00
12ab63e2fb feat: corrections router (#73) + Magpie flywheel hook (#28)
Corrections router (kiwi#73):
- Wire make_corrections_router() from cf-core at /api/v1/corrections
- Add get_db() dependency in session.py yielding store.conn (raw
  sqlite3.Connection as cf-core expects); cloud-aware via get_session
- Migration 040: corrections table + indexes (copied from cf-core DDL)
- Feeds Avocet SFT training pipeline via GET /corrections/export JSONL

Magpie flywheel hook (kiwi#28):
- app/services/magpie_hook.py: async fire_recipe_signal() that reads
  magpie_opt_in setting, checks external_id, POSTs anonymized payload
  to MAGPIE_INGEST_URL; stubs gracefully when URL unset or Magpie
  unreachable (DEBUG log, never raises)
- Hooks into save_recipe and update_saved_recipe as background tasks
- MAGPIE_INGEST_URL config key added to Settings
- SettingsView: "Data Sharing" toggle for magpie_opt_in, cloud-only
  (v-if VITE_CLOUD_MODE), plain-language consent label
2026-04-25 23:31:20 -07:00
9350719516 feat(recipes): LLM style classifier (#27) + cooked leftovers shelf-life (#112)
Some checks failed
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
Style classifier (kiwi#27):
- app/services/recipe/style_classifier.py: LLM prompt with curated vocab,
  cf-orch/LLMRouter fallback, JSON + regex tag extraction
- POST /recipes/saved/{recipe_id}/classify-style: Paid/BYOK tier gate,
  fetches recipe from corpus, returns {suggested_tags:[...]}
- SaveRecipeModal.vue: "Suggest tags" button with loading state; merges
  LLM suggestions into existing tags without overwriting user's choices
- 403/empty list silently ignored — button is a no-op when tier not met

Cooked leftovers shelf-life (kiwi#112):
- app/services/leftovers_predictor.py: deterministic FDA/USDA lookup table
  with shortest-component-wins for proteins and dish-type override for
  assembled dishes; special entries for ceviche (2d, acid != heat),
  fermented/cured (kimchi 14d, confit/lardo 7d), soups, rice, pasta, etc.
- POST /recipes/{recipe_id}/leftovers: free tier, no gate
- RecipeDetailPanel.vue: shelf-life section appears after "I cooked this"
  with fridge/freeze days, freeze-by advice, per-instance dismiss; calm
  framing per no-panic UX policy
- LeftoversResponse Pydantic schema added to recipe.py
2026-04-25 23:18:16 -07:00
9c4d8b7883 feat(recipe-engine): time-effort profile, product-label tokenisation, L1 tuning
Some checks failed
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
- Add TimeEffortProfile + StepAnalysis Pydantic schemas; serialised into
  RecipeSuggestion so the frontend receives active/passive/total minutes,
  effort label, and detected equipment per suggestion.
- parse_time_effort() now drives max_total_min filter (falls back to step-count
  estimate when directions contain no explicit time mentions).
- _PRODUCT_TOKEN_STOPWORDS: strips marketing/packaging words from multi-word
  product labels before adding individual ingredient tokens to pantry_set.
  "Organic Extra Firm Tofu" → adds "tofu"; improves packaged-food pantry match.
- L1 candidate pool raised to 60 (was 20); min_match_ratio lowered to 0.35
  (was 0.60) to keep enough results for plant-based / packaged-food pantries.
- household.py: tighten import to pull HEIMDALL_URL/ADMIN_TOKEN from
  services.heimdall_orch (matches refactor in cloud_session.py).
2026-04-25 21:44:26 -07:00
ed04b655be fix(saved-recipes): resolve FK constraint, null title, and load reliability
- Migration 039: drop saved_recipes.recipe_id FK (SQLite table rebuild).
  The FK referenced main.recipes but corpus lives in an ATTACH'd DB — caused
  500 on every POST /recipes/saved in cloud mode.
- _to_summary: row.get("title") or "" to handle corpus JOIN returning NULL
  title (e.g. placeholder recipe_id 99999).
- list_collections: return [] for Free tier instead of 403 — prevents
  Promise.all in savedStore.load() from aborting the saved-recipes fetch.
- savedStore.load(): switched to Promise.allSettled so a collections failure
  never blocks the saved list from populating.
- RecipesView: star indicator now reflects savedStore.isSaved() (server-side
  saved state) rather than localStorage bookmarks; changed to <span> since
  the star is now read-only visual feedback.
- Removed { immediate: true } from saved-tab watcher — premature bounce to
  Build Your Own before onMounted load() completes.
2026-04-25 21:44:10 -07:00
f6b29693c8 refactor: replace hand-rolled JWT+Heimdall with cf-core CloudSessionFactory
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
Delegates JWT validation, Heimdall provision/tier-resolve, bypass-IP
handling, and guest session management to circuitforge_core. Kiwi keeps
its own CloudUser (db path, household fields, BYOK flag) and DB helpers.
detect_byok() is now imported from cf-core instead of a local copy.
household_id/is_household_owner/license_key flow through core_user.meta
(cf-core already forwards all Heimdall response extras into meta).
Removes ~217 lines of duplicated auth code.

Note: guest cookie name changes from kiwi_guest_id to cf_guest_id (cf-core
managed). Existing guest sessions get a new UUID on first visit — acceptable
for alpha.
2026-04-25 16:35:56 -07:00
b86b7732dc fix(pwa): set start_url/scope from VITE_BASE_URL so install launches /kiwi/ not site root
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
2026-04-25 12:59:59 -07:00
7e0722cc23 feat(pwa): add Progressive Web App support — installable to homescreen
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
- vite-plugin-pwa with generateSW strategy (Workbox)
- manifest.webmanifest: name, short_name, display standalone, theme_color #e8a820
- Service worker: precaches JS/CSS/HTML shell; API routes network-first (60s);
  Google Fonts cache-first (1 year)
- Icons: 192 + 512px regular + maskable variants generated from App.vue bird SVG
- index.html: theme-color meta, apple-touch-icon, apple-mobile-web-app-* tags
  for iOS Safari homescreen support (iOS ignores the manifest icons array)
- autoUpdate mode: new versions install silently and activate on next navigation
2026-04-25 12:33:22 -07:00
e2c358c90a fix: extend source CHECK constraints to include visual_capture (kiwi#79)
Some checks failed
CI / Frontend (Vue) (push) Waiting to run
CI / Backend (Python) (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
Migrations 037 and 038 rebuild products and inventory_items tables
to add 'visual_capture' as a valid source value, which the label-confirm
endpoint sets when saving user-verified nutrition label data.
Adds 2 schema tests covering the new allowed value.
2026-04-25 08:46:44 -07:00
0bac494ecd chore: bump to v0.6.0, fix TS build errors, remove cf-orch sidecar
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
Release / release (push) Waiting to run
- Bump version to 0.6.0 (visual label capture release)
- Remove unused TimeEffortProfile import in RecipeDetailPanel.vue
- Prefix unused value params with _ in SettingsView.vue sensory fns
- Remove cf-orch agent sidecar from compose.override.yml (Sif now has
  its own dedicated systemd cf-orch-agent service)
2026-04-24 21:19:44 -07:00
17e62c451f feat: visual label capture for unenriched barcodes (kiwi#79)
When a barcode scan finds no product in FDC/OFF, paid-tier users now see a
"Capture label" offer instead of a dead-end "add manually" prompt.

Backend:
- Migration 036: captured_products local cache table (keyed by barcode,
  UPSERT on conflict so re-capture refreshes rather than errors)
- store.get_captured_product / save_captured_product (with JSON decode for
  ingredient_names and allergens)
- app/services/label_capture.py: wraps cf-core VisionRouter (caption API);
  graceful fallback to zero-confidence mock when stub/error; JSON fence
  stripping; confidence clamped to [0,1]; KIWI_LABEL_CAPTURE_MOCK=1 for tests
- New schemas: LabelCaptureResponse, LabelConfirmRequest, LabelConfirmResponse
- POST /inventory/scan/label-capture — image to extraction (paid+ gate, 403)
- POST /inventory/scan/label-confirm — save confirmed product + optional
  inventory add
- Both scan endpoints now: check captured_products cache before FDC/OFF;
  set needs_visual_capture=True for gap products on paid tier; BarcodeScanResult
  gains needs_visual_capture field
- visual_label_capture feature gate added to tiers.py (paid)

Tests: 42 new tests (service, store/migration, API endpoints) — 367 total passing

Frontend:
- InventoryList.vue: capturePhase state machine (offer => uploading => reviewing)
- Offer card appears after scan gap (calm UX: no urgency, Discard always visible)
- Review form: pre-populated from extraction; amber label highlights for
  unread fields (confidence < 0.7); comma-separated ingredients/allergens
- api.ts: LabelCaptureResult + LabelConfirmRequest types; captureLabelPhoto()
  and confirmLabelCapture() API methods
2026-04-24 17:57:25 -07:00
3463aa1e17 feat: wire dietary constraints into secondary use filter on all inventory endpoints
_user_constraints() loads dietary_constraints from user_settings once per
request. All 7 _enrich_item call sites now pass constraints so wine (and
any future alcohol-containing entries) are suppressed for halal/alcohol-free
users at the API response layer.
2026-04-24 17:12:39 -07:00
e45b07c203 feat: expand secondary use windows + dietary constraint filter (kiwi#110)
Adds 10 new secondary use entries and corrects all 8 existing ones.
New: apples/soft, leafy_greens/wilting, tomatoes/soft, cooked_pasta/day-old,
cooked_potatoes/day-old, yogurt/tangy, cream/sour, wine/open,
cooked_beans/day-old, cooked_meat/leftover.

Corrections: milk uses (specific recipes, not 'baking'/'sauces'); dairy uses
expanded; cheese label well-aged→rind-ready with named dishes (minestrone,
ribollita); rice uses (onigiri, arancini, congee); tortillas warning added;
bakery uses and synonyms expanded to named pastries; bananas synonyms
(spotty/brown/black/mushy); rice synonyms (old rice).

New fields on every SECONDARY_WINDOW entry:
- discard_signs: qualitative cues for when the item has gone past its
  secondary window (shown in UI alongside uses)
- constraints_exclude: dietary labels that suppress the entry entirely
  (wine suppressed for halal/alcohol-free)

ExpirationPredictor.filter_secondary_by_constraints() applies constraint
suppression; _enrich_item() now accepts user_constraints and passes
secondary_discard_signs through to the API response.
2026-04-24 17:08:45 -07:00
b5eb8e4772 feat: cross-encoder reranker for recipe suggestions (kiwi#117)
Integrates cf-core reranker into the L1/L2 recipe engine. Paid+ tier
gets a BGE cross-encoder pass over the top-20 FTS candidates, scoring
each recipe against the user's full context: pantry state, dietary
constraints, allergies, expiry urgency, style preference, and effort
preference. Free tier keeps the existing overlap sort unchanged.

- New app/services/recipe/reranker.py: build_query, build_candidate_string,
  rerank_suggestions with tier gate (_RERANKER_TIERS) and graceful fallback
- rerank_score field added to RecipeSuggestion (None on free tier, float on paid+)
- recipe_engine.py: single call after candidate assembly, before final sort;
  hard_day_mode tier grouping preserved as primary sort when reranker active
- Fix pre-existing circular import in app/services/__init__.py (eager import
  of ReceiptService triggered store.py → services → receipt_service → store)
- 27 unit tests (mock backend, no model weights) + 2 engine-level tier tests;
  325 tests passing, no regressions
2026-04-24 16:39:51 -07:00
91867f15f4 feat(streaming): add COORDINATOR_URL and COORDINATOR_KIWI_KEY to cloud compose 2026-04-24 10:26:53 -07:00
1182c6cffb feat(streaming): add EventSource streaming UI to RecipesView 2026-04-24 10:25:35 -07:00
7292c5e7fc feat(streaming): add StreamTokenResponse type and getRecipeStreamToken API 2026-04-24 10:23:09 -07:00
63517d135b feat(streaming): add POST /recipes/stream-token endpoint 2026-04-24 10:22:30 -07:00
2547f80893 feat(streaming): add StreamTokenRequest/Response schemas 2026-04-24 10:19:18 -07:00
0996ea8c7a feat(streaming): add coordinator_proxy service module 2026-04-24 10:18:40 -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
521cb419bc feat: sensory profile filter — texture/smell/noise filtering for Browse and Find (kiwi#51)
- Migration 035: add sensory_tags column to recipes (default '{}')
- scripts/tag_sensory_profiles.py: batch tagger using ingredient names,
  direction keywords, and ingredient_profiles texture data
- app/services/recipe/sensory.py: SensoryExclude frozen dataclass,
  build_sensory_exclude(), passes_sensory_filter() with graceful degradation
  (untagged recipes always pass; malformed JSON always passes)
- store.browse_recipes and _browse_by_match: accept SensoryExclude, apply
  filter in recipe-building loop (default path) and scoring loop (match sort)
- recipe_engine.suggest: load sensory_preferences from settings, apply
  passes_sensory_filter() after exclude_set check in the rows loop
- settings endpoint: add sensory_preferences to _ALLOWED_KEYS
- Frontend: SensoryPreferences types in api.ts; sensoryPreferences state and
  saveSensory() action in settings store; Sensory section in SettingsView with
  texture avoid pills, smell/noise tolerance scale pills with ok/limit/neutral
  color coding
- 66 new tests (29 classification + 13 sensory service + 2 settings); 281 total
2026-04-24 09:47:48 -07:00
302285a1a5 feat: step-by-step cook mode with progress bar, keyboard nav, and swipe (kiwi#49)
- Cook/Exit toggle button in recipe detail header (hidden for recipes with no steps)
- Cook mode progress bar between header and body showing step N of M
- Single-step view replaces recipe body; shows Active/Wait badge and passive hint
  from #50 time_effort data (null-safe — degrades gracefully without it)
- Prev/Next nav buttons; Next becomes green Done on last step
- ArrowLeft/ArrowRight keyboard navigation (preventDefault to suppress scroll)
- Touch swipe left/right (40px horizontal threshold, 80px vertical abort)
- Done triggers handleCook() then exitCookMode() so success banner appears instantly
2026-04-24 09:35:12 -07:00
b1e187c779 feat: time & effort signals — active/passive split, effort cards, annotated steps (kiwi#50)
- Add app/services/recipe/time_effort.py: parse_time_effort(), TimeEffortProfile,
  StepAnalysis dataclasses; two-branch regex for time ranges and single values;
  whole-word passive keyword detection; 480 min/step cap; 1825 day global cap
- Add directions to browse_recipes and _browse_by_match SELECT queries in store.py
- Enrich browse and detail endpoints with active_min/passive_min/time_effort fields
- Add StepAnalysis, TimeEffortProfile TS interfaces to api.ts
- RecipeBrowserPanel: split pill badge showing active/passive time
- RecipeDetailPanel: collapsible ingredients summary, effort cards (Active/Hands-off/Total),
  equipment chips, annotated step list with Active/Wait badges and passive hints
- 45 new tests (40 unit + 5 API); 215 total passing
2026-04-24 09:29:54 -07:00
70205ebb25 feat(recipe-tags): 'Categorize this' CTA and tag submission modal
Zero-count subcategory buttons show a + badge. Clicking opens a modal:
- Recipe search (debounced, 3-char min) using existing browse API
- Pre-filled domain/category/subcategory from current browse context,
  fully correctable via selects populated from loaded domains/categories
- Submit calls POST /recipes/community-tags; 409 on duplicate
- Success message: 'It will appear once a second user confirms'

api.ts: adds submitRecipeTag(), upvoteRecipeTag(), listRecipeTags() to browserAPI.
CSS: tag-cta pill on subcat buttons, modal-backdrop + modal-box with theme vars.

TODO: wire real community pseudonym (currently hardcoded 'anon').
Refs kiwi#118.
2026-04-22 12:37:56 -07:00
9697c7b64f feat(recipe-tags): merge accepted community tags into browse counts + FTS fallback
browse_counts_cache.py: after FTS counts, _merge_community_tag_counts() queries
  accepted tags (upvotes>=2) grouped by (domain,category,subcategory) and adds
  distinct recipe_id counts to the cached keyword-set totals. Skips silently
  when community Postgres is unavailable.

store.py: fetch_recipes_by_ids() fetches corpus recipes by explicit ID list,
  used by the FTS fallback when a subcategory returns zero FTS results.

recipes.py (browse endpoint): when FTS total==0 for a subcategory, queries
  community store for accepted tag IDs and serves those recipes directly.
  Sets community_tagged=True in the response so the UI can surface context.
  Refs kiwi#118.
2026-04-22 12:37:44 -07:00
f962748073 feat(recipe-tags): community subcategory tagging API endpoints
GET  /recipes/community-tags/{recipe_id} — all tags for a recipe
POST /recipes/community-tags             — submit tag (requires pseudonym)
POST /recipes/community-tags/{id}/upvote — vote on a tag

Validates (domain, category, subcategory) against DOMAINS taxonomy before
accepting. Returns 409 on duplicate submission or double-vote. Fails soft
(503) when community Postgres is unavailable so the browse path is unaffected.
Refs kiwi#118.
2026-04-22 12:37:32 -07:00