From ed04b655bec93c8fa325db8049cc4f85ed32bc18 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Sat, 25 Apr 2026 21:44:10 -0700 Subject: [PATCH] fix(saved-recipes): resolve FK constraint, null title, and load reliability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 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. --- app/api/endpoints/saved_recipes.py | 6 ++-- .../migrations/039_saved_recipes_drop_fk.sql | 31 +++++++++++++++++++ frontend/src/components/RecipesView.vue | 15 ++++----- frontend/src/stores/savedRecipes.ts | 9 ++++-- 4 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 app/db/migrations/039_saved_recipes_drop_fk.sql diff --git a/app/api/endpoints/saved_recipes.py b/app/api/endpoints/saved_recipes.py index c0a2efa..8f9616e 100644 --- a/app/api/endpoints/saved_recipes.py +++ b/app/api/endpoints/saved_recipes.py @@ -35,7 +35,7 @@ def _to_summary(row: dict, store: Store) -> SavedRecipeSummary: return SavedRecipeSummary( id=row["id"], recipe_id=row["recipe_id"], - title=row.get("title", ""), + title=row.get("title") or "", saved_at=row["saved_at"], notes=row.get("notes"), rating=row.get("rating"), @@ -104,8 +104,10 @@ async def list_saved_recipes( async def list_collections( session: CloudUser = Depends(get_session), ) -> list[CollectionSummary]: + # Free users can list (they'll always have zero — creating requires Paid). + # Returning 403 here breaks savedStore.load() via Promise.all for non-Paid users. if not can_use("recipe_collections", session.tier): - raise HTTPException(status_code=403, detail="Collections require Paid tier.") + return [] rows = await asyncio.to_thread( _in_thread, session.db, lambda s: s.get_collections() ) diff --git a/app/db/migrations/039_saved_recipes_drop_fk.sql b/app/db/migrations/039_saved_recipes_drop_fk.sql new file mode 100644 index 0000000..fa03e49 --- /dev/null +++ b/app/db/migrations/039_saved_recipes_drop_fk.sql @@ -0,0 +1,31 @@ +-- Migration 039: Drop FK constraint on saved_recipes.recipe_id. +-- +-- In cloud mode the recipe corpus is ATTACHed as a separate database. +-- SQLite FK constraints only resolve against the `main` schema, so +-- `REFERENCES recipes(id)` was always failing for cloud saves (the +-- main.recipes table is empty; all data lives in corpus.recipes). +-- The corpus is read-only and never modified by the app, so cascade-on-delete +-- is meaningless anyway. Remove the constraint without changing any data. + +PRAGMA foreign_keys = OFF; + +CREATE TABLE saved_recipes_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + recipe_id INTEGER NOT NULL, + saved_at TEXT NOT NULL DEFAULT (datetime('now')), + notes TEXT, + rating INTEGER CHECK (rating IS NULL OR (rating >= 0 AND rating <= 5)), + style_tags TEXT NOT NULL DEFAULT '[]', + UNIQUE (recipe_id) +); + +INSERT INTO saved_recipes_new SELECT * FROM saved_recipes; + +DROP TABLE saved_recipes; + +ALTER TABLE saved_recipes_new RENAME TO saved_recipes; + +CREATE INDEX IF NOT EXISTS idx_saved_recipes_saved_at ON saved_recipes (saved_at DESC); +CREATE INDEX IF NOT EXISTS idx_saved_recipes_rating ON saved_recipes (rating); + +PRAGMA foreign_keys = ON; diff --git a/frontend/src/components/RecipesView.vue b/frontend/src/components/RecipesView.vue index 919e13b..f139746 100644 --- a/frontend/src/components/RecipesView.vue +++ b/frontend/src/components/RecipesView.vue @@ -513,12 +513,12 @@ ~{{ recipe.estimated_time_min }}m Level {{ recipe.level }} Wildcard - + :class="['btn-icon', 'btn-bookmark', { active: savedStore.isSaved(recipe.id) }]" + :aria-label="savedStore.isSaved(recipe.id) ? 'Saved: ' + recipe.title : recipe.title" + :title="savedStore.isSaved(recipe.id) ? 'Saved' : 'Not saved'" + >{{ savedStore.isSaved(recipe.id) ? '★' : '☆' }}