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