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:
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,
+ ¬ifications,
+ &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,
+ ¬ifications,
+ &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(¬ifications));
+ 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,