From 0ab156d2d917bcc57377d61d79107cb08d03059b Mon Sep 17 00:00:00 2001 From: LyAhn Date: Mon, 8 Jun 2026 22:09:50 +0100 Subject: [PATCH] fix: startup crash, watcher count, timeline date fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit db.rs: - Move idx_images_taken_at creation to after ensure_column so it never runs against a schema where taken_at doesn't exist yet; this was causing a startup panic on any DB that predates Phase 1 - COALESCE(taken_at, created_at) → COALESCE(taken_at, modified_at) since created_at is never populated (modified_at is always set) - Remove dead is_tagging_job_cancelled function (superseded by is_tagging_job_processing) and unused thumbnail_path from ImagePathRecord indexer.rs: - Watcher create path now calls update_folder_count after commit_batch and emits folder-counts-changed so the sidebar count stays in sync; the notification is only emitted when the DB write actually succeeds store.ts / Timeline.tsx: - compareImages taken_asc/taken_desc: fall back to modified_at not created_at (created_at is always null) - Timeline groupImages: same fallback fix so images group by month correctly for libraries not yet re-indexed for EXIF - subscribeToProgress: listen for folder-counts-changed and call loadFolders() to keep the sidebar image count live --- src-tauri/src/db.rs | 27 +++++++++------------------ src-tauri/src/indexer.rs | 10 ++++++++++ src/components/Timeline.tsx | 2 +- src/store.ts | 9 +++++++-- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index e5454ea..b2c02ba 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -273,7 +273,6 @@ pub fn migrate(conn: &Connection) -> Result<()> { CREATE INDEX IF NOT EXISTS idx_images_folder_id ON images(folder_id); CREATE INDEX IF NOT EXISTS idx_images_modified_at ON images(modified_at); - CREATE INDEX IF NOT EXISTS idx_images_taken_at ON images(taken_at); CREATE INDEX IF NOT EXISTS idx_embedding_jobs_status ON embedding_jobs(status); CREATE INDEX IF NOT EXISTS idx_thumbnail_jobs_status ON thumbnail_jobs(status); CREATE INDEX IF NOT EXISTS idx_metadata_jobs_status ON metadata_jobs(status); @@ -316,6 +315,12 @@ pub fn migrate(conn: &Connection) -> Result<()> { ensure_column(conn, "images", "ai_tagged_at", "TEXT")?; ensure_column(conn, "images", "ai_tagger_error", "TEXT")?; ensure_column(conn, "images", "taken_at", "TEXT")?; + // Index must be created after ensure_column adds the column; it cannot live + // in the execute_batch above because that batch runs before the column exists + // on databases that predate Phase 1. + conn.execute_batch( + "CREATE INDEX IF NOT EXISTS idx_images_taken_at ON images(taken_at);", + )?; ensure_column(conn, "folders", "scan_error", "TEXT")?; vector::migrate(conn)?; @@ -1500,8 +1505,8 @@ pub fn get_images( "rating_desc" => "rating DESC, modified_at DESC NULLS LAST", "duration_asc" => "duration_ms ASC NULLS LAST", "duration_desc" => "duration_ms DESC NULLS LAST", - "taken_asc" => "COALESCE(taken_at, created_at) ASC NULLS LAST", - "taken_desc" => "COALESCE(taken_at, created_at) DESC NULLS LAST", + "taken_asc" => "COALESCE(taken_at, modified_at) ASC NULLS LAST", + "taken_desc" => "COALESCE(taken_at, modified_at) DESC NULLS LAST", _ => "modified_at DESC NULLS LAST", }; @@ -1671,7 +1676,6 @@ pub fn search_tags_autocomplete( pub struct ImagePathRecord { pub id: i64, pub path: String, - pub thumbnail_path: Option, } pub fn get_all_image_paths( @@ -1679,14 +1683,13 @@ pub fn get_all_image_paths( folder_id: Option, ) -> Result> { let mut stmt = conn.prepare( - "SELECT id, path, thumbnail_path FROM images WHERE (?1 IS NULL OR folder_id = ?1) ORDER BY id", + "SELECT id, path FROM images WHERE (?1 IS NULL OR folder_id = ?1) ORDER BY id", )?; let rows = stmt .query_map(params![folder_id], |row| { Ok(ImagePathRecord { id: row.get(0)?, path: row.get(1)?, - thumbnail_path: row.get(2)?, }) })? .collect::>>()?; @@ -2057,18 +2060,6 @@ pub fn requeue_processing_tagging_jobs_for_folder(conn: &Connection, folder_id: Ok(()) } -/// Returns `true` when the job row for `image_id` currently has status = 'cancelled'. -/// Used by the worker to discard inference results for jobs that were cancelled -/// while inference was running. -pub fn is_tagging_job_cancelled(conn: &Connection, image_id: i64) -> Result { - let count: i64 = conn.query_row( - "SELECT COUNT(*) FROM tagging_jobs WHERE image_id = ?1 AND status = 'cancelled'", - [image_id], - |row| row.get(0), - )?; - Ok(count > 0) -} - /// Returns `true` when the job row for `image_id` is still `processing`. /// A `false` result means the row was reset to `pending` (pause) or `cancelled` /// while inference was running — either way the result must be discarded. diff --git a/src-tauri/src/indexer.rs b/src-tauri/src/indexer.rs index 2ac7ed4..704c466 100644 --- a/src-tauri/src/indexer.rs +++ b/src-tauri/src/indexer.rs @@ -1480,8 +1480,17 @@ fn process_watcher_path( match commit_batch(pool, &[record]) { Ok(committed) if !committed.is_empty() => { + // Always emit the images — they are committed to the DB. emit_images(app, &IndexedImagesBatch { folder_id, images: committed }); emit_folder_job_progress(app, pool, &[folder_id], false); + // Update the sidebar count only if we successfully write the new + // count; skip the frontend notification on pool/DB failure to + // avoid showing a stale number. + if let Ok(count_conn) = pool.get() { + if db::update_folder_count(&count_conn, folder_id).is_ok() { + let _ = app.emit("folder-counts-changed", ()); + } + } } Ok(_) => {} Err(e) => eprintln!("Watcher: commit error for {:?}: {}", path, e), @@ -1494,6 +1503,7 @@ fn process_watcher_path( if db::delete_images_by_ids(&conn, &[image_id]).is_ok() { db::update_folder_count(&conn, folder_id).ok(); let _ = app.emit("watcher-deleted", vec![image_id]); + let _ = app.emit("folder-counts-changed", ()); } } Ok(None) => {} // never indexed or already removed diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index e9e1c83..1855eb0 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -22,7 +22,7 @@ function buildLabel(key: string): string { function groupImages(images: ImageRecord[]): TimelineGroup[] { const map = new Map(); for (const img of images) { - const ds = img.taken_at ?? img.created_at; + const ds = img.taken_at ?? img.modified_at; const key = ds ? ds.substring(0, 7) : "unknown"; let bucket = map.get(key); if (bucket === undefined) { diff --git a/src/store.ts b/src/store.ts index 129ac4f..a3f6f43 100644 --- a/src/store.ts +++ b/src/store.ts @@ -498,9 +498,9 @@ function compareImages(a: ImageRecord, b: ImageRecord, sort: SortOrder): number case "duration_desc": return compareNullableNumber(b.duration_ms, a.duration_ms); case "taken_asc": - return compareNullableDate(a.taken_at ?? a.created_at, b.taken_at ?? b.created_at); + return compareNullableDate(a.taken_at ?? a.modified_at, b.taken_at ?? b.modified_at); case "taken_desc": - return compareNullableDate(b.taken_at ?? b.created_at, a.taken_at ?? a.created_at); + return compareNullableDate(b.taken_at ?? b.modified_at, a.taken_at ?? a.modified_at); default: return compareNullableDate(b.modified_at, a.modified_at); } @@ -1770,6 +1770,10 @@ export const useGalleryStore = create((set, get) => ({ }); }); + const unlistenFolderCounts = await listen("folder-counts-changed", () => { + void get().loadFolders(); + }); + return () => { unlistenProgress(); unlistenMediaJobs(); @@ -1778,6 +1782,7 @@ export const useGalleryStore = create((set, get) => ({ unlistenImages(); unlistenThumbnails(); unlistenWatcherDeleted(); + unlistenFolderCounts(); }; }, }));