app

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

commit 537865a4131d8af7645c9e1d39ffc4286f674473
parent 13b48aa4c3982fed105105ace491c4c3a6bee85b
Author: triesap <tyson@radroots.org>
Date:   Mon,  2 Feb 2026 21:47:12 +0000

app: add recovery reset flow

- add recovery reset action with confirmation
- reset app data and logs before returning to setup
- surface reset status and disable actions while running
- add recovery reset copy and rebuild i18n assets

Diffstat:
Mapp/i18n/build/i18n.catalog.json | 22++++++++++++++++++++++
Mapp/i18n/build/id_map.json | 2++
Mapp/i18n/build/id_map_hash | 2+-
Mapp/i18n/build/manifest.json | 4++--
Mapp/i18n/build/packs/en.mf2pack | 0
Mapp/i18n/locales/en/messages.mf2 | 4++++
Mapp/src/app.rs | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 147 insertions(+), 3 deletions(-)

diff --git a/app/i18n/build/i18n.catalog.json b/app/i18n/build/i18n.catalog.json @@ -1160,6 +1160,28 @@ } }, { + "key": "app.recovery.reset.button", + "id": 3705159239, + "args": [], + "features": { + "select": false, + "plural_cardinal": false, + "plural_ordinal": false, + "formatters": [] + } + }, + { + "key": "app.recovery.reset.confirm", + "id": 1689963212, + "args": [], + "features": { + "select": false, + "plural_cardinal": false, + "plural_ordinal": false, + "formatters": [] + } + }, + { "key": "app.recovery.title", "id": 853393776, "args": [], diff --git a/app/i18n/build/id_map.json b/app/i18n/build/id_map.json @@ -104,6 +104,8 @@ "app.nav.ui": 2416341108, "app.not_found": 3182331848, "app.recovery.body": 3376325333, + "app.recovery.reset.button": 3705159239, + "app.recovery.reset.confirm": 1689963212, "app.recovery.title": 853393776, "app.settings.actions.export_db": 4012031673, "app.settings.actions.logout": 1314061244, diff --git a/app/i18n/build/id_map_hash b/app/i18n/build/id_map_hash @@ -1 +1 @@ -sha256:414ba68526effd986360e614ed3e75adba48b0ad41b41aa80b7cf623d77b078b +sha256:e5e758fa6183b4c5bf523e75c87909d48d14d832aa89802c1fc270495e2c80e3 diff --git a/app/i18n/build/manifest.json b/app/i18n/build/manifest.json @@ -1 +1 @@ -{"schema":1,"release_id":"dev","generated_at":"2026-02-02T00:00:00Z","default_locale":"en","supported_locales":["en"],"id_map_hash":"sha256:414ba68526effd986360e614ed3e75adba48b0ad41b41aa80b7cf623d77b078b","mf2_packs":{"en":{"kind":"base","url":"packs/en.mf2pack","hash":"sha256:aeed28bc97e29d8ddddcd91072c2b5eb1c3e4fd5078906282f9bdaa5fce9bde1","size":11253,"content_encoding":"identity","pack_schema":0}}} -\ No newline at end of file +{"schema":1,"release_id":"dev","generated_at":"2026-02-02T00:00:00Z","default_locale":"en","supported_locales":["en"],"id_map_hash":"sha256:e5e758fa6183b4c5bf523e75c87909d48d14d832aa89802c1fc270495e2c80e3","mf2_packs":{"en":{"kind":"base","url":"packs/en.mf2pack","hash":"sha256:887081ef60f00d9602e23b05c62845e5c7952b5ed3b2aaa96604ddfbe224ace5","size":11393,"content_encoding":"identity","pack_schema":0}}} +\ No newline at end of file diff --git a/app/i18n/build/packs/en.mf2pack b/app/i18n/build/packs/en.mf2pack Binary files differ. diff --git a/app/i18n/locales/en/messages.mf2 b/app/i18n/locales/en/messages.mf2 @@ -34,6 +34,10 @@ app.recovery.title = Recovery required app.recovery.body = This device could not verify its setup state. Reset the device to continue. +app.recovery.reset.button = Reset device + +app.recovery.reset.confirm = This will erase local data on this device. Continue? + # init stages app.init.stage.idle = idle diff --git a/app/src/app.rs b/app/src/app.rs @@ -1414,6 +1414,93 @@ fn SetupPage() -> impl IntoView { #[component] fn RecoveryPage() -> impl IntoView { + let context = app_context(); + let fallback_backends = RwSignal::new_local(None::<RadrootsAppBackends>); + let fallback_setup_status = RwSignal::new_local(RadrootsAppSetupStatus::Unknown); + let backends = context + .as_ref() + .map(|value| value.backends) + .unwrap_or(fallback_backends); + let setup_status = context + .as_ref() + .map(|value| value.setup_status) + .unwrap_or(fallback_setup_status); + let reset_running = RwSignal::new_local(false); + let reset_status = RwSignal::new_local(None::<String>); + let navigate = use_navigate(); + let reset_disabled = move || backends.with(|value| value.is_none()) || reset_running.get(); + let reset_label = move || reset_status.get().as_deref().map(reset_status_label); + let on_reset: Callback<MouseEvent> = { + let backends = backends.clone(); + let reset_running = reset_running.clone(); + let reset_status = reset_status.clone(); + let setup_status = setup_status.clone(); + let navigate = navigate.clone(); + Callback::new(move |_| { + if reset_running.get() { + return; + } + reset_status.set(None); + let config = backends + .with_untracked(|value| value.as_ref().map(|backends| backends.config.clone())); + let reset_running = reset_running.clone(); + let reset_status = reset_status.clone(); + let setup_status = setup_status.clone(); + let navigate = navigate.clone(); + spawn_local(async move { + let Some(config) = config else { + reset_status.set(Some("reset_missing_backends".to_string())); + return; + }; + let notifications = RadrootsAppNotifications::new(None); + let confirm_message = t!("app.recovery.reset.confirm"); + let confirm = notifications.confirm_message(&confirm_message).await; + if !confirm { + return; + } + reset_running.set(true); + reset_status.set(Some("resetting".to_string())); + let datastore = radroots_app_core::datastore::RadrootsClientWebDatastore::new( + Some(config.datastore.idb_config), + ); + let keystore = radroots_app_core::keystore::RadrootsClientWebKeystoreNostr::new( + Some(config.keystore.nostr_store), + ); + match app_init_reset( + Some(&datastore), + Some(&config.datastore.key_maps), + Some(&keystore), + ) + .await + { + Ok(()) => { + let log_datastore = logs_datastore(); + if let Err(err) = log_datastore.reset().await { + let reset_err = RadrootsAppInitError::Datastore(err); + let _ = app_log_error_emit(&reset_err); + reset_status.set(Some(reset_err.to_string())); + reset_running.set(false); + return; + } + reset_status.set(Some("reset_done".to_string())); + setup_status.set(RadrootsAppSetupStatus::Required); + navigate("/setup", Default::default()); + } + Err(err) => { + let log_datastore = logs_datastore(); + let _ = app_log_error_store( + &log_datastore, + &config.datastore.key_maps, + &err, + ) + .await; + reset_status.set(Some(err.to_string())); + } + } + reset_running.set(false); + }); + }) + }; view! { <main id="app-recovery" class="app-page app-page-fixed relative w-full flex flex-col"> <section @@ -1430,6 +1517,35 @@ fn RecoveryPage() -> impl IntoView { <p class="font-mono font-[400] text-ly0-gl text-base text-center"> {t!("app.recovery.body")} </p> + {move || { + reset_label() + .map(|label| { + view! { + <p class="font-mono font-[400] text-ly0-gl text-sm text-center"> + {label} + </p> + } + .into_any() + }) + .unwrap_or_else(|| view! { <></> }.into_any()) + }} + </div> + <div + id="app-recovery-actions" + class="flex flex-col w-full pt-6 justify-center items-center" + > + {move || { + let reset_action = RadrootsAppUiButtonLayoutAction { + label: t!("app.recovery.reset.button"), + disabled: reset_disabled(), + loading: reset_running.get(), + on_click: on_reset.clone(), + class: None, + class_label: None, + style: None, + }; + view! { <RadrootsAppUiButtonLayoutPair continue_action=reset_action class="gap-2".to_string() /> } + }} </div> </section> </main>