Adds minimal sort/search to the recipe browser for cognitive access diversity —
linear scanners, alphabet browsers, and keyword diggers each get a different
way in without duplicating the full search tab.
- browse_recipes: q (LIKE title filter) + sort (default/alpha/alpha_desc)
- API endpoint: q/sort query params with validation
- Frontend: debounced search input (350ms) + sort pills (Default/A→Z/Z→A)
- Search and sort reset on domain/category change
- _all path supports q+sort; keyword-FTS path adds AND filter on top
Adds a two-level browse tree (domain → category → subcategory) to the
recipe browser, plus an "All" unfiltered option at the top of every
domain.
browser_domains.py:
- Category values now support list[str] (flat) or dict with "keywords"
and "subcategories" keys — backward compatible with all existing flat
categories
- Added subcategories to: Italian (Sicilian, Neapolitan, Tuscan, Roman,
Venetian, Ligurian), Mexican (Oaxacan, Yucatecan, Veracruz, Street
Food, Mole), Asian (Korean, Japanese, Chinese, Thai, Vietnamese,
Filipino, Indonesian), Indian (North, South, Bengali, Gujarati),
Mediterranean (Greek, Turkish, Moroccan, Lebanese, Israeli), American
(Southern, Cajun/Creole, BBQ, Tex-Mex, New England), European
(French, Spanish, German, British/Irish, Scandinavian), Latin American
(Peruvian, Brazilian, Colombian, Cuban, Caribbean), Dinner, Lunch,
Breakfast, Snack, Dessert, Chicken, Beef, Pork, Fish, Vegetables
- New helpers: category_has_subcategories, get_subcategory_names,
get_keywords_for_subcategory
store.py:
- get_browser_categories now accepts has_subcategories_by_category and
includes has_subcategories: bool in each result row
- New get_browser_subcategories method for subcategory count queries
recipes.py endpoints:
- GET /browse/{domain}/{category}/subcategories — returns subcategory
list with recipe counts (registered before /{subcategory} to avoid
path collision)
- GET /browse/{domain}/{category} gains optional ?subcategory=X param
to narrow results within a category
- GET /browse/{domain}/{category}/_all — unfiltered paginated browse
(landed in previous commit)
api.ts: BrowserCategory adds has_subcategories; new BrowserSubcategory
type; listSubcategories() call; browse() gains subcategory param
RecipeBrowserPanel.vue:
- Category pills show a › indicator when subcategories exist
- Selecting such a category fetches subcategories in the background
(non-blocking — recipes load immediately at the category level)
- Subcategory row appears below the category list with an
"All [Category]" pill + one pill per subcategory with count
- Active subcategory highlighted; clicking "All [Category]" resets
to the full category view
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
Add E2E_TEST_USER_ID setting (opt-in via env); session bootstrap logs
at DEBUG instead of INFO for the known test user so test runs don't
inflate session counts. Still visible with DEBUG=true.
- App.vue: lazy-mount pattern (v-if + v-show) so non-active tabs only mount on
first visit, eliminating concurrent onMounted calls across all components (#98)
- nginx.cloud.conf: add /kiwi/api/ location to proxy API calls on direct-port
access (localhost:8515); was serving SPA HTML → causing M.map/filter/find
TypeError chain on load (#98)
- nginx.cloud.conf: $host → $http_host so 307 redirects preserve port number (#107)
- RecipeBrowserPanel: show friendly "corpus not loaded" notice and skip auto-select
when all category counts are 0, instead of rendering confusing empty buttons (#106)
- Defensive Array.isArray guards in inventory store, mealPlan store, ReceiptsView
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
- MealPlanGrid: edit-mode toggle (visible when >1 meal type) reveals
per-row ↑/↓ reorder and ✕ remove controls; label column expands to
auto width via CSS class swap
- mealPlan store: removeMealType() and reorderMealTypes() — both send
the full updated array to the existing PATCH /meal-plans/{id} endpoint
- MealPlanView: wires remove-meal-type and reorder-meal-types emits;
shares mealTypeAdding loading flag to disable controls during save
- Guard: remove disabled when only one type remains (always keep ≥1)
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)
- Add autoSelectPlan() to the store: after loadPlans() resolves, set
activePlan to the current week's plan (or most recent) without a second
API round-trip -- list already returns full PlanSummary with slots
- Call autoSelectPlan(mondayOfCurrentWeek()) in onMounted so the grid
populates immediately without the user touching the dropdown
- Make onNewPlan idempotent: if a 409 comes back, activate the existing
plan for that week instead of surfacing an error to the user
- Fix sqlite3.OperationalError: the recipes table uses `title` not `name`;
get_plan_slots JOIN was crashing every list_plans call with a 500,
making the + New week button appear broken (plans were being created
silently but the selector refresh always failed)
- Add migration 032 to add UNIQUE INDEX on meal_plans(week_start)
to prevent duplicate plans accumulating while the button was broken
- Raise HTTP 409 on IntegrityError in create_plan so duplicates produce
a clear error instead of a 500
- Fix mondayOfCurrentWeek to build the date string from local date parts
instead of toISOString(), which converts through UTC and can produce the
wrong calendar day for UTC+ timezones
- Add planCreating/planError state to MealPlanView so button shows
"Creating..." during the request and displays errors inline
- Refactor _lookup_in_database to accept a shared httpx.AsyncClient so
all three Open*Facts database attempts reuse one TLS connection instead
of opening a new one per call; restores pre-fallback scan speed
- Increase recipe suggest timeout to 120s (was 30s) to survive cf-orch
model cold-start on first request of a session
- Include product brand in barcode scan success message so the user can
clearly see what was found (e.g. "Added: Cheerios (General Mills) to pantry")
#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
Adds GET /export/json that bundles inventory and saved recipes into a
single timestamped JSON file for data portability. The export envelope
includes schema version and export timestamp so future import logic can
handle version differences.
Frontend: new primary-styled JSON download button in the Export card with
a short description of what is included.
Closes#62
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
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
#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#12Closes#60
Aligns llm_recipe.py with the pattern already used by the meal plan
service. cf-text routes through a lighter GGUF/llama.cpp path and
shares VRAM budget with other products via cf-orch, rather than
requiring a dedicated vLLM process. Also drops model_candidates
(not applicable to cf-text allocation).
Closes#70
- Backend: new /api/v1/feedback/attach endpoint uploads image to
Forgejo as an issue asset, then pins it as a comment so the
screenshot is visible inline on the issue
- Frontend: file input in feedback form (all types, max 5 MB)
with inline thumbnail preview and remove button
- Attach call is non-fatal: if upload fails after issue creation,
the issue is still filed and the user sees success
- Screenshot state clears on modal reset
Closes#82
- 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
- #42: Replace deficit framing — "You'd need:" → "To complete this recipe:", element_gaps
card-warning → card-secondary, missing/gap chips status-warning → status-info,
"Your pantry is missing..." → "These would expand your options:"
- #46: Add activeNutritionFilterCount computed; show count in Advanced filters summary
when filters are active so it's visible while collapsed
- #47: Wildcard confirmation status-warning → status-info, copy updated to calm framing;
wildcard recipe card badge status-warning → status-info
- M1: Add re-search hint below Hard Day Mode toggle when results are already showing
- M8: Move swap candidates collapsible to after directions/steps section
- L2: Add autocomplete="off" to filter search, constraint, and allergy text inputs
- L5: Add title="This is an affiliate link" disclosure to grocery affiliate links
Items already correct (no change needed):
- M2: Level description already always visible via activeLevel computed
- M3: Rate limit copy already using calm framing
- M5: No-results copy already calm
- M6: levelLabels already uses full names
- M7: "that's part of the fun" was part of the wildcard copy fixed under #47
- L1: Neon/konami handler not present in this file
- Import and mount OrchUsagePill near the recipe level selector in RecipesView;
pill is self-hiding when not enabled or no lifetime key is present
- Add useOrchUsage composable to SettingsView with a Display section checkbox
so users can opt in to seeing the cloud recipe call budget pill
- Add @/ path alias to vite.config.ts and tsconfig.app.json to resolve the
existing @/services/api import in useOrchUsage.ts (fixes vite build error)
- tsc --noEmit and vite build both pass clean
Adds full meal planning workflow to Kiwi:
- Weekly meal plan creation with configurable meal types (Paid gate)
- Drag-and-assign recipe slots per day
- Prep session generation with sequenced task lists and time estimates
- LLM-assisted full-week plan and timing fill-in (BYOK-unlockable)
- Community feed (local ActivityPub-compat + cloud federation)
- Build Your Own recipe tab with assembly templates
- Save/bookmark any recipe with star rating, notes, and style tags
- Shopping list export from built recipes
- Tab reorder: Saved > Build > Community > Find > Browse
- Auto-redirect from empty Saved tab to Build
- Custom ingredient injection persists in candidate list
- z-index fix: save modal above recipe detail panel
- Route ordering fix: /recipes/saved before /{recipe_id} catch-all
- Use monkeypatch.setattr to patch cloud_session._LOCAL_KIWI_DB
instead of wrong KIWI_DB_PATH env var (module-level singleton
computed at import time; env var had no effect)
- Assert id > 0 (real persisted DB id) instead of -1 (old
pre-persistence sentinel value)
- Persist built recipes to recipes table on /build so they get real DB IDs
and can be bookmarked via saved_recipes (FK was pointing at negative IDs)
- Populate missing_ingredients in build_from_selection() from role_overrides
vs pantry diff -- backend now owns shopping list computation
- Remove client-side cartItems tracking; shopping list derived from
builtRecipe.missing_ingredients instead
- Fix saved_recipes 422: mount saved_recipes router before recipes router in
routes.py so /recipes/saved isn't captured by /recipes/{recipe_id}
- Bump SaveRecipeModal z-index to 500 (above detail-overlay at 400)
- Replace "Add to pantry" primary action with "Grocery list" clipboard copy;
"Add to pantry" demoted to compact secondary button