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:
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"));
+ }
+}