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
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
#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 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
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
- 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
- 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
Both functions are DB-free public API additions to assembly_recipes.py.
get_role_candidates() scores pantry candidates against a wizard step using
element-profile overlap with prior picks; build_from_selection() builds a
RecipeSuggestion from explicit role overrides with required-role validation.
Extends AssemblyTemplate dataclass with slug, icon, descriptor, and
role_hints fields. Updates all 13 template instantiations with
appropriate values. Adds _TEMPLATE_BY_SLUG lookup dict and
get_templates_for_api() serialiser for the templates endpoint.
Hard Day Mode now prioritises results by effort tier before match_count:
Tier 0 (premade): frozen/instant title keywords, or ≤2 ingredients with
heat/microwave-only steps (frozen dinner, heat-and-eat, microwave meal)
Tier 1 (super simple): ≤3 ingredients + any easy method (quesadilla,
cheese toast, scrambled eggs)
Tier 2 (easy/moderate): everything else that passed the 'involved' filter
Assembly templates default to tier 1 (inherently simple). Normal mode sort
is unchanged — match_count only.
Introduces a thin HTTP client for the cf-docuvision service and wires it
as a fast path in VisionLanguageOCR.extract_receipt_data(). When CF_ORCH_URL
is set, the pipeline attempts docuvision allocation via CFOrchClient before
loading the heavy local VLM; falls back gracefully if unavailable.
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.