diff --git a/app/api/endpoints/recipes.py b/app/api/endpoints/recipes.py index 0ccfbe8..557500c 100644 --- a/app/api/endpoints/recipes.py +++ b/app/api/endpoints/recipes.py @@ -212,11 +212,27 @@ async def build_recipe( for item in items if item.get("product_name") } - return build_from_selection( + suggestion = build_from_selection( template_slug=req.template_id, role_overrides=req.role_overrides, pantry_set=pantry_set, ) + if suggestion is None: + return None + # Persist to recipes table so the result can be saved/bookmarked. + # external_id encodes template + selections for stable dedup. + import hashlib as _hl, json as _js + sel_hash = _hl.md5( + _js.dumps(req.role_overrides, sort_keys=True).encode() + ).hexdigest()[:8] + external_id = f"assembly:{req.template_id}:{sel_hash}" + real_id = store.upsert_built_recipe( + external_id=external_id, + title=suggestion.title, + ingredients=suggestion.matched_ingredients, + directions=suggestion.directions, + ) + return suggestion.model_copy(update={"id": real_id}) finally: store.close() diff --git a/app/api/routes.py b/app/api/routes.py index 97e2637..c19770c 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -9,11 +9,11 @@ api_router.include_router(receipts.router, prefix="/receipts", tags= api_router.include_router(ocr.router, prefix="/receipts", tags=["ocr"]) api_router.include_router(export.router, tags=["export"]) api_router.include_router(inventory.router, prefix="/inventory", tags=["inventory"]) +api_router.include_router(saved_recipes.router, prefix="/recipes/saved", tags=["saved-recipes"]) api_router.include_router(recipes.router, prefix="/recipes", tags=["recipes"]) api_router.include_router(settings.router, prefix="/settings", tags=["settings"]) api_router.include_router(staples.router, prefix="/staples", tags=["staples"]) api_router.include_router(feedback.router, prefix="/feedback", tags=["feedback"]) api_router.include_router(household.router, prefix="/household", tags=["household"]) -api_router.include_router(saved_recipes.router, prefix="/recipes/saved", tags=["saved-recipes"]) api_router.include_router(imitate.router, prefix="/imitate", tags=["imitate"]) api_router.include_router(community_router) \ No newline at end of file diff --git a/app/db/store.py b/app/db/store.py index 65fafdd..0e6546f 100644 --- a/app/db/store.py +++ b/app/db/store.py @@ -686,6 +686,44 @@ class Store: def get_recipe(self, recipe_id: int) -> dict | None: return self._fetch_one("SELECT * FROM recipes WHERE id = ?", (recipe_id,)) + def upsert_built_recipe( + self, + external_id: str, + title: str, + ingredients: list[str], + directions: list[str], + ) -> int: + """Persist an assembly-built recipe and return its DB id. + + Uses external_id as a stable dedup key so the same build slug doesn't + accumulate duplicate rows across multiple user sessions. + """ + import json as _json + self.conn.execute( + """ + INSERT OR IGNORE INTO recipes + (external_id, title, ingredients, ingredient_names, directions, source) + VALUES (?, ?, ?, ?, ?, 'assembly') + """, + ( + external_id, + title, + _json.dumps(ingredients), + _json.dumps(ingredients), + _json.dumps(directions), + ), + ) + # Update title in case the build was re-run with tweaked selections + self.conn.execute( + "UPDATE recipes SET title = ? WHERE external_id = ?", + (title, external_id), + ) + self.conn.commit() + row = self._fetch_one( + "SELECT id FROM recipes WHERE external_id = ?", (external_id,) + ) + return row["id"] # type: ignore[index] + def get_element_profiles(self, names: list[str]) -> dict[str, list[str]]: """Return {ingredient_name: [element_tag, ...]} for the given names. diff --git a/app/services/recipe/assembly_recipes.py b/app/services/recipe/assembly_recipes.py index a9c1ca5..c68e6ca 100644 --- a/app/services/recipe/assembly_recipes.py +++ b/app/services/recipe/assembly_recipes.py @@ -995,6 +995,12 @@ def build_from_selection( effective_pantry = pantry_set | set(role_overrides.values()) title = _personalized_title(tmpl, effective_pantry, seed + tmpl.id) + # Items in role_overrides that aren't in the user's pantry = shopping list + missing = [ + item for item in role_overrides.values() + if item and item not in pantry_set + ] + return RecipeSuggestion( id=tmpl.id, title=title, @@ -1002,7 +1008,7 @@ def build_from_selection( element_coverage={}, swap_candidates=[], matched_ingredients=all_matched, - missing_ingredients=[], + missing_ingredients=missing, directions=tmpl.directions, notes=tmpl.notes, level=1, diff --git a/frontend/src/components/BuildYourOwnTab.vue b/frontend/src/components/BuildYourOwnTab.vue index 108f8b7..0c63e02 100644 --- a/frontend/src/components/BuildYourOwnTab.vue +++ b/frontend/src/components/BuildYourOwnTab.vue @@ -186,6 +186,13 @@ @close="phase = 'select'" @cooked="phase = 'select'" /> + +
{{ addError }}
✓ Added to pantry!
- +