feat(recovery): redesign the wallet auto-recovery flow

Turns the "Daemon Error + raw log dump" moment into one calm, honest recovery
dialog plus a recovery-aware rescan screen. Presentation + orchestration only —
the file-safety logic in rebuildWalletDatabase()/restoreOriginalWallet() (source
selection, verify-before-swap, copy/rename-never-delete, .bak) is unchanged.

- One authoritative dialog with a phase machine Offer -> Working -> Done/Failed.
  The duplicate in-overlay recovery card, the untranslated red "Daemon Error"
  heading, and the raw daemon-log dump are gone for the recovery case (they stay
  for genuine, unrelated crashes).
- Offer is a choice-cards layout: "Repair automatically" (recommended, accent-
  tinted) vs "Restore original", side by side; the rare actions ("Show me the
  files", "Decide later") and a plain-language "What happens to my files?" sit in
  a quiet footer. When the rebuild helper is missing, it collapses to a single
  Restore card — never a dead end.
- Post-repair rescan shows a calm "Finishing your wallet repair" screen with
  elapsed time + the growing wallet size, instead of "RPC timeout / taking longer
  than expected / restart daemon"; the daemon-crash toast is suppressed and the
  detection toast is downgraded from red to info.
- Fixes a confirmed dead-end: if a repair succeeds but the restarted daemon then
  crashes for a *different* reason (block index, disk, OOM), the recovery flags
  now clear (in tryConnect + onConnected) so it surfaces as a normal daemon
  failure instead of freezing forever on a reassuring "don't restart" screen.
- Clickable "Wallet repair available" status-bar chip for re-entry.

The same app.cpp changes HiDPI-harden the surfaces the recovery flow lives on:
the status-bar and loading-overlay hand-drawn geometry are multiplied by dpiScale
(they rendered native-size and clipped at HiDPI / font_scale>1), the loading-
overlay status text wraps instead of running off both edges, and the node-status
banner floors its height to its DPI-baked font so the title can't clip off the top.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-10 22:11:26 -05:00
parent 001e85ac1a
commit 5edbe8a276
4 changed files with 604 additions and 130 deletions

View File

@@ -1194,15 +1194,50 @@ void I18n::loadBuiltinEnglish()
strings_["block_db_reindex_started"] = "Rebuilding the block database from your blocks — this can take a while.";
// Wallet auto-recovery warning (the node moved wallet.dat aside and loaded a salvaged copy).
strings_["wallet_recovered_title"] = "Your wallet was auto-recovered";
strings_["wallet_recovered_warn"] = "The node moved your wallet aside and loaded a salvaged copy.";
strings_["wallet_recovered_body"] = "On startup the node decided your wallet.dat looked damaged and recovered it automatically. Your ORIGINAL wallet was NOT deleted — it was renamed to \"wallet.<numbers>.bak\" in your data folder, and a salvaged copy is loaded now.\n\nThe salvaged copy may be incomplete, so the balance shown here could be wrong — don't treat it as final.\n\nThis is often a false alarm caused by leftover database files (e.g. after moving the wallet between machines). To restore your original: quit the wallet, then in the data folder rename the current wallet.dat aside, rename \"wallet.<numbers>.bak\" back to \"wallet.dat\", delete the \"database\" folder and any \"__db.*\" files, and reopen.";
strings_["wallet_recovered_open_folder"] = "Open data folder";
strings_["wallet_recovered_dismiss"] = "Keep salvaged copy";
strings_["wallet_recovered_restore"] = "Restore original wallet";
strings_["wallet_recovered_notify"] = "The node recovered your wallet and moved the original to a .bak — your shown balance may be incomplete. See the prompt to restore it.";
strings_["wallet_recovered_title"] = "Your wallet file needs a quick repair";
strings_["wallet_recovered_safety"] = "Your coins are safe.";
strings_["wallet_recovered_warn"] = "When the app started, it found that your wallet file didn't pass its consistency check — this usually happens after an app update or an unclean shutdown. The app already protected your data: it set the old file aside and loaded a repaired copy so you're not stuck.";
strings_["wallet_recovered_body"] = "Nothing has been deleted. Your original wallet is still saved on your computer as a dated backup file, and every option below only copies or renames files — it never erases one. Your keys are never regenerated, only re-read.\n\nThe repaired copy that's loaded now may be missing a few recent transactions, so your balance can look a little low until you finish below and it re-scans.";
strings_["wallet_recovered_open_folder"] = "Show me the files";
strings_["wallet_recovered_open_folder_sub"] = "Opens the wallet data folder so you can inspect the backup files yourself — nothing is changed.";
strings_["wallet_recovered_dismiss"] = "Not now — keep the repaired copy";
strings_["wallet_recovered_dismiss_sub"] = "No files change. You can reopen this anytime from the status bar; your original stays safely backed up either way.";
strings_["wallet_recovered_restore"] = "Restore the original file instead";
strings_["wallet_recovered_restore_sub"] = "Puts your largest untouched backup back in place, verbatim, then re-scans — slightly faster, but only as complete as that one file was. Your current file is kept as a dated backup either way.";
strings_["wallet_recovered_notify"] = "Your wallet file needed a repair — your original was safely backed up. Open the app to review your options.";
// In-dialog recovery lifecycle (Offer → Working → Done/Failed) + disclosures.
strings_["wallet_recovery_working_label"] = "Working";
strings_["wallet_recovery_done"] = "Done";
strings_["wallet_recovery_other_options"] = "Other options";
strings_["wallet_recovery_details_label"] = "Show technical details";
strings_["wallet_recovery_try_other"] = "Try the other option";
strings_["wallet_recovery_success_body"] = "The app loaded your repaired wallet and is re-scanning to total your balance — this can take a few minutes. It reads every record it can, but on rare damaged files it may recover slightly fewer — or occasionally more — addresses than before. Once the re-scan finishes, check your balance and history look right.";
strings_["wallet_recovery_success_restore"] = "Your largest backup file is back in place and loading now, and the app is re-scanning. It's restored verbatim, so it's exactly as complete as that file was — check your balance once the re-scan finishes.";
strings_["wallet_recovery_failure_title"] = "The repair didn't go through";
strings_["wallet_recovery_failure_body"] = "The repair ran, but its result didn't pass verification (it couldn't be read, or had no addresses), so it was discarded automatically before it ever replaced anything. Your wallet is exactly as it was before you clicked — nothing on disk changed.";
strings_["wallet_recovery_whats_happened"] = "What happened?";
// Choice-card layout: two side-by-side cards (recommended one highlighted) + quiet footer links.
strings_["wallet_recovery_rebuild_card"] = "Repair automatically";
strings_["wallet_recovery_restore_card"] = "Restore original";
strings_["wallet_recovery_rebuild_card_desc"] = "Reads every recoverable record into a clean file, then restarts. The most thorough option.";
strings_["wallet_recovery_restore_card_desc"] = "Puts your largest untouched backup back, verbatim \xE2\x80\x94 only as complete as that file was.";
strings_["wallet_recovery_recommended"] = "RECOMMENDED";
strings_["wallet_recovery_repair_go"] = "Repair";
strings_["wallet_recovery_restore_go"] = "Restore";
strings_["wallet_recovery_notnow_short"] = "Not now";
strings_["wallet_recovery_decide_later"] = "Decide later";
strings_["wallet_recovery_decide_later_tip"] = "Closes this and keeps the copy that's loaded now. Nothing is changed, and you can repair anytime \xE2\x80\x94 the status bar keeps a \xE2\x80\x9CWallet repair available\xE2\x80\x9D link.";
strings_["wallet_recovery_files_label"] = "What happens to my files?";
strings_["wallet_recovery_files_detail"] = "Your original wallet is still here \xE2\x80\x94 the app renamed it to a dated backup (wallet.<numbers>.bak) in your data folder and hasn't deleted anything. \xE2\x80\x9CRepair\xE2\x80\x9D reads every record from your fullest wallet into a brand-new clean file. \xE2\x80\x9CRestore\xE2\x80\x9D copies your largest backup back exactly as it is. The copy loaded right now is kept as a backup too.";
// Post-repair rescan screen (shown while the node restarts + re-scans, before it answers RPC).
strings_["wallet_recovery_rescan_title"] = "Finishing your wallet repair";
strings_["wallet_recovery_rescan_body"] = "Re-scanning the blockchain to rebuild your balance and transaction history. This can take several minutes — you don't need to do anything, and please don't restart the node while it's running.";
strings_["wallet_recovery_rescan_elapsed"] = "Working for %s";
strings_["wallet_recovery_rescan_size"] = "Rebuilt wallet is now %s and still filling in";
strings_["wallet_recovery_rescan_slow"] = "This is taking longer than usual, which is normal for a large wallet. It's safe to leave it running — you can watch progress under Advanced \xE2\x96\xB8 Console.";
strings_["sb_finishing_repair"] = "Finishing wallet repair — re-scanning…";
// One-click "Restore original wallet" flow.
strings_["wallet_restore_started"] = "Restoring your original wallet and restarting the node…";
strings_["wallet_restore_started"] = "Restoring your original file — please don't close this window.";
strings_["wallet_restore_busy"] = "The node is busy restarting — try again in a moment.";
strings_["wallet_restore_ok"] = "Original wallet restored. The node is loading it now.";
strings_["wallet_restore_no_backup"] = "Couldn't find a wallet.<timestamp>.bak to restore. Nothing was changed.";
@@ -1212,8 +1247,9 @@ void I18n::loadBuiltinEnglish()
strings_["wallet_restore_copy_failed"] = "Couldn't install the backup wallet; your current wallet was left in place.";
strings_["wallet_restore_no_restart"] = "Your original wallet was restored, but the node didn't restart — start it from Settings.";
// One-click "Rebuild wallet database" flow (fixes a BDB-inconsistent wallet that keeps getting salvaged).
strings_["wallet_recovered_rebuild"] = "Rebuild wallet database (recommended)";
strings_["wallet_rebuild_started"] = "Rebuilding your wallet database and restarting the node…";
strings_["wallet_recovered_rebuild"] = "Repair automatically (recommended)";
strings_["wallet_recovered_rebuild_sub"] = "Re-reads every recoverable record from your fullest wallet file and writes a clean new one, then restarts. The most thorough option — your current file is kept as a dated backup either way.";
strings_["wallet_rebuild_started"] = "Repairing your wallet file — please don't close this window.";
strings_["wallet_rebuild_ok"] = "Wallet database rebuilt — the node is loading it and rescanning for your balance.";
strings_["wallet_rebuild_no_helper"] = "The wallet-rebuild helper isn't available in this build. Use Restore, or rebuild manually.";
strings_["wallet_rebuild_no_source"] = "Couldn't find a readable wallet to rebuild. Nothing was changed.";
@@ -1345,7 +1381,7 @@ void I18n::loadBuiltinEnglish()
strings_["sb_daemon_crashed"] = "Daemon crashed %d times";
strings_["sb_daemon_start_failed"] = "Couldn't start dragonxd";
strings_["sb_block_db_unreadable"] = "Block database unreadable — rebuild required";
strings_["sb_wallet_needs_recovery"] = "Wallet needs recovery — see the prompt";
strings_["sb_wallet_needs_recovery"] = "Wallet repair available";
// Persistent node-status banner (App::renderNodeStatusBanner).
strings_["node_banner_offline_title"] = "Not connected to the DragonX node";
strings_["node_banner_crashed_title"] = "The node stopped unexpectedly";