app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit d9a75abc8d1a4a853e7daf893e20f49966e1991a
parent 9d8a30d21bcb3f5a017582dc674cef3fbc4003a1
Author: triesap <tyson@radroots.org>
Date:   Sun, 24 May 2026 05:16:35 +0000

runtime: refresh buyer Browse selection

- make buyer Browse selection a fallible shared local-events refresh boundary
- reload selected-account context before presenting Browse projections
- report Browse selection refresh failures through the shell command error path
- validate focused Browse tests, cargo check, fmt, and diff checks

Diffstat:
Mcrates/launchers/desktop/src/runtime.rs | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mcrates/launchers/desktop/src/window.rs | 20++++++++++++++++----
2 files changed, 169 insertions(+), 16 deletions(-)

diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -273,7 +273,10 @@ impl DesktopAppRuntime { section_changed || editor_changed } - pub fn select_personal_section(&self, section: PersonalSection) -> bool { + pub fn select_personal_section( + &self, + section: PersonalSection, + ) -> Result<bool, AppSqliteError> { self.lock_state_mut().select_personal_section(section) } @@ -1170,7 +1173,15 @@ impl DesktopAppRuntimeState { } } - fn select_personal_section(&mut self, section: PersonalSection) -> bool { + fn select_personal_section( + &mut self, + section: PersonalSection, + ) -> Result<bool, AppSqliteError> { + let freshness_changed = if section == PersonalSection::Browse { + self.refresh_personal_browse_navigation()? + } else { + false + }; let section_changed = self .state_store .apply_in_memory(AppStateCommand::SelectSection(ShellSection::Personal( @@ -1178,7 +1189,15 @@ impl DesktopAppRuntimeState { ))); let editor_changed = self.close_product_editor(); - section_changed || editor_changed + Ok(freshness_changed || section_changed || editor_changed) + } + + fn refresh_personal_browse_navigation(&mut self) -> Result<bool, AppSqliteError> { + let report = self.import_shared_local_events()?; + let local_changed = report.imported_records > 0 || report.skipped_records > 0; + let context_changed = self.refresh_selected_account_context_after_local_events()?; + + Ok(local_changed || context_changed) } fn open_personal_product_detail( @@ -1193,8 +1212,12 @@ impl DesktopAppRuntimeState { return Ok(false); }; - let section_changed = matches!(section, PersonalSection::Browse | PersonalSection::Search) - && self.select_personal_section(section); + let section_changed = + if matches!(section, PersonalSection::Browse | PersonalSection::Search) { + self.select_personal_section(section)? + } else { + false + }; let detail_changed = self.set_personal_product_detail(section, Some(detail)); Ok(section_changed || detail_changed) @@ -1290,7 +1313,7 @@ impl DesktopAppRuntimeState { } changed }); - let section_changed = self.select_personal_section(PersonalSection::Cart); + let section_changed = self.select_personal_section(PersonalSection::Cart)?; Ok(cart_changed || section_changed) } @@ -1395,7 +1418,7 @@ impl DesktopAppRuntimeState { changed }); - let section_changed = self.select_personal_section(PersonalSection::Orders); + let section_changed = self.select_personal_section(PersonalSection::Orders)?; let pending_changed = if matches!(buyer_context, BuyerContext::Account(_)) { self.enqueue_selected_account_sync_operations(vec![pending_sync_upsert( SyncAggregateRef::Order(order_id), @@ -1424,7 +1447,7 @@ impl DesktopAppRuntimeState { }; let detail_changed = self.set_personal_order_detail(Some(order_detail)); - let section_changed = self.select_personal_section(PersonalSection::Orders); + let section_changed = self.select_personal_section(PersonalSection::Orders)?; Ok(detail_changed || section_changed) } @@ -1471,7 +1494,7 @@ impl DesktopAppRuntimeState { changed }); - let section_changed = self.select_personal_section(PersonalSection::Cart); + let section_changed = self.select_personal_section(PersonalSection::Cart)?; Ok(personal_changed || section_changed) } @@ -5286,7 +5309,9 @@ mod tests { use radroots_app_remote_signer::{ RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSessionRecord, }; - use radroots_app_sqlite::{AppSqliteStore, DatabaseTarget, latest_schema_version}; + use radroots_app_sqlite::{ + AppSqliteError, AppSqliteStore, DatabaseTarget, latest_schema_version, + }; use radroots_app_state::{ APP_STATE_FILE_NAME, AppStateCommand, AppStatePersistenceRepository, AppStateRepository, AppStateRepositoryError, AppStateStore, AppStateStoreError, FileBackedAppStateRepository, @@ -5948,6 +5973,114 @@ mod tests { } #[test] + fn runtime_buyer_browse_selection_refreshes_shared_local_events() { + let (runtime, paths) = bootstrapped_runtime("buyer_browse_selection_shared_events_refresh"); + assert!( + runtime + .generate_local_account(Some("Buyer".to_owned())) + .expect("account should generate") + ); + assert_eq!( + runtime + .summary() + .personal_projection + .browse + .listings + .rows + .len(), + 0 + ); + + append_cli_signed_buyer_listing_record_with( + &paths, + "browse-selection-first-listing", + "DDDDDDDDDDDDDDDDDDDDDD", + "Buyer Visible Eggs", + 1100, + ); + + assert!( + runtime + .select_personal_section(PersonalSection::Browse) + .expect("buyer Browse selection should refresh") + ); + let first_summary = runtime.summary(); + assert_eq!( + first_summary.personal_projection.browse.listings.rows.len(), + 1 + ); + + append_cli_signed_buyer_listing_record_with( + &paths, + "browse-selection-second-listing", + "EEEEEEEEEEEEEEEEEEEEEE", + "Buyer Visible Eggs Two", + 1200, + ); + + assert!( + runtime + .select_personal_section(PersonalSection::Browse) + .expect("same buyer Browse selection should refresh") + ); + let refreshed_summary = runtime.summary(); + let titles = refreshed_summary + .personal_projection + .browse + .listings + .rows + .iter() + .map(|row| row.title.as_str()) + .collect::<BTreeSet<_>>(); + assert_eq!( + titles, + BTreeSet::from(["Buyer Visible Eggs", "Buyer Visible Eggs Two"]) + ); + + assert!( + !runtime + .select_personal_section(PersonalSection::Browse) + .expect("idempotent buyer Browse selection should refresh") + ); + + cleanup_bootstrapped_runtime_paths(&paths); + } + + #[test] + fn runtime_buyer_browse_selection_surfaces_shared_local_events_import_errors() { + let (runtime, paths) = bootstrapped_runtime("buyer_browse_selection_import_error"); + assert!( + runtime + .generate_local_account(Some("Buyer".to_owned())) + .expect("account should generate") + ); + let database_path = + super::shared_local_events_database_path(&paths).expect("shared local events path"); + if let Some(parent) = database_path.parent() { + fs::create_dir_all(parent).expect("shared local events parent directory"); + } + if database_path.is_file() { + fs::remove_file(&database_path).expect("shared local events file should be removable"); + } else if database_path.is_dir() { + fs::remove_dir_all(&database_path) + .expect("shared local events directory should be removable"); + } + fs::create_dir(&database_path).expect("directory should block sqlite open"); + + let error = runtime + .select_personal_section(PersonalSection::Browse) + .expect_err("buyer Browse selection should surface import errors"); + match error { + AppSqliteError::LocalEventsSql { operation, .. } => { + assert_eq!(operation, "open shared local events database"); + } + unexpected => panic!("unexpected Browse selection error: {unexpected:?}"), + } + + cleanup_bootstrapped_runtime_paths(&paths); + } + + #[test] fn runtime_app_farm_and_listing_writes_append_shared_local_work_records() { let (runtime, paths) = bootstrapped_runtime("app_local_work_records"); assert!( @@ -7272,7 +7405,11 @@ mod tests { fn guest_marketplace_entry_selects_personal_browse_without_an_account() { let runtime = memory_runtime(); - assert!(runtime.select_personal_section(PersonalSection::Browse)); + assert!( + runtime + .select_personal_section(PersonalSection::Browse) + .expect("guest Browse selection should succeed") + ); let summary = runtime.summary(); assert_eq!(summary.startup_gate, AppStartupGate::SetupRequired); @@ -7926,7 +8063,11 @@ mod tests { .expect("buyer order should place") ); let order_id = runtime.summary().personal_projection.orders.list.rows[0].order_id; - assert!(runtime.select_personal_section(PersonalSection::Browse)); + assert!( + runtime + .select_personal_section(PersonalSection::Browse) + .expect("buyer Browse selection should succeed") + ); assert!(runtime.lock_state_mut().set_personal_order_detail(None)); assert!( diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -1129,10 +1129,22 @@ impl HomeView { } fn select_personal_section(&mut self, section: PersonalSection, cx: &mut Context<Self>) { - if self.runtime.select_personal_section(section) { - self.products_stock_editor = None; - self.product_editor_form = None; - cx.notify(); + match self.runtime.select_personal_section(section) { + Ok(true) => { + self.products_stock_editor = None; + self.product_editor_form = None; + cx.notify(); + } + Ok(false) => {} + Err(runtime_error) => { + error!( + target: "shell", + event = "buyer.section_select_failed", + section = ?section, + error = %runtime_error, + "failed to select buyer section" + ); + } } }