commit 7b9ff92b83f9b26a71356953249c3cd060752232
parent d47649d6ea91cf0643618d7660d4002baded551d
Author: triesap <triesap@radroots.dev>
Date: Mon, 19 Jan 2026 18:50:43 +0000
app: add health check panel
- add health report state and run controls
- run aggregate health checks with datastore and keystore
- render per-check status indicators and labels
- disable health run while busy or uninitialized
Diffstat:
| M | app/src/app.rs | | | 105 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 105 insertions(+), 0 deletions(-)
diff --git a/app/src/app.rs b/app/src/app.rs
@@ -7,17 +7,38 @@ use crate::{
app_init_mark_completed,
app_init_reset,
app_config_default,
+ app_health_check_all,
AppBackends,
+ AppHealthCheckResult,
+ AppHealthCheckStatus,
+ AppHealthReport,
AppInitError,
AppInitStage,
};
+fn health_status_color(status: AppHealthCheckStatus) -> &'static str {
+ match status {
+ AppHealthCheckStatus::Ok => "green",
+ AppHealthCheckStatus::Error => "red",
+ AppHealthCheckStatus::Skipped => "gray",
+ }
+}
+
+fn health_result_label(result: &AppHealthCheckResult) -> String {
+ match result.message.as_deref() {
+ Some(message) => format!("{}: {}", result.status.as_str(), message),
+ None => result.status.as_str().to_string(),
+ }
+}
+
#[component]
pub fn App() -> impl IntoView {
let backends = RwSignal::new_local(None::<AppBackends>);
let init_error = RwSignal::new_local(None::<AppInitError>);
let init_state = RwSignal::new_local(app_init_state_default());
let reset_status = RwSignal::new_local(None::<String>);
+ let health_report = RwSignal::new_local(AppHealthReport::empty());
+ let health_running = RwSignal::new_local(false);
provide_context(backends);
provide_context(init_error);
provide_context(init_state);
@@ -53,6 +74,9 @@ pub fn App() -> impl IntoView {
.get()
.unwrap_or_else(|| "reset_idle".to_string())
};
+ let health_disabled = move || {
+ backends.with(|value| value.is_none()) || health_running.get()
+ };
view! {
<main>
<div>"app"</div>
@@ -90,6 +114,87 @@ pub fn App() -> impl IntoView {
</button>
<span>{reset_label}</span>
</div>
+ <div style="margin-top: 16px;">
+ <div style="font-weight: 600;">"health checks"</div>
+ <div style="margin-top: 8px; display: flex; align-items: center; gap: 8px;">
+ <button
+ on:click=move |_| {
+ let config = backends.with_untracked(|value| value.as_ref().map(|backends| backends.config.clone()));
+ health_running.set(true);
+ spawn_local(async move {
+ let Some(config) = config else {
+ health_running.set(false);
+ return;
+ };
+ let datastore = radroots_app_core::datastore::RadrootsClientWebDatastore::new(
+ Some(config.datastore.idb_config),
+ );
+ 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;
+ health_report.set(report);
+ health_running.set(false);
+ });
+ }
+ disabled=health_disabled
+ >
+ {move || if health_running.get() { "checking" } else { "run checks" }}
+ </button>
+ </div>
+ <div style="margin-top: 8px; display: grid; gap: 6px;">
+ <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().key_maps.status)
+ )
+ ></span>
+ <span>"key_maps"</span>
+ <span>{move || health_result_label(&health_report.get().key_maps)}</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().bootstrap_config.status)
+ )
+ ></span>
+ <span>"bootstrap_config"</span>
+ <span>{move || health_result_label(&health_report.get().bootstrap_config)}</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().bootstrap_app_data.status)
+ )
+ ></span>
+ <span>"bootstrap_app_data"</span>
+ <span>{move || health_result_label(&health_report.get().bootstrap_app_data)}</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>
+ <span>"datastore_roundtrip"</span>
+ <span>{move || health_result_label(&health_report.get().datastore_roundtrip)}</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().keystore.status)
+ )
+ ></span>
+ <span>"keystore"</span>
+ <span>{move || health_result_label(&health_report.get().keystore)}</span>
+ </div>
+ </div>
+ </div>
</main>
}
}