app

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

commit 26566ff74fe5781873eeb062421d63813743e036
parent 63910f32cb22693e8d29ee5f98a9ef4b89dfbb57
Author: triesap <triesap@radroots.dev>
Date:   Wed, 21 Jan 2026 14:32:12 +0000

app: add support bundle flow

- add support instructions content and display

- add support bundle and copy instructions actions

- track support status in logs toolbar

- add unit test for instructions text

Diffstat:
Mapp/src/logs.rs | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 72 insertions(+), 0 deletions(-)

diff --git a/app/src/logs.rs b/app/src/logs.rs @@ -202,6 +202,16 @@ fn log_dump_with_context(entries: &[RadrootsAppLogEntry], header: String) -> Str format!("{header}\n{}", app_log_entries_dump(entries)) } +fn support_instructions_text() -> String { + let lines = [ + "support bundle", + "1) download the log dump jsonl file", + "2) share the file with support", + "3) include notes about the issue and time window", + ]; + lines.join("\n") +} + #[cfg(any(test, target_arch = "wasm32"))] fn log_dump_filename_from_ms(timestamp_ms: i64) -> String { format!("radroots-logs-{timestamp_ms}.jsonl") @@ -272,6 +282,8 @@ pub fn RadrootsAppLogsPage() -> impl IntoView { let loading = RwSignal::new_local(false); let dump_status = RwSignal::new_local(None::<String>); let dump_action_running = RwSignal::new_local(false); + let support_status = RwSignal::new_local(None::<String>); + let support_running = RwSignal::new_local(false); let did_load = RwSignal::new_local(false); let interval_started = RwSignal::new_local(false); let filter_query = RwSignal::new_local(String::new()); @@ -450,6 +462,43 @@ pub fn RadrootsAppLogsPage() -> impl IntoView { }); }) }; + let copy_support = { + Rc::new(move || { + let text = support_instructions_text(); + support_running.set(true); + spawn_local(async move { + let status = match log_dump_copy(text).await { + Ok(()) => String::from("support_copy_ok"), + Err(err) => err, + }; + support_status.set(Some(status)); + support_running.set(false); + }); + }) + }; + let support_bundle = { + let dump_text = dump_text.clone(); + Rc::new(move || { + let text = dump_text.get(); + if text.is_empty() { + support_status.set(Some(String::from("dump_empty"))); + return; + } + support_running.set(true); + let instructions = support_instructions_text(); + spawn_local(async move { + let download = log_dump_download(text).await; + let copy = log_dump_copy(instructions).await; + let status = match (download, copy) { + (Ok(()), Ok(())) => String::from("support_bundle_ready"), + (Err(err), _) => err, + (_, Err(err)) => err, + }; + support_status.set(Some(status)); + support_running.set(false); + }); + }) + }; let refresh_effect = Rc::clone(&refresh); let context_effect = Rc::clone(&context); Effect::new(move || { @@ -503,6 +552,9 @@ 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 support_label = + move || support_status.get().unwrap_or_else(|| "idle".to_string()); + let support_disabled = move || support_running.get(); let prev_disabled = move || page_index.get() == 0; let next_disabled = move || { let total = page_total.get(); @@ -605,6 +657,13 @@ pub fn RadrootsAppLogsPage() -> impl IntoView { > "next" </button> + <button on:click=move |_| support_bundle() disabled=support_disabled> + "support bundle" + </button> + <button on:click=move |_| copy_support() disabled=support_disabled> + "copy instructions" + </button> + <div style="font-size:12px;color:#6b7280;">{support_label}</div> </div> <div style="margin-top:12px;display:flex;flex-wrap:wrap;gap:16px;"> <section style="flex:1 1 520px;min-width:280px;"> @@ -660,6 +719,12 @@ pub fn RadrootsAppLogsPage() -> impl IntoView { prop:value=move || dump_text.get() style="margin-top:8px;width:100%;height:60vh;border:1px solid #e5e7eb;border-radius:8px;padding:8px;font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;" ></textarea> + <div style="margin-top:12px;font-weight:600;font-size:14px;">"support instructions"</div> + <textarea + readonly + prop:value=move || support_instructions_text() + style="margin-top:8px;width:100%;height:140px;border:1px solid #e5e7eb;border-radius:8px;padding:8px;font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;" + ></textarea> </section> </div> </main> @@ -681,6 +746,7 @@ mod tests { log_page_count, log_timestamp_matches, parse_log_timestamp, + support_instructions_text, logs_auto_refresh_ms, logs_max_visible, logs_page_size_default, @@ -811,4 +877,10 @@ mod tests { assert_eq!(log_page_index_clamp(3, 2), 1); assert_eq!(log_page_index_clamp(1, 3), 1); } + + #[test] + fn support_instructions_are_present() { + let text = support_instructions_text(); + assert!(text.contains("support bundle")); + } }