From 9e08d9cce36eed7c41741a97adb225cebba7f7b8 Mon Sep 17 00:00:00 2001 From: LyAhn Date: Sat, 6 Jun 2026 22:09:54 +0100 Subject: [PATCH] fix: three more correctness bugs from PR review - upsert_image now resets ai_tagged_at, ai_rating, ai_tagger_model, and ai_tagger_error to NULL and deletes AI-sourced image_tags when a file is reindexed after content changes, preventing stale tags/ratings from the previous version being shown and blocking re-tagging - get_pending_tagging_jobs_excluding now applies the paused-folder exclusion in SQL before LIMIT using folder_exclusion_clause, matching the pattern used by caption and embedding job claimers; previously a large paused-folder backlog could starve all unpaused folders - Merged similarRequestToken into galleryRequestToken so all gallery-producing requests share one generation counter; a late similarity response can no longer overwrite a subsequently loaded folder, and vice versa --- src-tauri/src/db.rs | 24 +++++++++++++++++------- src/store.ts | 22 ++++++++++++---------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 22f009f..15134a2 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -349,7 +349,11 @@ pub fn upsert_image(conn: &Connection, img: &ImageRecord) -> Result { generated_caption = excluded.generated_caption, caption_model = excluded.caption_model, caption_updated_at = excluded.caption_updated_at, - caption_error = excluded.caption_error + caption_error = excluded.caption_error, + ai_rating = NULL, + ai_tagger_model = NULL, + ai_tagged_at = NULL, + ai_tagger_error = NULL RETURNING id", params![ img.folder_id, @@ -385,6 +389,12 @@ pub fn upsert_image(conn: &Connection, img: &ImageRecord) -> Result { ], |row| row.get(0), )?; + // Remove stale AI tags when content changes so they aren't shown for the + // new file. User-sourced tags are preserved. + conn.execute( + "DELETE FROM image_tags WHERE image_id = ?1 AND source = 'ai'", + [id], + )?; Ok(id) } @@ -1678,14 +1688,17 @@ fn get_pending_tagging_jobs_excluding( paused_folder_ids: &std::collections::HashSet, limit: usize, ) -> Result> { - let mut stmt = conn.prepare( + let sql = format!( "SELECT j.image_id, i.folder_id, i.path FROM tagging_jobs j JOIN images i ON i.id = j.image_id WHERE j.status = 'pending' + {} ORDER BY j.created_at ASC LIMIT ?1", - )?; + folder_exclusion_clause("i", paused_folder_ids) + ); + let mut stmt = conn.prepare(&sql)?; let rows = stmt .query_map([limit as i64], |row| { Ok(TaggingJob { @@ -1695,10 +1708,7 @@ fn get_pending_tagging_jobs_excluding( }) })? .collect::>>()?; - Ok(rows - .into_iter() - .filter(|job| !paused_folder_ids.contains(&job.folder_id)) - .collect()) + Ok(rows) } pub fn update_ai_tags( diff --git a/src/store.ts b/src/store.ts index dd82273..9730662 100644 --- a/src/store.ts +++ b/src/store.ts @@ -365,8 +365,10 @@ const PAGE_SIZE = 200; const AI_CAPTIONS_ENABLED_KEY = "phokus.aiCaptionsEnabled"; const SIMILAR_DISTANCE_THRESHOLD = 0.24; +// Single token shared by all gallery-producing requests (folder loads, searches, +// similarity, region search). Any new request increments it so a stale response +// from a previous collection type cannot overwrite newer results. let galleryRequestToken = 0; -let similarRequestToken = 0; let tagCloudRequestToken = 0; let exploreTagRequestToken = 0; @@ -902,7 +904,7 @@ export const useGalleryStore = create((set, get) => ({ }, showVisualCluster: async (imageIds) => { - const requestToken = ++similarRequestToken; + const requestToken = ++galleryRequestToken; set((state) => ({ activeView: "gallery", search: "", @@ -923,7 +925,7 @@ export const useGalleryStore = create((set, get) => ({ const images = await invoke("get_images_by_ids", { params: { image_ids: imageIds }, }); - if (requestToken !== similarRequestToken) return; + if (requestToken !== galleryRequestToken) return; set({ images, totalImages: images.length, @@ -933,7 +935,7 @@ export const useGalleryStore = create((set, get) => ({ collectionTitle: "Explore Cluster", }); } catch (error) { - if (requestToken !== similarRequestToken) return; + if (requestToken !== galleryRequestToken) return; set({ images: [], totalImages: 0, @@ -951,7 +953,7 @@ export const useGalleryStore = create((set, get) => ({ }, loadSimilarImages: async (imageId, folderId = get().selectedFolderId, reset = true, sourceFolderId = folderId ?? null) => { - const requestToken = ++similarRequestToken; + const requestToken = ++galleryRequestToken; const offset = reset ? 0 : get().loadedCount; const similarScope = folderId === null ? "all_media" : "current_folder"; set((state) => ({ @@ -978,7 +980,7 @@ export const useGalleryStore = create((set, get) => ({ }, }); - if (requestToken !== similarRequestToken) return; + if (requestToken !== galleryRequestToken) return; set((state) => { const nextImages = reset ? result.images : [...state.images, ...result.images]; @@ -999,7 +1001,7 @@ export const useGalleryStore = create((set, get) => ({ }; }); } catch (error) { - if (requestToken !== similarRequestToken) return; + if (requestToken !== galleryRequestToken) return; console.error("Failed to load similar images:", error); set({ images: [], @@ -1019,7 +1021,7 @@ export const useGalleryStore = create((set, get) => ({ }, loadSimilarByRegion: async (imageId, crop, folderId = get().selectedFolderId, sourceFolderId = folderId ?? null) => { - const requestToken = ++similarRequestToken; + const requestToken = ++galleryRequestToken; const similarScope = folderId === null ? "all_media" : "current_folder"; set((state) => ({ images: [], @@ -1050,7 +1052,7 @@ export const useGalleryStore = create((set, get) => ({ }, }); - if (requestToken !== similarRequestToken) return; + if (requestToken !== galleryRequestToken) return; set({ images: result.images, @@ -1067,7 +1069,7 @@ export const useGalleryStore = create((set, get) => ({ similarScope, }); } catch (error) { - if (requestToken !== similarRequestToken) return; + if (requestToken !== galleryRequestToken) return; console.error("Failed to load region search results:", error); set({ images: [],