fix: five PR review bugs and folder-recovery UX for missing/renamed folders

BackgroundTasks: dismiss no longer calls clearTaggingJobs; dismissal only
updates local dismissed state

DuplicateFinder: entering the view clears stale groups when the stored scan
scope differs from selectedFolderId; duplicateScanFolderId is recorded on
each scan and cache load so scope is always tracked

Tagger threshold: set_tagger_threshold now sets TAGGER_SESSION_DIRTY so the
worker rebuilds its WdTagger instance and applies the new threshold on the
next batch

Region search global scope: fetch offset+limit+2 candidates before removing
the source image so has_more is accurate across pages

Region search pagination: loadMoreImages now sets loadingImages and checks
galleryRequestToken before appending results, preventing duplicate pages and
stale responses corrupting a new collection

Folder recovery: when index_folder detects a missing path it records the
error in a new folders.scan_error column (ensure_column migration) and emits
done without deleting images, preserving them for recovery. A new
update_folder_path command rewrites image paths in the DB before reindexing
so thumbnails and embeddings are not needlessly regenerated for unchanged
files. The sidebar shows an amber warning icon on affected folders and a
recovery banner with Locate Folder and Remove actions
This commit is contained in:
2026-06-07 09:00:32 +01:00
parent 9e08d9cce3
commit 7871d52d39
8 changed files with 203 additions and 35 deletions
+37 -2
View File
@@ -30,6 +30,7 @@ pub struct Folder {
pub name: String,
pub image_count: i64,
pub indexed_at: Option<String>,
pub scan_error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -172,7 +173,8 @@ pub fn migrate(conn: &Connection) -> Result<()> {
path TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
image_count INTEGER NOT NULL DEFAULT 0,
indexed_at TEXT
indexed_at TEXT,
scan_error TEXT
);
CREATE TABLE IF NOT EXISTS images (
@@ -304,6 +306,7 @@ pub fn migrate(conn: &Connection) -> Result<()> {
ensure_column(conn, "images", "ai_tagger_model", "TEXT")?;
ensure_column(conn, "images", "ai_tagged_at", "TEXT")?;
ensure_column(conn, "images", "ai_tagger_error", "TEXT")?;
ensure_column(conn, "folders", "scan_error", "TEXT")?;
vector::migrate(conn)?;
Ok(())
@@ -1339,7 +1342,7 @@ pub fn get_images_by_ids(conn: &Connection, image_ids: &[i64]) -> Result<Vec<Ima
pub fn get_folders(conn: &Connection) -> Result<Vec<Folder>> {
let mut stmt =
conn.prepare("SELECT id, path, name, image_count, indexed_at FROM folders ORDER BY name")?;
conn.prepare("SELECT id, path, name, image_count, indexed_at, scan_error FROM folders ORDER BY name")?;
let rows = stmt.query_map([], |row| {
Ok(Folder {
id: row.get(0)?,
@@ -1347,11 +1350,43 @@ pub fn get_folders(conn: &Connection) -> Result<Vec<Folder>> {
name: row.get(2)?,
image_count: row.get(3)?,
indexed_at: row.get(4)?,
scan_error: row.get(5)?,
})
})?;
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
}
pub fn update_folder_path(conn: &Connection, folder_id: i64, old_path: &str, new_path: &str, new_name: &str) -> Result<()> {
conn.execute(
"UPDATE folders SET path = ?2, name = ?3, scan_error = NULL WHERE id = ?1",
params![folder_id, new_path, new_name],
)?;
// Rewrite image paths so the indexer can match them by path and skip
// re-generating thumbnails and embeddings for unchanged files.
// SQLite's replace() does a literal prefix substitution on each path.
conn.execute(
"UPDATE images SET path = replace(path, ?1, ?2) WHERE folder_id = ?3",
params![old_path, new_path, folder_id],
)?;
Ok(())
}
pub fn set_folder_scan_error(conn: &Connection, folder_id: i64, error: &str) -> Result<()> {
conn.execute(
"UPDATE folders SET scan_error = ?2 WHERE id = ?1",
params![folder_id, error],
)?;
Ok(())
}
pub fn clear_folder_scan_error(conn: &Connection, folder_id: i64) -> Result<()> {
conn.execute(
"UPDATE folders SET scan_error = NULL WHERE id = ?1",
[folder_id],
)?;
Ok(())
}
pub fn get_images(
conn: &Connection,
folder_id: Option<i64>,