app

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

commit 95f0b2a4ff18ad4f3212a6025fd7a0646199f9dd
parent 379a18851617478c1db2bedb5829c2749908624f
Author: triesap <triesap@radroots.dev>
Date:   Tue, 20 Jan 2026 15:49:52 +0000

app: add log clear action

- add datastore helper to delete log entries by prefix

- expose clear action on logs page

- refresh list after clear

- add unit tests for log clear helper

Diffstat:
Mapp/src/lib.rs | 1+
Mapp/src/logging.rs | 43+++++++++++++++++++++++++++++++++++++++++--
Mapp/src/logs.rs | 103+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
3 files changed, 109 insertions(+), 38 deletions(-)

diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -58,6 +58,7 @@ pub use logging::{ app_log_buffer_flush, app_log_buffer_push, app_log_entries_dump, + app_log_entries_clear, app_log_entries_load, app_log_entries_prune, app_log_error_emit, diff --git a/app/src/logging.rs b/app/src/logging.rs @@ -410,6 +410,18 @@ pub async fn app_log_entries_load<T: RadrootsClientDatastore>( Ok(out) } +pub async fn app_log_entries_clear<T: RadrootsClientDatastore>( + datastore: &T, + key_maps: &AppKeyMapConfig, +) -> AppLogResult<usize> { + let prefix = app_log_entry_prefix(key_maps)?; + let removed = datastore + .del_pref(&prefix) + .await + .map_err(AppLogError::Datastore)?; + Ok(removed.len()) +} + pub fn app_log_entries_dump(entries: &[AppLogEntry]) -> String { let mut out = String::new(); for (idx, entry) in entries.iter().enumerate() { @@ -497,6 +509,7 @@ pub fn app_logging_init(meta: Option<AppLogMetadata>) -> AppLoggingResult<()> { #[cfg(test)] mod tests { use super::{ + app_log_entries_clear, app_log_entries_dump, app_log_entries_load, app_log_entries_prune, @@ -618,8 +631,19 @@ mod tests { Ok(key.to_string()) } - async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> { - Err(RadrootsClientDatastoreError::IdbUndefined) + async fn del_pref(&self, key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> { + let mut entries = self.entries.lock().unwrap_or_else(|err| err.into_inner()); + let mut removed = Vec::new(); + let mut kept = Vec::new(); + for entry in entries.drain(..) { + if entry.key.starts_with(key_prefix) { + removed.push(entry.key); + } else { + kept.push(entry); + } + } + *entries = kept; + Ok(removed) } async fn set_param( @@ -737,6 +761,21 @@ mod tests { } #[test] + fn log_entries_clear_removes_prefixed_keys() { + let key_maps = app_key_maps_default(); + let key_a = app_log_entry_key(&key_maps, "a").expect("key"); + let entries = vec![ + RadrootsClientDatastoreEntry::new(key_a, Some(String::from("{}"))), + RadrootsClientDatastoreEntry::new(String::from("other:1"), Some(String::from("{}"))), + ]; + let datastore = TestDatastore::new(entries); + let removed = futures::executor::block_on(app_log_entries_clear(&datastore, &key_maps)) + .expect("clear"); + assert_eq!(removed, 1); + assert_eq!(datastore.len(), 1); + } + + #[test] fn log_entries_dump_serializes_jsonl() { let entries = vec![AppLogEntry { id: String::from("a"), diff --git a/app/src/logs.rs b/app/src/logs.rs @@ -9,6 +9,7 @@ use radroots_app_core::datastore::RadrootsClientWebDatastore; use crate::{ app_context, app_log_buffer_flush, + app_log_entries_clear, app_log_entries_dump, app_log_entries_load, AppLogEntry, @@ -30,43 +31,72 @@ pub fn LogsPage() -> impl IntoView { let dump = RwSignal::new_local(String::new()); let loading = RwSignal::new_local(false); let did_load = RwSignal::new_local(false); - let context = app_context(); - let refresh = Rc::new(move || { - let Some(context) = context.clone() else { - entries.set(Vec::new()); - dump.set(String::new()); - return; - }; - let config = context - .backends - .with_untracked(|value| value.as_ref().map(|backends| backends.config.clone())); - let Some(config) = config else { - entries.set(Vec::new()); - dump.set(String::new()); - return; - }; - loading.set(true); - let entries_signal = entries; - let dump_signal = dump; - let loading_signal = loading; - spawn_local(async move { - let datastore = RadrootsClientWebDatastore::new(Some(config.datastore.idb_config)); - let _ = app_log_buffer_flush(&datastore, &config.datastore.key_maps).await; - let result = app_log_entries_load(&datastore, &config.datastore.key_maps).await; - match result { - Ok(mut items) => { - items.sort_by(|a, b| b.timestamp_ms.cmp(&a.timestamp_ms)); - dump_signal.set(app_log_entries_dump(&items)); - entries_signal.set(items); + let context = Rc::new(app_context()); + let refresh = { + let context = Rc::clone(&context); + Rc::new(move || { + let Some(context) = context.as_ref().clone() else { + entries.set(Vec::new()); + dump.set(String::new()); + return; + }; + let config = context + .backends + .with_untracked(|value| value.as_ref().map(|backends| backends.config.clone())); + let Some(config) = config else { + entries.set(Vec::new()); + dump.set(String::new()); + return; + }; + loading.set(true); + let entries_signal = entries; + let dump_signal = dump; + let loading_signal = loading; + spawn_local(async move { + let datastore = RadrootsClientWebDatastore::new(Some(config.datastore.idb_config)); + let _ = app_log_buffer_flush(&datastore, &config.datastore.key_maps).await; + let result = app_log_entries_load(&datastore, &config.datastore.key_maps).await; + match result { + Ok(mut items) => { + items.sort_by(|a, b| b.timestamp_ms.cmp(&a.timestamp_ms)); + dump_signal.set(app_log_entries_dump(&items)); + entries_signal.set(items); + } + Err(err) => { + dump_signal.set(format!("error: {err}")); + entries_signal.set(Vec::new()); + } } - Err(err) => { - dump_signal.set(format!("error: {err}")); - entries_signal.set(Vec::new()); - } - } - loading_signal.set(false); - }); - }); + loading_signal.set(false); + }); + }) + }; + let clear = { + let context = Rc::clone(&context); + let refresh = Rc::clone(&refresh); + Rc::new(move || { + let Some(context) = context.as_ref().clone() else { + entries.set(Vec::new()); + dump.set(String::new()); + return; + }; + let config = context + .backends + .with_untracked(|value| value.as_ref().map(|backends| backends.config.clone())); + let Some(config) = config else { + entries.set(Vec::new()); + dump.set(String::new()); + return; + }; + loading.set(true); + let refresh = Rc::clone(&refresh); + spawn_local(async move { + let datastore = RadrootsClientWebDatastore::new(Some(config.datastore.idb_config)); + let _ = app_log_entries_clear(&datastore, &config.datastore.key_maps).await; + refresh(); + }); + }) + }; let refresh_effect = Rc::clone(&refresh); Effect::new(move || { if did_load.get() { @@ -81,6 +111,7 @@ pub fn LogsPage() -> impl IntoView { <div style="display:flex;align-items:center;gap:12px;"> <div style="font-size:18px;font-weight:600;">"logs"</div> <button on:click=move |_| refresh()>"refresh"</button> + <button on:click=move |_| clear()>"clear"</button> <div style="font-size:12px;color:#6b7280;">{status_label}</div> </div> <div style="margin-top:12px;display:flex;flex-wrap:wrap;gap:16px;">