app

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

commit dea764e8b7a14d13cfe251cee14f125faa64ccf5
parent 7ef625465d7206d31d0ac0439f03200285016c25
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 00:35:50 +0000

core: add release-safe offline geocoder diagnostics

- add a typed offline geocoder diagnostic snapshot with stable unavailable codes and export text
- expose the release-safe diagnostic in the shared offline geocoder details ui with a copy action
- keep raw debug messages out of exported diagnostics while preserving them for debug builds
- extend core tests to lock the new diagnostic contract and export behavior

Diffstat:
Mcrates/core/src/lib.rs | 23++++++++++++++++++++---
Mcrates/core/src/offline_geocoder.rs | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 83 insertions(+), 3 deletions(-)

diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs @@ -9,7 +9,8 @@ mod offline_geocoder; pub const APP_NAME: &str = "Rad Roots"; pub use offline_geocoder::{ - RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, + RadrootsOfflineGeocoderDiagnostic, RadrootsOfflineGeocoderState, + RadrootsOfflineGeocoderUnavailableKind, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -294,8 +295,13 @@ impl RadrootsApp { ui.label(user_message); ui.add_space(6.0); ui.collapsing("Offline geocoder details", |ui| { - if let Some(technical_message) = state.technical_message() { - ui.label(technical_message); + if let Some(diagnostic) = state.diagnostic() { + ui.label(diagnostic.technical_message); + ui.add_space(6.0); + ui.monospace(format!("diagnostic code: {}", diagnostic.code)); + if ui.button("Copy Offline Geocoder Diagnostic").clicked() { + ui.ctx().copy_text(diagnostic.export_text()); + } } if cfg!(debug_assertions) { if let Some(debug_message) = state.debug_message() { @@ -1189,5 +1195,16 @@ mod tests { .and_then(RadrootsOfflineGeocoderState::debug_message), Some("failed to open staged geocoder db") ); + let diagnostic = app + .offline_geocoder_state + .as_ref() + .and_then(RadrootsOfflineGeocoderState::diagnostic) + .unwrap(); + assert_eq!(diagnostic.code, "initialization_failed"); + assert!( + !diagnostic + .export_text() + .contains("failed to open staged geocoder db") + ); } } diff --git a/crates/core/src/offline_geocoder.rs b/crates/core/src/offline_geocoder.rs @@ -6,6 +6,14 @@ pub enum RadrootsOfflineGeocoderUnavailableKind { } impl RadrootsOfflineGeocoderUnavailableKind { + pub fn code(self) -> &'static str { + match self { + Self::MissingBuildAsset => "missing_build_asset", + Self::InitializationFailed => "initialization_failed", + Self::InternalError => "internal_error", + } + } + pub fn technical_message(self) -> &'static str { match self { Self::MissingBuildAsset => { @@ -31,6 +39,23 @@ impl RadrootsOfflineGeocoderUnavailableKind { } #[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsOfflineGeocoderDiagnostic { + pub code: &'static str, + pub summary_label: &'static str, + pub user_message: &'static str, + pub technical_message: &'static str, +} + +impl RadrootsOfflineGeocoderDiagnostic { + pub fn export_text(&self) -> String { + format!( + "offline geocoder diagnostic\ncode: {}\nstatus: {}\nuser: {}\ntechnical: {}", + self.code, self.summary_label, self.user_message, self.technical_message + ) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub enum RadrootsOfflineGeocoderState { Initializing, Ready, @@ -58,6 +83,18 @@ impl RadrootsOfflineGeocoderState { } } + pub fn diagnostic(&self) -> Option<RadrootsOfflineGeocoderDiagnostic> { + match self { + Self::Unavailable { kind, .. } => Some(RadrootsOfflineGeocoderDiagnostic { + code: kind.code(), + summary_label: self.summary_label(), + user_message: kind.user_message(), + technical_message: kind.technical_message(), + }), + Self::Initializing | Self::Ready => None, + } + } + pub fn summary_label(&self) -> &'static str { match self { Self::Initializing => "Offline geocoder: initializing", @@ -80,3 +117,29 @@ impl RadrootsOfflineGeocoderState { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unavailable_state_exposes_release_safe_diagnostic() { + let state = RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + "failed to open staged geocoder db: /tmp/geonames.db", + ); + let diagnostic = state.diagnostic().unwrap(); + + assert_eq!(diagnostic.code, "initialization_failed"); + assert_eq!(diagnostic.summary_label, "Offline geocoder unavailable"); + assert_eq!( + diagnostic.user_message, + "Offline geocoder could not be initialized on this device." + ); + assert_eq!( + diagnostic.technical_message, + "The offline geocoder data file could not be prepared on this device." + ); + assert!(!diagnostic.export_text().contains("/tmp/geonames.db")); + } +}