feat(ux): robust sync ETA, persist Explorer settings, gate export-all out of lite

- Sync ETA: a new pure util::SyncRateEstimator (App-owned) replaces the
  function-local statics that went stale after a rescan/restart (producing the
  "412h" ETA). It self-resets on a height drop and on connect/disconnect/rescan/
  restart (via an atomic flag, since onConnected can run off-thread), formats
  with days, and uses the Warning() token.
- Settings: the Explorer-tab URL fields + custom-fees/fetch-prices checkboxes
  now persist on edit — they were the only controls still relying on the
  About-only Save button that a user on this tab never sees.
- Export-all-keys: hidden in the lite build (which has its own key export),
  instead of dead-ending on "Not connected to the daemon".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 13:02:46 -05:00
co-authored by Claude Opus 4.8
parent 8fe20f6385
commit f3f0b32a48
5 changed files with 99 additions and 42 deletions
+13 -37
View File
@@ -3084,45 +3084,19 @@ void App::renderStatusBar()
int blocksLeft = chainTip - state_.sync.blocks;
if (blocksLeft < 0) blocksLeft = 0;
// Calculate sync speed (blocks/sec) using a smoothed rolling average
static int s_prev_blocks = 0;
static double s_prev_time = 0.0;
static double s_blocks_per_sec = 0.0;
double now = ImGui::GetTime();
if (s_prev_time == 0.0) {
// First sample — seed baseline, don't compute rate yet
s_prev_blocks = state_.sync.blocks;
s_prev_time = now;
} else if (state_.sync.blocks > s_prev_blocks) {
double dt = now - s_prev_time;
if (dt > 0.5) {
double raw = (state_.sync.blocks - s_prev_blocks) / dt;
// Exponential smoothing (alpha ~0.3 per update)
s_blocks_per_sec = s_blocks_per_sec > 0.0
? s_blocks_per_sec * 0.7 + raw * 0.3
: raw;
s_prev_blocks = state_.sync.blocks;
s_prev_time = now;
}
} else if (now - s_prev_time > 10.0) {
// No new blocks for 10s — decay the displayed rate
s_blocks_per_sec *= 0.5;
s_prev_time = now;
}
if (s_blocks_per_sec > 0.1) {
int eta_sec = (int)(blocksLeft / s_blocks_per_sec);
// App-owned smoothed sync-rate estimator: self-resets after a rescan/restart height drop (and on
// connect/disconnect via the pending flag), so the ETA never goes stale/absurd ("412h"); days-aware.
if (sync_eta_reset_pending_.exchange(false)) sync_eta_.reset();
sync_eta_.update(state_.sync.blocks, ImGui::GetTime());
const ImVec4 syncCol = ImGui::ColorConvertU32ToFloat4(ui::material::Warning());
const long long etaSec = sync_eta_.etaSeconds(blocksLeft);
if (etaSec >= 0) {
char eta[32];
if (eta_sec >= 3600)
snprintf(eta, sizeof(eta), "%dh %dm", eta_sec / 3600, (eta_sec % 3600) / 60);
else if (eta_sec >= 60)
snprintf(eta, sizeof(eta), "%dm %ds", eta_sec / 60, eta_sec % 60);
else
snprintf(eta, sizeof(eta), "%ds", eta_sec);
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), TR("sb_syncing_eta"),
state_.sync.verification_progress * 100.0, blocksLeft, s_blocks_per_sec, eta);
util::formatEta(etaSec, eta, sizeof(eta));
ImGui::TextColored(syncCol, TR("sb_syncing_eta"),
state_.sync.verification_progress * 100.0, blocksLeft, sync_eta_.rate(), eta);
} else {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), TR("sb_syncing_basic"),
ImGui::TextColored(syncCol, TR("sb_syncing_basic"),
state_.sync.verification_progress * 100.0, blocksLeft);
}
} else if (state_.connected && !isLiteBuild()) {
@@ -6337,6 +6311,7 @@ void App::rescanBlockchain()
// actually observe the restarted daemon rescanning — so the first poll (which may still reach the
// pre-restart daemon and see rescanning=false) can't be misread as instant completion.
state_.sync.rescanning = true;
sync_eta_reset_pending_.store(true, std::memory_order_relaxed); // rescan drops height → reset ETA
rescan_confirmed_active_ = false;
user_initiated_rescan_ = true; // user-triggered rescan → its completion should toast
state_.sync.rescan_progress = 0.0f;
@@ -8136,6 +8111,7 @@ std::string App::buildDiagnosticsReport()
void App::restartDaemon()
{
sync_eta_reset_pending_.store(true, std::memory_order_relaxed);
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;