fix(lite): surface sync failures instead of a frozen "Syncing 99.9%"; don't report a lost server as "Synced"

Lite-parity audit, sync-progress semantics (state/presentation only — no send/sync backend call changed):
- A scan failure set scan->failed + lite_error, but no UI read lite_error, so the user saw "Syncing 99.9%"
  forever. The status bar now shows the error text on a lite scan/server failure.
- After the initial catch-up, a server loss on the cheap probe path (ProbeFailed) left the last "Synced"
  model on screen. The worker now re-publishes that model demoted (not complete, reason "server unreachable").
- refreshModelImpl's height-read "synced" override no longer fires while syncstatus says the backend is
  still scanning or is indeterminate, and a server-bound read failure in a full cycle demotes to not-synced.
- The syncstatus parser no longer treats a missing/unrecognised "syncing" field as fully synced: it reports
  an explicit "indeterminate" (not complete). Explicit "false" stays caught-up; the fake backend's
  syncing:true 1000/1000 behaviour is unchanged.

Tests: parser cases for missing/odd/idle "syncing" (testLiteSyncStatus*).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 15:16:36 -05:00
co-authored by Claude Opus 4.8
parent 02fa1a228a
commit 6f369376ec
6 changed files with 64 additions and 4 deletions
+4
View File
@@ -3110,6 +3110,10 @@ void App::renderStatusBar()
} else if (rpc_busy_gate_->busy()) {
// The node holds back our requests (a long operation holds its cs_main): data may be stale.
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::Warning()), "%s", TR("sb_node_busy"));
} else if (isLiteBuild() && !state_.sync.lite_error.empty()) {
// Lite scan/server failure: show the error instead of a frozen 'Syncing 99.9%'.
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::Error()), TR("sb_lite_sync_error"),
state_.sync.lite_error.c_str());
} else if (state_.sync.syncing) {
int chainTip = state_.longestchain > 0 ? state_.longestchain : state_.sync.headers;
int blocksLeft = chainTip - state_.sync.blocks;