app

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

commit 0b415b05b8dbb8015cca29d480eed7054a12c498
parent 79c6b924ba35694a489faa97f356591832062e08
Author: triesap <triesap@radroots.dev>
Date:   Wed, 21 Jan 2026 14:16:07 +0000

app: add logs pagination controls

- add prev/next controls and page size selector

- clamp page index when filters change

- track page count for status display

- add unit tests for page index clamp

Diffstat:
Mapp/src/logs.rs | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 73 insertions(+), 1 deletion(-)

diff --git a/app/src/logs.rs b/app/src/logs.rs @@ -141,6 +141,16 @@ fn log_entries_page( entries[start..end].to_vec() } +fn log_page_index_clamp(page_index: usize, total_pages: usize) -> usize { + if total_pages == 0 { + return 0; + } + if page_index >= total_pages { + return total_pages - 1; + } + page_index +} + #[cfg(any(test, target_arch = "wasm32"))] fn log_dump_filename_from_ms(timestamp_ms: i64) -> String { format!("radroots-logs-{timestamp_ms}.jsonl") @@ -243,6 +253,20 @@ pub fn RadrootsAppLogsPage() -> impl IntoView { let page_total = Memo::new(move |_| { log_page_count(filtered_entries.get().len(), page_size.get()) }); + Effect::new(move || { + let _ = filter_query.get(); + let _ = filter_level.get(); + let _ = filter_from.get(); + let _ = filter_to.get(); + page_index.set(0); + }); + Effect::new(move || { + let total_pages = page_total.get(); + let next = log_page_index_clamp(page_index.get(), total_pages); + if next != page_index.get() { + page_index.set(next); + } + }); let dump_text = Memo::new(move |_| { if let Some(err) = dump_error.get() { return err; @@ -388,6 +412,11 @@ pub fn RadrootsAppLogsPage() -> impl IntoView { let dump_action_label = move || dump_status.get().unwrap_or_else(|| "idle".to_string()); let dump_action_disabled = move || dump_action_running.get(); + let prev_disabled = move || page_index.get() == 0; + let next_disabled = move || { + let total = page_total.get(); + total == 0 || page_index.get() + 1 >= total + }; view! { <main> <div style="display:flex;align-items:center;gap:12px;"> @@ -440,17 +469,52 @@ pub fn RadrootsAppLogsPage() -> impl IntoView { } style="width:130px;border:1px solid #e5e7eb;border-radius:8px;padding:6px 8px;font-size:12px;" /> + <select + prop:value=move || page_size.get().to_string() + on:change=move |ev| { + if let Ok(size) = event_target_value(&ev).parse::<usize>() { + page_size.set(size); + page_index.set(0); + } + } + style="border:1px solid #e5e7eb;border-radius:8px;padding:6px 8px;font-size:12px;" + > + <option value="25">"25"</option> + <option value="50">"50"</option> + <option value="100">"100"</option> + <option value="250">"250"</option> + </select> <div style="font-size:12px;color:#6b7280;"> {move || { let total = entries.get().len(); let visible = filtered_entries.get().len(); let limit = filter_limit.get(); - let page = page_index.get() + 1; let pages = page_total.get(); + let page = if pages == 0 { 0 } else { page_index.get() + 1 }; format!("showing {visible} of {total} (limit {limit}) page {page}/{pages}") }} </div> </div> + <div style="margin-top:8px;display:flex;align-items:center;gap:8px;"> + <button + on:click=move |_| { + let next = page_index.get().saturating_sub(1); + page_index.set(next); + } + disabled=prev_disabled + > + "prev" + </button> + <button + on:click=move |_| { + let next = page_index.get() + 1; + page_index.set(next); + } + disabled=next_disabled + > + "next" + </button> + </div> <div style="margin-top:12px;display:flex;flex-wrap:wrap;gap:16px;"> <section style="flex:1 1 520px;min-width:280px;"> <div style="font-weight:600;font-size:14px;">"entries"</div> @@ -518,6 +582,7 @@ mod tests { log_dump_with_header, log_entry_matches, log_entries_page, + log_page_index_clamp, log_page_count, log_timestamp_matches, parse_log_timestamp, @@ -623,4 +688,11 @@ mod tests { assert_eq!(page.len(), 2); assert_eq!(page[0].id, "id-2"); } + + #[test] + fn log_page_index_clamp_bounds() { + assert_eq!(log_page_index_clamp(0, 0), 0); + assert_eq!(log_page_index_clamp(3, 2), 1); + assert_eq!(log_page_index_clamp(1, 3), 1); + } }