app

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

commit 379a18851617478c1db2bedb5829c2749908624f
parent e9efb1e8a8eb3038c83cfa4c1810229dcdf40d7d
Author: triesap <triesap@radroots.dev>
Date:   Tue, 20 Jan 2026 15:45:15 +0000

app: flush logs after health checks

- add logged health check wrapper with buffer flush

- persist buffered entries into idb after runs

- switch app health runner to use the wrapper

- add unit tests for health log flush behavior

Diffstat:
Mapp/src/app.rs | 4++--
Mapp/src/health.rs | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/src/lib.rs | 1+
3 files changed, 165 insertions(+), 2 deletions(-)

diff --git a/app/src/app.rs b/app/src/app.rs @@ -20,7 +20,7 @@ use crate::{ app_log_error_store, app_config_default, app_datastore_read_app_data, - app_health_check_all, + app_health_check_all_logged, AppBackends, AppConfig, AppHealthCheckResult, @@ -80,7 +80,7 @@ fn spawn_health_checks( ); let notifications = AppNotifications::new(None); let tangle = AppTangleClientStub::new(); - let report = app_health_check_all( + let report = app_health_check_all_logged( &datastore, &keystore, &notifications, diff --git a/app/src/health.rs b/app/src/health.rs @@ -84,6 +84,7 @@ use crate::{ app_datastore_has_config, app_datastore_key_nostr_key, app_datastore_read_app_data, + app_log_buffer_flush, app_log_debug_emit, app_log_entry_new, app_log_entry_record, @@ -283,11 +284,24 @@ pub async fn app_health_check_all<T: RadrootsClientDatastore, K: RadrootsClientK } } +pub async fn app_health_check_all_logged<T: RadrootsClientDatastore, K: RadrootsClientKeystoreNostr, G: AppTangleClient>( + datastore: &T, + keystore: &K, + notifications: &AppNotifications, + tangle: &G, + key_maps: &AppKeyMapConfig, +) -> AppHealthReport { + let report = app_health_check_all(datastore, keystore, notifications, tangle, key_maps).await; + let _ = app_log_buffer_flush(datastore, key_maps).await; + report +} + #[cfg(test)] mod tests { use super::{ app_health_check_app_data_active_key, app_health_check_all, + app_health_check_all_logged, app_health_check_key_maps, app_health_check_bootstrap_app_data, app_health_check_bootstrap_config, @@ -300,11 +314,13 @@ mod tests { AppHealthCheckStatus, AppHealthReport, }; + use crate::app_log_buffer_drain; use crate::AppKeyMapConfig; use async_trait::async_trait; use radroots_app_core::datastore::{ RadrootsClientDatastore, RadrootsClientDatastoreEntries, + RadrootsClientDatastoreEntry, RadrootsClientDatastoreError, RadrootsClientDatastoreResult, RadrootsClientWebDatastore, @@ -318,6 +334,7 @@ mod tests { use radroots_app_core::idb::IDB_CONFIG_DATASTORE; use radroots_app_core::backup::RadrootsClientBackupDatastorePayload; use radroots_app_core::idb::RadrootsClientIdbConfig; + use std::sync::Mutex; #[test] fn health_status_as_str() { @@ -693,4 +710,149 @@ mod tests { assert_eq!(result.status, AppHealthCheckStatus::Error); assert_eq!(result.message.as_deref(), Some("error.app.tangle.not_implemented")); } + + struct FlushDatastore { + entries: Mutex<Vec<RadrootsClientDatastoreEntry>>, + } + + impl FlushDatastore { + fn new() -> Self { + Self { + entries: Mutex::new(Vec::new()), + } + } + + fn entry_len(&self) -> usize { + self.entries.lock().unwrap_or_else(|err| err.into_inner()).len() + } + } + + #[async_trait(?Send)] + impl RadrootsClientDatastore for FlushDatastore { + fn get_config(&self) -> RadrootsClientIdbConfig { + IDB_CONFIG_DATASTORE + } + + fn get_store_id(&self) -> &str { + "test" + } + + async fn init(&self) -> RadrootsClientDatastoreResult<()> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn set(&self, _key: &str, _value: &str) -> RadrootsClientDatastoreResult<String> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn get(&self, _key: &str) -> RadrootsClientDatastoreResult<String> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn set_obj<T>(&self, key: &str, value: &T) -> RadrootsClientDatastoreResult<T> + where + T: serde::Serialize + serde::de::DeserializeOwned + Clone, + { + let serialized = + serde_json::to_string(value).map_err(|_| RadrootsClientDatastoreError::NoResult)?; + let mut entries = self.entries.lock().unwrap_or_else(|err| err.into_inner()); + entries.push(RadrootsClientDatastoreEntry::new( + key.to_string(), + Some(serialized), + )); + Ok(value.clone()) + } + + async fn update_obj<T>(&self, _key: &str, _value: &T) -> RadrootsClientDatastoreResult<T> + where + T: serde::Serialize + serde::de::DeserializeOwned + Clone, + { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T> + where + T: serde::de::DeserializeOwned, + { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn del(&self, key: &str) -> RadrootsClientDatastoreResult<String> { + let mut entries = self.entries.lock().unwrap_or_else(|err| err.into_inner()); + entries.retain(|entry| entry.key != key); + Ok(key.to_string()) + } + + async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn set_param( + &self, + _key: &str, + _key_param: &str, + _value: &str, + ) -> RadrootsClientDatastoreResult<String> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn get_param( + &self, + _key: &str, + _key_param: &str, + ) -> RadrootsClientDatastoreResult<String> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> { + let entries = self.entries.lock().unwrap_or_else(|err| err.into_inner()); + Ok(entries.clone()) + } + + async fn reset(&self) -> RadrootsClientDatastoreResult<()> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn export_backup( + &self, + ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn import_backup( + &self, + _payload: RadrootsClientBackupDatastorePayload, + ) -> RadrootsClientDatastoreResult<()> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + } + + #[test] + fn health_check_all_logged_flushes_buffer() { + let _ = app_log_buffer_drain(); + let datastore = FlushDatastore::new(); + let keystore = TestKeystore { + read_result: Err(RadrootsClientKeystoreError::MissingKey), + }; + let notifications = crate::AppNotifications::new(None); + let tangle = crate::AppTangleClientStub::new(); + let key_maps = crate::app_key_maps_default(); + let report = futures::executor::block_on(app_health_check_all_logged( + &datastore, + &keystore, + &notifications, + &tangle, + &key_maps, + )); + assert_eq!(report.key_maps.status, AppHealthCheckStatus::Ok); + assert!(datastore.entry_len() > 0); + } } diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -27,6 +27,7 @@ pub use context::{app_context, AppContext}; pub use data::{AppAppData, AppConfigData, AppConfigRole}; pub use health::{ app_health_check_all, + app_health_check_all_logged, app_health_check_app_data_active_key, app_health_check_bootstrap_app_data, app_health_check_bootstrap_config,