From 052d95cd661a656529fe2389b9c7a24bca9df026 Mon Sep 17 00:00:00 2001 From: Antonio Sokic <5701985+finaler1x@users.noreply.github.com> Date: Fri, 30 Oct 2026 19:58:09 +0100 Subject: [PATCH 1/2] Issue #3406: Auto Import says when it can't read the watched folder A watched folder that was removed, unmounted or not allowed looked like one with nothing new: the listing error was logged at debug only. It is now logged as a warning once when it starts (and once when the folder can be read again), and Settings > Import shows the reason under the watched folder. --- crates/ui-egui/src/app/frame.rs | 24 +++++++++++++++---- crates/ui-egui/src/dialogs/settings/import.rs | 9 +++++++ crates/ui-egui/src/state.rs | 5 ++++ crates/ui-egui/src/tests/drop_import.rs | 18 ++++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/app/frame.rs b/crates/ui-egui/src/app/frame.rs index 6e92091c7..995796bd4 100644 --- a/crates/ui-egui/src/app/frame.rs +++ b/crates/ui-egui/src/app/frame.rs @@ -91,11 +91,27 @@ impl LightcraftApp { && !self.tasks.is_running(LABEL) { self.ui.auto_import_at = now; - let work = move || lightcraft_engine::cmd::library::list_auto_import_folder(&folder); - let done = |app: &mut LightcraftApp, _ctx: &egui::Context, listing: Result, String>| { + let listed = folder.clone(); + let work = move || lightcraft_engine::cmd::library::list_auto_import_folder(&listed); + let done = move |app: &mut LightcraftApp, _ctx: &egui::Context, listing: Result, String>| { let listing = match listing { - Ok(l) => l, - Err(e) => return log::debug!("auto import: {e}"), + Ok(l) => { + if app.ui.auto_import_error.take().is_some() { + log::info!("auto import: the watched folder can be read again: {folder}"); + } + l + } + // removed, renamed, unmounted or not allowed: said once, not every few seconds + Err(e) => { + let error = Some((folder, e)); + if app.ui.auto_import_error != error { + if let Some((_, e)) = &error { + log::warn!("auto import: can't read the watched folder {e}"); + } + app.ui.auto_import_error = error; + } + return; + } }; let p = serde_json::json!({"listing": listing, "start": false}); let Ok(r) = app.session.execute("library.autoImportScan", &p) else { return }; diff --git a/crates/ui-egui/src/dialogs/settings/import.rs b/crates/ui-egui/src/dialogs/settings/import.rs index 97f34d6ab..18b221e7e 100644 --- a/crates/ui-egui/src/dialogs/settings/import.rs +++ b/crates/ui-egui/src/dialogs/settings/import.rs @@ -163,6 +163,15 @@ pub(super) fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) let _ = app.run("library.autoImport", json!({"folder": null})); } }); + // the last listing of the watched folder failed: say why (it reads "OS error: reason") + if let (Some(folder), Some((failed, e))) = (&d.auto_folder, &app.ui.auto_import_error) + && folder == failed + { + let reason = e.strip_prefix(folder.as_str()).and_then(|r| r.strip_prefix(": ")).unwrap_or(e); + row(ui, t, "", |ui| { + ui.label(RichText::new(crate::i18n::tr_format!("Can't read this folder: {}", reason)).size(11.0).color(t.caution)); + }); + } if d.auto_folder.is_some() { row(ui, t, "", |ui| { let mut copy = d.auto_copy; diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 38b50902c..60c87a527 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -166,6 +166,10 @@ pub struct UiState { /// scanning). #[serde(skip)] pub auto_import_at: f64, + /// The watched folder that couldn't be listed and why, as the last listing found it: logged once when it + /// starts and once when the folder can be read again, and shown in Settings ▸ Import. + #[serde(skip)] + pub auto_import_error: Option<(String, String)>, /// When the automatic XMP writes that failed were last tried again (monotonic app time), and how /// many were waiting then. #[serde(skip)] @@ -421,6 +425,7 @@ impl Default for UiState { external_edits: Vec::new(), was_focused: true, auto_import_at: 0.0, + auto_import_error: None, sidecar_retry_at: 0.0, sidecars_waiting: 0, known_cards: None, diff --git a/crates/ui-egui/src/tests/drop_import.rs b/crates/ui-egui/src/tests/drop_import.rs index 3d8031a69..ced30ddc1 100644 --- a/crates/ui-egui/src/tests/drop_import.rs +++ b/crates/ui-egui/src/tests/drop_import.rs @@ -1107,3 +1107,21 @@ fn public_drop_take_preserves_reentered_barrier_order_and_clears_protection() { assert_eq!(photos(&h), 0, "neither taken handles nor refused arrivals replay"); } } + +/// A watched folder that can't be listed (removed, unmounted, not allowed) is noted with the reason, for +/// Settings ▸ Import to show, and the note goes once the folder can be read again. +#[test] +fn auto_import_notes_a_watched_folder_it_cannot_read() { + let dir = Scratch::new("auto-unreadable"); + let mut h = app(&dir); + let watch = dir.path("watch"); + h.app.session.import_defaults.auto_folder = Some(watch.clone()); + assert!(h.step_until(T, |h| h.app.ui.auto_import_error.is_some()), "the missing folder is noted"); + let (folder, e) = h.app.ui.auto_import_error.clone().unwrap(); + assert_eq!(folder, watch); + assert!(e.starts_with(&watch) && e.len() > watch.len() + 2, "the error says why: {e}"); + write_png(&dir.path("watch/w.png"), 3); + assert!(h.step_until(T, |h| h.app.ui.auto_import_error.is_none()), "readable again: the note goes"); + assert!(h.step_until(T, |h| photos(h) == 1), "and the photo arrives"); + h.app.session.import_defaults.auto_folder = None; +} From b9ba9d4640a15dde83eb18633b9e90ba53f44bfe Mon Sep 17 00:00:00 2001 From: Antonio Sokic <5701985+finaler1x@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:26:34 +0100 Subject: [PATCH 2/2] Issue #3406: tie the unreadable-folder note to the folder that failed The note now goes when another folder is watched, a listing of a folder no longer watched is ignored, and "can be read again" is logged only for the folder that failed. --- crates/ui-egui/src/app/frame.rs | 11 ++++++++++- crates/ui-egui/src/tests/drop_import.rs | 7 +++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/app/frame.rs b/crates/ui-egui/src/app/frame.rs index 995796bd4..08c5bfee9 100644 --- a/crates/ui-egui/src/app/frame.rs +++ b/crates/ui-egui/src/app/frame.rs @@ -81,6 +81,10 @@ impl LightcraftApp { focus_scan::pump(self, ctx, now); // auto import: list the watched folder every few seconds (on a worker thread: it may be on // a network share), then import what's new like any import (also on a worker thread) + // a folder that couldn't be read stops being one once another is watched (or none) + if self.ui.auto_import_error.as_ref().is_some_and(|(f, _)| self.session.import_defaults.auto_folder.as_ref() != Some(f)) { + self.ui.auto_import_error = None; + } #[cfg(not(target_arch = "wasm32"))] if let Some(folder) = self.session.import_defaults.auto_folder.clone() { const LABEL: &str = "Auto Import"; @@ -94,9 +98,14 @@ impl LightcraftApp { let listed = folder.clone(); let work = move || lightcraft_engine::cmd::library::list_auto_import_folder(&listed); let done = move |app: &mut LightcraftApp, _ctx: &egui::Context, listing: Result, String>| { + // the watched folder changed while this one was listed: its result is for nothing on screen + if app.session.import_defaults.auto_folder.as_ref() != Some(&folder) { + return; + } let listing = match listing { Ok(l) => { - if app.ui.auto_import_error.take().is_some() { + if app.ui.auto_import_error.as_ref().is_some_and(|(f, _)| *f == folder) { + app.ui.auto_import_error = None; log::info!("auto import: the watched folder can be read again: {folder}"); } l diff --git a/crates/ui-egui/src/tests/drop_import.rs b/crates/ui-egui/src/tests/drop_import.rs index ced30ddc1..c380b7774 100644 --- a/crates/ui-egui/src/tests/drop_import.rs +++ b/crates/ui-egui/src/tests/drop_import.rs @@ -1120,6 +1120,13 @@ fn auto_import_notes_a_watched_folder_it_cannot_read() { let (folder, e) = h.app.ui.auto_import_error.clone().unwrap(); assert_eq!(folder, watch); assert!(e.starts_with(&watch) && e.len() > watch.len() + 2, "the error says why: {e}"); + // another folder watched: the note was about the old one, so it goes, and a listing of the old one + // still under way doesn't bring it back + h.app.session.import_defaults.auto_folder = Some(dir.path("trip")); + h.step(); + assert!(h.app.ui.auto_import_error.is_none(), "{:?}", h.app.ui.auto_import_error); + h.app.session.import_defaults.auto_folder = Some(watch.clone()); + assert!(h.step_until(T, |h| h.app.ui.auto_import_error.is_some()), "back to the missing folder: noted again"); write_png(&dir.path("watch/w.png"), 3); assert!(h.step_until(T, |h| h.app.ui.auto_import_error.is_none()), "readable again: the note goes"); assert!(h.step_until(T, |h| photos(h) == 1), "and the photo arrives");