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
+12 -3
View File
@@ -587,11 +587,20 @@ LiteSyncStatusParseResult parseLiteSyncStatusResponse(const json& value)
// The backend reports "syncing" as a STRING ("true"/"false"). synced_blocks/total_blocks
// are present only while actively syncing; an idle wallet returns just {"syncing":"false"}.
std::string syncing = "false";
if (!readOptionalStringField(value, "syncing", syncing, result)) return result;
std::optional<std::string> syncingField;
if (!readOptionalStringField(value, "syncing", syncingField, result)) return result;
const std::string syncing = syncingField.value_or(std::string());
const bool isSyncing = (syncing == "true" || syncing == "1");
const bool isIdle = (syncing == "false" || syncing == "0");
if (isSyncing) {
if (!isSyncing && !isIdle) {
// Missing/odd "syncing": we do not KNOW the wallet is caught up, so never default to synced.
result.syncStatus.syncedBlocks = 0;
result.syncStatus.totalBlocks = 0;
result.syncStatus.complete = false;
result.syncStatus.indeterminate = true;
result.syncStatus.progress = 0.0;
} else if (isSyncing) {
if (!readRequiredUnsignedField(value, "synced_blocks", result.syncStatus.syncedBlocks, result)) return result;
if (!readRequiredUnsignedField(value, "total_blocks", result.syncStatus.totalBlocks, result)) return result;
result.syncStatus.complete = false; // actively syncing