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:
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;">