app

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

commit fa13454bf172f9e4038e1210183e4981f688c350
parent d46567d46be1b0d7cf6bb10cda8296309623a698
Author: triesap <triesap@radroots.dev>
Date:   Mon, 19 Jan 2026 21:20:05 +0000

app: add notifications health check

- add notifications health check and report field
- include notifications in aggregate health report
- render notifications status row in UI
- add unit tests for notifications health behavior

Diffstat:
Mapp/src/app.rs | 20+++++++++++++++++++-
Mapp/src/health.rs | 41+++++++++++++++++++++++++++++++++++++++--
Mapp/src/lib.rs | 1+
3 files changed, 59 insertions(+), 3 deletions(-)

diff --git a/app/src/app.rs b/app/src/app.rs @@ -22,6 +22,7 @@ use crate::{ AppHealthReport, AppInitError, AppInitStage, + AppNotifications, }; fn health_status_color(status: AppHealthCheckStatus) -> &'static str { @@ -65,7 +66,14 @@ fn spawn_health_checks( let keystore = radroots_app_core::keystore::RadrootsClientWebKeystoreNostr::new( Some(config.keystore.nostr_store), ); - let report = app_health_check_all(&datastore, &keystore, &config.datastore.key_maps).await; + let notifications = AppNotifications::new(None); + let report = app_health_check_all( + &datastore, + &keystore, + &notifications, + &config.datastore.key_maps, + ) + .await; let active_key_value = match app_datastore_read_app_data(&datastore, &config.datastore.key_maps).await { Ok(data) if data.active_key.is_empty() => None, Ok(data) => Some(data.active_key), @@ -278,6 +286,16 @@ pub fn App() -> impl IntoView { <span style=move || format!( "display:inline-block;width:10px;height:10px;border-radius:50%;background:{};", + health_status_color(health_report.get().notifications.status) + ) + ></span> + <span>"notifications"</span> + <span>{move || health_result_label(&health_report.get().notifications)}</span> + </div> + <div style="display: flex; align-items: center; gap: 8px;"> + <span + style=move || format!( + "display:inline-block;width:10px;height:10px;border-radius:50%;background:{};", health_status_color(health_report.get().datastore_roundtrip.status) ) ></span> diff --git a/app/src/health.rs b/app/src/health.rs @@ -52,6 +52,7 @@ pub struct AppHealthReport { pub bootstrap_config: AppHealthCheckResult, pub bootstrap_app_data: AppHealthCheckResult, pub app_data_active_key: AppHealthCheckResult, + pub notifications: AppHealthCheckResult, pub datastore_roundtrip: AppHealthCheckResult, pub keystore: AppHealthCheckResult, } @@ -63,6 +64,7 @@ impl Default for AppHealthReport { bootstrap_config: AppHealthCheckResult::skipped(), bootstrap_app_data: AppHealthCheckResult::skipped(), app_data_active_key: AppHealthCheckResult::skipped(), + notifications: AppHealthCheckResult::skipped(), datastore_roundtrip: AppHealthCheckResult::skipped(), keystore: AppHealthCheckResult::skipped(), } @@ -81,6 +83,7 @@ use crate::{ app_datastore_key_nostr_key, app_datastore_read_app_data, app_key_maps_validate, + AppNotifications, AppKeyMapConfig, }; use radroots_app_core::datastore::{RadrootsClientDatastore, RadrootsClientDatastoreError}; @@ -141,6 +144,21 @@ pub async fn app_health_check_app_data_active_key<T: RadrootsClientDatastore>( AppHealthCheckResult::ok() } +pub async fn app_health_check_notifications( + notifications: &AppNotifications, +) -> AppHealthCheckResult { + match notifications.permission().await { + Ok(permission) => { + if permission == radroots_app_core::notifications::RadrootsClientNotificationsPermission::Granted { + AppHealthCheckResult::ok() + } else { + AppHealthCheckResult::error(permission.as_str()) + } + } + Err(err) => AppHealthCheckResult::error(err.to_string()), + } +} + const APP_HEALTH_TEMP_KEY: &str = "radroots.health.temp"; pub async fn app_health_check_datastore_roundtrip<T: RadrootsClientDatastore>( @@ -190,6 +208,7 @@ pub async fn app_health_check_keystore_access<T: RadrootsClientDatastore, K: Rad pub async fn app_health_check_all<T: RadrootsClientDatastore, K: RadrootsClientKeystoreNostr>( datastore: &T, keystore: &K, + notifications: &AppNotifications, key_maps: &AppKeyMapConfig, ) -> AppHealthReport { AppHealthReport { @@ -197,6 +216,7 @@ pub async fn app_health_check_all<T: RadrootsClientDatastore, K: RadrootsClientK bootstrap_config: app_health_check_bootstrap_config(datastore, key_maps).await, bootstrap_app_data: app_health_check_bootstrap_app_data(datastore, key_maps).await, app_data_active_key: app_health_check_app_data_active_key(datastore, key_maps).await, + notifications: app_health_check_notifications(notifications).await, datastore_roundtrip: app_health_check_datastore_roundtrip(datastore).await, keystore: app_health_check_keystore_access(datastore, keystore, key_maps).await, } @@ -212,6 +232,7 @@ mod tests { app_health_check_bootstrap_config, app_health_check_datastore_roundtrip, app_health_check_keystore_access, + app_health_check_notifications, AppHealthCheckResult, AppHealthCheckStatus, AppHealthReport, @@ -260,6 +281,7 @@ mod tests { assert_eq!(report.bootstrap_config.status, AppHealthCheckStatus::Skipped); assert_eq!(report.bootstrap_app_data.status, AppHealthCheckStatus::Skipped); assert_eq!(report.app_data_active_key.status, AppHealthCheckStatus::Skipped); + assert_eq!(report.notifications.status, AppHealthCheckStatus::Skipped); assert_eq!(report.datastore_roundtrip.status, AppHealthCheckStatus::Skipped); assert_eq!(report.keystore.status, AppHealthCheckStatus::Skipped); } @@ -564,14 +586,29 @@ mod tests { fn health_check_all_reports_idb_errors() { let datastore = RadrootsClientWebDatastore::new(None); let keystore = RadrootsClientWebKeystoreNostr::new(None); + let notifications = crate::AppNotifications::new(None); let key_maps = crate::app_key_maps_default(); - let report = - futures::executor::block_on(app_health_check_all(&datastore, &keystore, &key_maps)); + let report = futures::executor::block_on(app_health_check_all( + &datastore, + &keystore, + &notifications, + &key_maps, + )); assert_eq!(report.key_maps.status, AppHealthCheckStatus::Ok); assert_eq!(report.bootstrap_config.status, AppHealthCheckStatus::Error); assert_eq!(report.bootstrap_app_data.status, AppHealthCheckStatus::Error); assert_eq!(report.app_data_active_key.status, AppHealthCheckStatus::Error); + assert_eq!(report.notifications.status, AppHealthCheckStatus::Error); assert_eq!(report.datastore_roundtrip.status, AppHealthCheckStatus::Error); assert_eq!(report.keystore.status, AppHealthCheckStatus::Error); } + + #[test] + fn health_check_notifications_reports_unavailable() { + let notifications = crate::AppNotifications::new(None); + let result = + futures::executor::block_on(app_health_check_notifications(&notifications)); + assert_eq!(result.status, AppHealthCheckStatus::Error); + assert_eq!(result.message.as_deref(), Some("unavailable")); + } } diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -29,6 +29,7 @@ pub use health::{ app_health_check_bootstrap_config, app_health_check_datastore_roundtrip, app_health_check_keystore_access, + app_health_check_notifications, app_health_check_key_maps, AppHealthCheckResult, AppHealthCheckStatus,