feat(ui): say "Node busy" instead of "Launching dragonxd"; honest quit copy
A node rebuilding note witnesses for an hour was shown as "Starting DragonX node... Launching dragonxd", which invited closing it. - Overlay, a new NodeBusy banner and the status bar now say what the node is doing (rebuilding witnesses / catching up on blocks / up but not answering), with the elapsed time and Phase-1 block progress. A rebuild during startup warm-up replaces the stale "-28 Activating best chain" text, and the overlay re-applies its text once a second instead of only on a connect attempt (30s apart while the node is busy). - Quit prompt: elapsed time, "Keep node running & quit" is the default, "Stop anyway & quit" becomes "Stop node & wait" (it no longer kills). - A reinstall skipped because the node is still running now notifies. - Status bar: don't repeat the warm-up status on the right, and don't print "Connected" there in non-English locales. - i18n: 31 new keys in all 8 languages; 10 dead force-quit/old-modal keys removed; CJK subset rebuilt for the new glyphs. - Tests: chooseDaemonStatus, NodeBusy banner priority, and every new key present in all 8 JSONs with matching printf conversions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+114
-10
@@ -946,6 +946,18 @@ void App::update()
|
||||
pumpRpcBusyCanary();
|
||||
const bool rpcBusy = rpc_busy_gate_->busy();
|
||||
|
||||
// Keep the "node starting / warming up / busy" overlay current between connect attempts and polls,
|
||||
// unless something else has replaced the status (see init_status_title_, applyWarmupText).
|
||||
if (ImGui::GetTime() >= status_refresh_at_) {
|
||||
status_refresh_at_ = ImGui::GetTime() + 1.0;
|
||||
if (state_.warming_up && !warmup_rpc_message_.empty() && connection_status_ == state_.warmup_status) {
|
||||
applyWarmupText(warmup_rpc_message_);
|
||||
} else if (state_.daemon_initializing && !state_.connected && !init_status_title_.empty() &&
|
||||
connection_status_ == init_status_title_) {
|
||||
applyDaemonInitStatus(init_status_busy_);
|
||||
}
|
||||
}
|
||||
|
||||
// Fast refresh (mining stats + daemon memory) every second
|
||||
// Skip when wallet is locked — no need to poll, and queued tasks
|
||||
// would delay the PIN unlock worker task.
|
||||
@@ -1629,6 +1641,8 @@ void App::render()
|
||||
snprintf(deletedMsg, sizeof(deletedMsg), TR("appx_blockchain_data_deleted"), deletedN);
|
||||
ui::Notifications::instance().success(deletedMsg);
|
||||
}
|
||||
if (pending_reinstall_node_busy_.exchange(false, std::memory_order_relaxed))
|
||||
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_node_busy"), 12.0f);
|
||||
}
|
||||
|
||||
|
||||
@@ -2315,6 +2329,9 @@ void App::renderNodeStatusBanner()
|
||||
in.lite_open_error = lite_open_error_;
|
||||
in.tip_known = state_.sync.tip_known;
|
||||
in.blocks = state_.sync.blocks;
|
||||
const NodeBusyText busy = nodeBusyText();
|
||||
in.node_busy = busy.busy;
|
||||
in.node_busy_detail = busy.compact; // one line; the overlay and the quit prompt explain more
|
||||
|
||||
const ui::NodeBannerState banner = ui::evaluateNodeStatusBanner(in);
|
||||
if (!banner.show) return;
|
||||
@@ -2349,6 +2366,8 @@ void App::renderNodeStatusBanner()
|
||||
title = TR("node_banner_lite_open_failed"); icon = ICON_MD_ERROR; break;
|
||||
case ui::NodeBannerReason::NoPeers:
|
||||
title = TR("node_banner_no_peers_title"); icon = ICON_MD_CLOUD_OFF; break;
|
||||
case ui::NodeBannerReason::NodeBusy:
|
||||
title = busy.title.c_str(); icon = ICON_MD_HOURGLASS_EMPTY; break;
|
||||
case ui::NodeBannerReason::FullNodeOffline:
|
||||
default:
|
||||
title = TR("node_banner_offline_title"); icon = ICON_MD_CLOUD_OFF; break;
|
||||
@@ -2672,6 +2691,9 @@ void App::renderStatusBar()
|
||||
const char* dotStr = (dots == 0) ? "." : (dots == 1) ? ".." : (dots == 2) ? "..." : "";
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", TR("sb_rescanning"), dotStr);
|
||||
}
|
||||
} 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 (state_.sync.syncing) {
|
||||
int chainTip = state_.longestchain > 0 ? state_.longestchain : state_.sync.headers;
|
||||
int blocksLeft = chainTip - state_.sync.blocks;
|
||||
@@ -2803,7 +2825,11 @@ void App::renderStatusBar()
|
||||
// with a small gap.
|
||||
float gap = sbSectionGap;
|
||||
float occupiedX = versionX; // leftmost X used by the version + connection status so far
|
||||
if (!connection_status_.empty() && connection_status_ != "Connected") {
|
||||
// The left of the bar already shows the warmup / "node busy" status; don't print it twice.
|
||||
const bool shownOnLeft = (state_.warming_up || state_.daemon_initializing) &&
|
||||
connection_status_ == state_.warmup_status;
|
||||
if (!connection_status_.empty() && connection_status_ != "Connected" &&
|
||||
connection_status_ != TR("connected") && !shownOnLeft) {
|
||||
// During a post-repair rescan the raw status is a scary "RPC request failed: Timeout" — show a
|
||||
// calm line instead (the rescan legitimately can't answer RPC yet).
|
||||
const std::string statusBase = post_recovery_rescan_ ? std::string(TR("sb_finishing_repair"))
|
||||
@@ -5833,6 +5859,7 @@ void App::reinstallBundledDaemon()
|
||||
if (stillUp()) {
|
||||
DEBUG_LOGF("[App] Reinstall bundled daemon: aborted — a dragonxd process is still running; "
|
||||
"binaries left untouched and no new node started\n");
|
||||
pending_reinstall_node_busy_.store(true, std::memory_order_relaxed); // told on the UI thread
|
||||
return;
|
||||
}
|
||||
// Overwrite the installed dragonx binaries (dragonxd/cli/tx) with the bundled ones.
|
||||
@@ -6084,6 +6111,70 @@ void App::feedDaemonBusy(const std::string& chunk)
|
||||
if (daemon_busy_reset_pending_.exchange(false)) daemon_busy_.reset();
|
||||
daemon_busy_.feed(chunk);
|
||||
node_rebuilding_ = daemon_busy_.rebuilding();
|
||||
if (!daemon_busy_.busy()) busy_first_seen_ = {};
|
||||
else if (busy_first_seen_ == std::chrono::steady_clock::time_point{}) busy_first_seen_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
static std::string formatBusyDuration(double secs)
|
||||
{
|
||||
const long s = secs > 0 ? static_cast<long>(secs) : 0;
|
||||
char buf[32];
|
||||
if (s >= 3600) snprintf(buf, sizeof(buf), "%ld:%02ld:%02ld", s / 3600, (s / 60) % 60, s % 60);
|
||||
else snprintf(buf, sizeof(buf), "%ld:%02ld", s / 60, s % 60);
|
||||
return buf;
|
||||
}
|
||||
|
||||
App::NodeBusyText App::nodeBusyText() const
|
||||
{
|
||||
NodeBusyText out;
|
||||
// The tracker reads OUR node's output; once that process is gone its last state means nothing.
|
||||
const bool ours = daemon_controller_ && daemon_controller_->isRunning();
|
||||
const bool rebuilding = ours && daemon_busy_.rebuilding();
|
||||
const bool replaying = ours && daemon_busy_.replaying() && !rebuilding;
|
||||
const bool rpcBusy = rpc_busy_gate_->busy();
|
||||
if (!rebuilding && !replaying && !rpcBusy) return out;
|
||||
out.busy = true;
|
||||
out.rebuilding = rebuilding;
|
||||
|
||||
// Running time: from the node's own (UTC) log timestamp when there is one, else since we first saw it.
|
||||
double secs = rpc_busy_gate_->busySeconds();
|
||||
if (rebuilding || replaying) {
|
||||
const std::int64_t startUtc = rebuilding ? daemon_busy_.rebuildStartUtc() : daemon_busy_.replayStartUtc();
|
||||
if (startUtc > 0) {
|
||||
secs = static_cast<double>(static_cast<std::int64_t>(std::time(nullptr)) - startUtc);
|
||||
} else if (busy_first_seen_ != std::chrono::steady_clock::time_point{}) {
|
||||
secs = std::chrono::duration<double>(std::chrono::steady_clock::now() - busy_first_seen_).count();
|
||||
}
|
||||
}
|
||||
out.elapsed = formatBusyDuration(secs);
|
||||
|
||||
char buf[512];
|
||||
std::string progress;
|
||||
if (rebuilding) {
|
||||
out.title = TR("node_busy_rebuild_title");
|
||||
snprintf(buf, sizeof(buf), TR("node_busy_rebuild_desc"), out.elapsed.c_str());
|
||||
out.detail = buf;
|
||||
// Phase 1 logs a line per 5000 blocks read; Phase 2 (extending the witnesses) logs nothing.
|
||||
const long done = daemon_busy_.readDone(), total = daemon_busy_.readTotal();
|
||||
const bool reading = total > 0 && done + 5000 < total;
|
||||
if (reading) snprintf(buf, sizeof(buf), TR("node_busy_rebuild_reading"), done, total);
|
||||
else snprintf(buf, sizeof(buf), "%s", TR("node_busy_rebuild_extending"));
|
||||
out.detail += " ";
|
||||
out.detail += buf;
|
||||
if (reading) progress = buf;
|
||||
} else if (replaying) {
|
||||
out.title = TR("node_busy_replay_title");
|
||||
snprintf(buf, sizeof(buf), TR("node_busy_replay_desc"), out.elapsed.c_str());
|
||||
out.detail = buf;
|
||||
} else {
|
||||
out.title = TR("node_busy_rpc_title");
|
||||
snprintf(buf, sizeof(buf), TR("node_busy_rpc_desc"), out.elapsed.c_str());
|
||||
out.detail = buf;
|
||||
}
|
||||
snprintf(buf, sizeof(buf), TR("node_busy_running"), out.elapsed.c_str());
|
||||
out.compact = buf;
|
||||
if (!progress.empty()) out.compact += " · " + progress;
|
||||
return out;
|
||||
}
|
||||
|
||||
void App::resetDaemonBusy()
|
||||
@@ -6142,17 +6233,35 @@ void App::renderDaemonStopConfirm()
|
||||
bool proceed = false;
|
||||
if (ImGui::BeginPopupModal("##DaemonStopConfirm", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||
// Honest about what each choice does now that the wallet never kills the node: stopping waits for
|
||||
// it to finish or abort the rebuild (Phase 1 aborts and is redone on the next start; Phase 2 can't
|
||||
// be interrupted). Keeping it running loses nothing, so it is the recommended, focused default.
|
||||
const NodeBusyText busy = nodeBusyText();
|
||||
std::string elapsed = busy.elapsed;
|
||||
if (!busy.busy && rpc_busy_since_ != std::chrono::steady_clock::time_point{})
|
||||
elapsed = formatBusyDuration(std::chrono::duration<double>(
|
||||
std::chrono::steady_clock::now() - rpc_busy_since_).count());
|
||||
const bool rebuilding = busy.rebuilding;
|
||||
if (Type().subtitle1()) ImGui::PushFont(Type().subtitle1());
|
||||
ImGui::TextUnformatted(TR("appx_node_rebuilding_witness_cache"));
|
||||
ImGui::TextUnformatted(TR(rebuilding ? "stopconfirm_rebuild_title" : "stopconfirm_busy_title"));
|
||||
if (Type().subtitle1()) ImGui::PopFont();
|
||||
ImGui::Spacing();
|
||||
char body[1024];
|
||||
snprintf(body, sizeof(body), TR(rebuilding ? "stopconfirm_rebuild_body" : "stopconfirm_busy_body"),
|
||||
elapsed.empty() ? "0:00" : elapsed.c_str());
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f);
|
||||
ImGui::TextUnformatted(TR("appx_stopping_node_discards_rebuild"));
|
||||
ImGui::TextUnformatted(body);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
if (TactileButton(TR("appx_keep_node_running_and_quit"), ImVec2(0, 0))) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Primary(), 210)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Primary(), 160)));
|
||||
const bool keep = TactileButton(TR("appx_keep_node_running_and_quit"), ImVec2(0, 0));
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::SetItemDefaultFocus();
|
||||
if (keep) {
|
||||
shutdown_keep_daemon_override_ = true;
|
||||
shutdown_confirmed_ = true;
|
||||
daemon_stop_confirm_open_ = false;
|
||||
@@ -6160,12 +6269,7 @@ void App::renderDaemonStopConfirm()
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 210)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Error()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 160)));
|
||||
const bool stopAnyway = TactileButton(TR("appx_stop_anyway_and_quit"), ImVec2(0, 0));
|
||||
ImGui::PopStyleColor(3);
|
||||
if (stopAnyway) {
|
||||
if (TactileButton(TR("stopconfirm_stop_wait"), ImVec2(0, 0))) {
|
||||
shutdown_confirmed_ = true;
|
||||
daemon_stop_confirm_open_ = false;
|
||||
proceed = true;
|
||||
|
||||
@@ -1077,6 +1077,7 @@ private:
|
||||
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
|
||||
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
|
||||
std::atomic<int> pending_delete_result_{-1};
|
||||
std::atomic<bool> pending_reinstall_node_busy_{false}; // reinstall gave up: the old node never exited
|
||||
|
||||
// Encryption state check timeout
|
||||
float encryption_check_timer_ = 0.0f;
|
||||
@@ -1186,6 +1187,17 @@ private:
|
||||
void abortRpcBusyCanary();
|
||||
void feedDaemonBusy(const std::string& chunk);
|
||||
void resetDaemonBusy();
|
||||
std::chrono::steady_clock::time_point busy_first_seen_{}; // when the tracker last turned busy (UI thread)
|
||||
// What to tell the user while the node is busy (overlay, banner, quit prompt). busy=false: it isn't.
|
||||
struct NodeBusyText {
|
||||
bool busy = false;
|
||||
bool rebuilding = false;
|
||||
std::string title; // e.g. "Node busy: rebuilding note witnesses — do not close"
|
||||
std::string detail; // running time, what it's doing, progress when the node reports any
|
||||
std::string elapsed; // "12:03"
|
||||
std::string compact; // one line for the banner: "Running for 12:03 · Reading blocks: 45438 / 62583"
|
||||
};
|
||||
NodeBusyText nodeBusyText() const;
|
||||
|
||||
// Export/Import state
|
||||
char export_result_[256] = {0}; // SECRET exported key — fixed buffer so it can be sodium_memzero'd
|
||||
@@ -1574,6 +1586,17 @@ private:
|
||||
// `reachableButBusy` is true when the probe connected but got no RPC reply (a timeout),
|
||||
// false when the daemon is merely launching (not bound yet). Returns the status title.
|
||||
std::string applyDaemonInitStatus(bool reachableButBusy);
|
||||
// The overlay text applyDaemonInitStatus last produced. It runs only on a connect failure, which a busy
|
||||
// node backs off to every 30s, so update() re-applies it once a second (the elapsed time and the
|
||||
// node's own progress tick) for as long as nothing else has replaced the status.
|
||||
std::string init_status_title_;
|
||||
bool init_status_busy_ = false;
|
||||
double status_refresh_at_ = 0.0;
|
||||
// The warm-up overlay text, from the node's last -28 message — unless its own log says it is rebuilding
|
||||
// witnesses: a startup rebuild runs under "Activating best chain...", which hides the step users must
|
||||
// not interrupt. update() re-applies it once a second too (see init_status_title_).
|
||||
void applyWarmupText(const std::string& rpcMessage);
|
||||
std::string warmup_rpc_message_;
|
||||
// Tear down a connection that died mid-session (daemon crash / restart / dropped
|
||||
// socket) so update()'s reconnect branch re-enters tryConnect(). Unlike onDisconnected
|
||||
// alone, this also rpc_->disconnect()s so rpc_->isConnected() actually flips to false.
|
||||
|
||||
+51
-31
@@ -533,16 +533,7 @@ void App::tryConnect()
|
||||
// the UI shows status instead of a blocking overlay.
|
||||
state_.warming_up = true;
|
||||
if (connect_stall_since_ <= 0.0) connect_stall_since_ = ImGui::GetTime(); // start the "taking too long" clock
|
||||
auto wt = translateWarmup(warmupStatus);
|
||||
state_.warmup_status = wt.title;
|
||||
state_.warmup_description = wt.description;
|
||||
// Append current block height from daemon output
|
||||
if (daemon_controller_) {
|
||||
int h = daemon_controller_->lastBlockHeight();
|
||||
if (h > 0)
|
||||
state_.warmup_status += " (Block " + std::to_string(h) + ")";
|
||||
}
|
||||
connection_status_ = state_.warmup_status;
|
||||
applyWarmupText(warmupStatus);
|
||||
}
|
||||
} else {
|
||||
// HTTP 401 = authentication failure. The daemon is running
|
||||
@@ -594,7 +585,9 @@ void App::tryConnect()
|
||||
// a connect refusal means it hasn't bound the RPC port yet. Either way, show a
|
||||
// clear "node initializing" overlay (status + phase + block height from the
|
||||
// daemon's own console output) instead of a bare technical error.
|
||||
const bool reachableButBusy = rpc::isRpcTimeout(connectKind);
|
||||
// Reached the node but no answer (reply timeout / full queue / held back): up but busy.
|
||||
// A refusal or connect timeout means it hasn't bound (or has already unbound) RPC.
|
||||
const bool reachableButBusy = rpc::rpcKindMeansBusy(connectKind);
|
||||
applyDaemonInitStatus(reachableButBusy);
|
||||
const float retryIn = connect_backoff_.next(connectKind);
|
||||
VERBOSE_LOGF("[connect #%d] RPC connection failed (%s) — daemon still starting, retry in %.0fs\n",
|
||||
@@ -605,7 +598,7 @@ void App::tryConnect()
|
||||
// An external daemon is on the RPC port but not answering. A timeout means it's
|
||||
// up and busy initializing; surface that as the init overlay (we can't read its
|
||||
// console since we didn't launch it, so no phase line — just a clear message).
|
||||
if (rpc::isRpcTimeout(connectKind)) {
|
||||
if (rpc::rpcKindMeansBusy(connectKind)) {
|
||||
applyDaemonInitStatus(/*reachableButBusy=*/true);
|
||||
} else if (!connectErr.empty()) {
|
||||
char buf[256]; snprintf(buf, sizeof(buf), TR("sb_connecting_err"), connectErr.c_str());
|
||||
@@ -699,6 +692,7 @@ void App::onConnected()
|
||||
wallet_switch_phase_.store(static_cast<int>(WalletSwitchPhase::None));
|
||||
}
|
||||
state_.daemon_initializing = false; // RPC is answering now; clear the "initializing" overlay
|
||||
init_status_title_.clear();
|
||||
daemon_wait_attempts_ = 0; // re-arm the port-busy / start-failure notifications
|
||||
connect_stall_since_ = 0.0; // connected — clear the "taking too long" clock
|
||||
// A repair's rescan finished and connected — retire the recovery session so its crash-toast/error-card
|
||||
@@ -870,19 +864,33 @@ std::string App::applyDaemonInitStatus(bool reachableButBusy)
|
||||
}
|
||||
}
|
||||
|
||||
// Most specific first: the node's own log says it is rebuilding witnesses or replaying blocks (then
|
||||
// "Launching dragonxd…" over a node that has been busy for an hour invited closing it), else an init
|
||||
// phase line, else "reachable but busy", else not bound yet.
|
||||
const auto kind = daemon::chooseDaemonStatus(!phaseLine.empty(), reachableButBusy, daemon_busy_);
|
||||
std::string busyTitle, busyDetail;
|
||||
WarmupText wt;
|
||||
if (!phaseLine.empty()) {
|
||||
switch (kind) {
|
||||
case daemon::DaemonStatusKind::Rebuilding:
|
||||
case daemon::DaemonStatusKind::Replaying: {
|
||||
const NodeBusyText busy = nodeBusyText();
|
||||
busyTitle = busy.title;
|
||||
busyDetail = busy.detail;
|
||||
wt = {busyTitle.c_str(), busyDetail.c_str()};
|
||||
break;
|
||||
}
|
||||
case daemon::DaemonStatusKind::InitPhase:
|
||||
wt = translateWarmup(phaseLine);
|
||||
} else if (reachableButBusy) {
|
||||
// The probe connected but got no RPC reply within the timeout: the node is up but busy
|
||||
// initializing (it isn't printing a recognizable phase, or we didn't launch it).
|
||||
wt = {"Starting DragonX node…",
|
||||
"The node is reachable but still initializing and isn't answering yet. "
|
||||
"This is normal after an update or on first launch — it can take a few minutes."};
|
||||
} else {
|
||||
break;
|
||||
case daemon::DaemonStatusKind::Busy:
|
||||
// The probe reached the node but got no answer (a reply timeout or a full queue): it is up but
|
||||
// too busy to answer (it isn't printing a recognizable phase, or we didn't launch it).
|
||||
wt = {TR("node_starting_title"), TR("node_starting_busy_desc")};
|
||||
break;
|
||||
case daemon::DaemonStatusKind::Launching:
|
||||
// The daemon is launching but hasn't bound its RPC port yet.
|
||||
wt = {"Starting DragonX node…",
|
||||
"Launching dragonxd and waiting for it to come online…"};
|
||||
wt = {TR("node_starting_title"), TR("node_starting_launch_desc")};
|
||||
break;
|
||||
}
|
||||
|
||||
std::string title = wt.title;
|
||||
@@ -892,9 +900,29 @@ std::string App::applyDaemonInitStatus(bool reachableButBusy)
|
||||
state_.warmup_status = title;
|
||||
state_.warmup_description = wt.description ? wt.description : "";
|
||||
connection_status_ = title;
|
||||
init_status_title_ = title;
|
||||
init_status_busy_ = reachableButBusy;
|
||||
return title;
|
||||
}
|
||||
|
||||
void App::applyWarmupText(const std::string& rpcMessage)
|
||||
{
|
||||
warmup_rpc_message_ = rpcMessage;
|
||||
if (daemon_busy_.rebuilding()) {
|
||||
const NodeBusyText busy = nodeBusyText();
|
||||
state_.warmup_status = busy.title;
|
||||
state_.warmup_description = busy.detail;
|
||||
} else {
|
||||
const WarmupText wt = translateWarmup(rpcMessage);
|
||||
state_.warmup_status = wt.title;
|
||||
state_.warmup_description = wt.description;
|
||||
}
|
||||
// Append current block height from daemon output
|
||||
const int h = daemon_controller_ ? daemon_controller_->lastBlockHeight() : -1;
|
||||
if (h > 0) state_.warmup_status += " (Block " + std::to_string(h) + ")";
|
||||
connection_status_ = state_.warmup_status;
|
||||
}
|
||||
|
||||
void App::handleLostConnection(const std::string& reason)
|
||||
{
|
||||
DEBUG_LOGF("[Connection] %s — tearing down for reconnect\n", reason.c_str());
|
||||
@@ -1799,15 +1827,7 @@ void App::refreshCoreData()
|
||||
refreshData();
|
||||
} else {
|
||||
// Still warming up — update status
|
||||
auto wt = translateWarmup(result.errorMessage);
|
||||
state_.warmup_status = wt.title;
|
||||
state_.warmup_description = wt.description;
|
||||
if (daemon_controller_) {
|
||||
int h = daemon_controller_->lastBlockHeight();
|
||||
if (h > 0)
|
||||
state_.warmup_status += " (Block " + std::to_string(h) + ")";
|
||||
}
|
||||
connection_status_ = state_.warmup_status;
|
||||
applyWarmupText(result.errorMessage);
|
||||
VERBOSE_LOGF("[warmup] Still warming up: %s\n", result.errorMessage.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -141,5 +141,24 @@ inline bool shouldConfirmStop(bool stopsDaemon, const DaemonBusyTracker& tracker
|
||||
return stopsDaemon && (tracker.rebuilding() || rpcBusyWhileAlive);
|
||||
}
|
||||
|
||||
// What the "node is starting / not answering" overlay should say, most specific first.
|
||||
enum class DaemonStatusKind {
|
||||
Rebuilding, // its own log says it's rebuilding note witnesses
|
||||
Replaying, // its own log says it's catching up on blocks it saved before (startup replay)
|
||||
InitPhase, // a recognisable init phase line (loading the block index, verifying, ...)
|
||||
Busy, // it answers the connection but not the request (reply timeout / queue full)
|
||||
Launching, // nothing bound to the RPC port yet
|
||||
};
|
||||
|
||||
inline DaemonStatusKind chooseDaemonStatus(bool hasInitPhaseLine, bool reachableButBusy,
|
||||
const DaemonBusyTracker& tracker)
|
||||
{
|
||||
if (tracker.rebuilding()) return DaemonStatusKind::Rebuilding;
|
||||
if (tracker.replaying()) return DaemonStatusKind::Replaying;
|
||||
if (hasInitPhaseLine) return DaemonStatusKind::InitPhase;
|
||||
if (reachableButBusy) return DaemonStatusKind::Busy;
|
||||
return DaemonStatusKind::Launching;
|
||||
}
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -32,6 +32,7 @@ enum class NodeBannerReason {
|
||||
DaemonCrashed, // the embedded daemon crashed repeatedly and auto-restart stopped
|
||||
LiteOpenFailed, // lite build: the wallet failed to open
|
||||
NoPeers, // connected to the daemon but no peer-derived tip (0 peers) — stalled, stale
|
||||
NodeBusy, // the node is up but busy (rebuilding witnesses / catching up / not answering)
|
||||
};
|
||||
|
||||
struct NodeBannerState {
|
||||
@@ -64,6 +65,12 @@ struct NodeBannerInputs {
|
||||
bool daemon_running = false;
|
||||
int daemon_crash_count = 0;
|
||||
|
||||
// The node is running but busy with something it can't be interrupted out of (a witness rebuild, the
|
||||
// startup replay) or too busy to answer RPC. Wallet data may be stale; a reconnect would only add
|
||||
// requests, so the banner offers no action. node_busy_detail is the live, translated description.
|
||||
bool node_busy = false;
|
||||
std::string node_busy_detail;
|
||||
|
||||
std::string connection_status; // human-readable status line (already translated)
|
||||
std::string daemon_last_error; // DaemonController::lastError() (may be empty)
|
||||
std::string lite_open_error; // lite: last wallet-open failure reason
|
||||
@@ -89,6 +96,18 @@ inline NodeBannerState evaluateNodeStatusBanner(const NodeBannerInputs& in) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Full node, busy. Before the startup-phase returns below: the overlay sets daemon_initializing while
|
||||
// the node is busy too, and the banner must still say why the wallet isn't updating. No action: a
|
||||
// "Reconnect" would only queue more requests behind the busy node.
|
||||
if (in.node_busy) {
|
||||
s.show = true;
|
||||
s.severity = NodeBannerSeverity::Warning;
|
||||
s.reason = NodeBannerReason::NodeBusy;
|
||||
s.action = NodeBannerAction::None;
|
||||
s.detail = in.node_busy_detail;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Full node. In an expected startup phase → the loading/warmup overlay owns the screen, so no
|
||||
// banner. An active connect attempt likewise shows progress, not an error — don't flicker a
|
||||
// banner over it. (These return even if `connected` has flipped early, matching prior behavior.)
|
||||
|
||||
+23
-12
@@ -582,14 +582,9 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["incorrect_passphrase"] = "Incorrect passphrase";
|
||||
strings_["pin_not_set"] = "PIN not set. Use passphrase to unlock.";
|
||||
strings_["restarting_after_encryption"] = "Restarting daemon after encryption...";
|
||||
strings_["force_quit"] = "Force Quit";
|
||||
strings_["force_quit_warning"] = "This will immediately kill the daemon without a clean shutdown. May require a blockchain resync.";
|
||||
strings_["force_quit_confirm_title"] = "Force Quit?";
|
||||
strings_["force_quit_confirm_msg"] = "This will immediately kill the daemon without a clean shutdown.\nThis may corrupt the blockchain index and require a resync.";
|
||||
strings_["force_quit_yes"] = "Force Quit";
|
||||
// Stopping the node on quit: deliver stop, wait, never kill unasked (translations: S10 batch)
|
||||
// Stopping the node on quit: deliver stop, wait, never kill unasked
|
||||
strings_["shutdown_leave_running"] = "Close wallet — leave node running";
|
||||
strings_["shutdown_leave_running_tip"] = "Closes the wallet now. The node keeps running and finishes shutting down on its own; nothing is lost.";
|
||||
strings_["shutdown_leave_running_tip"] = "Closes the wallet now. The node keeps running on its own and finishes what it is doing; nothing is lost.";
|
||||
strings_["shutdown_kill_node"] = "Kill node (unsafe)";
|
||||
strings_["shutdown_kill_node_tip"] = "Ends the node at once. Anything it has not saved is lost, and its next start replays the blocks since its last save.";
|
||||
strings_["shutdown_kill_confirm_title"] = "Kill the node?";
|
||||
@@ -600,6 +595,27 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["shutdown_stop_delivered"] = "The node is shutting down and saving its state";
|
||||
strings_["shutdown_stop_cpu_suffix"] = " · CPU %d%%";
|
||||
strings_["shutdown_stop_idle"] = "No CPU or disk activity for %d min — the node may be stuck";
|
||||
// The node is busy (S9): overlay, banner, status bar
|
||||
strings_["node_busy_rebuild_title"] = "Node busy: rebuilding note witnesses";
|
||||
strings_["node_busy_running"] = "Running for %s";
|
||||
strings_["node_busy_rebuild_desc"] = "Running for %s. Large wallets can take about an hour, and the node can't answer the wallet until it finishes.";
|
||||
strings_["node_busy_rebuild_reading"] = "Reading blocks: %ld / %ld";
|
||||
strings_["node_busy_rebuild_extending"] = "Extending witnesses (the node reports no progress for this step).";
|
||||
strings_["node_busy_replay_title"] = "Node busy: catching up on blocks";
|
||||
strings_["node_busy_replay_desc"] = "Running for %s. The node is re-applying blocks it received before it last stopped.";
|
||||
strings_["node_busy_rpc_title"] = "Node busy: not answering yet";
|
||||
strings_["node_busy_rpc_desc"] = "The node is running but has been too busy to answer the wallet for %s, usually because of a long operation such as a witness rebuild.";
|
||||
strings_["node_starting_title"] = "Starting DragonX node…";
|
||||
strings_["node_starting_busy_desc"] = "The node is reachable but busy and isn't answering yet. This is normal after an update or on first launch; it can take a few minutes.";
|
||||
strings_["node_starting_launch_desc"] = "Launching dragonxd and waiting for it to come online…";
|
||||
strings_["sb_node_busy"] = "Node busy";
|
||||
// Quit while the node is busy (S10)
|
||||
strings_["stopconfirm_rebuild_title"] = "The node is rebuilding note witnesses";
|
||||
strings_["stopconfirm_rebuild_body"] = "It has been running for %s, and large wallets can take about an hour. Stopping the node now either waits for the rebuild to finish or makes its next start redo the rebuild and any catch-up.\n\nRecommended: close the wallet and leave the node running. It finishes on its own.";
|
||||
strings_["stopconfirm_busy_title"] = "The node is busy";
|
||||
strings_["stopconfirm_busy_body"] = "It has been too busy to answer the wallet for %s, probably because of a long operation such as a witness rebuild. Stopping it now can make its next start redo that work.\n\nRecommended: close the wallet and leave the node running. It finishes on its own.";
|
||||
strings_["stopconfirm_stop_wait"] = "Stop node & wait";
|
||||
strings_["appx_daemon_reinstall_node_busy"] = "The node is still running, so the daemon was not reinstalled. Try again once it has stopped.";
|
||||
strings_["reduce_motion"] = "Reduce Motion";
|
||||
strings_["tt_reduce_motion"] = "Disable animated transitions and balance lerp for accessibility";
|
||||
strings_["ago"] = "ago";
|
||||
@@ -2638,10 +2654,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["appx_restoring_your_wallet"] = "Restoring your wallet…";
|
||||
strings_["appx_recovery_phrase_word_count"] = "Recovery phrase should be 24 words — you have %d.";
|
||||
strings_["appx_could_not_start_restore"] = "Could not start restore";
|
||||
strings_["appx_node_rebuilding_witness_cache"] = "Node is rebuilding its witness cache";
|
||||
strings_["appx_stopping_node_discards_rebuild"] = "Stopping the node now discards the in-progress rebuild and restarts it (several minutes) the next time you open the wallet. You can keep the node running instead.";
|
||||
strings_["appx_keep_node_running_and_quit"] = "Keep node running & quit";
|
||||
strings_["appx_stop_anyway_and_quit"] = "Stop anyway & quit";
|
||||
strings_["appx_cancel"] = "Cancel";
|
||||
strings_["appx_last_used_wallet_not_found_prefix"] = "Your last-used wallet file (";
|
||||
strings_["appx_last_used_wallet_not_found_suffix"] = ") was not found — opened the default wallet instead. If you moved it, restore it and switch back from the wallet list.";
|
||||
@@ -2671,8 +2684,6 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["appx_shutdown_complete"] = "Shutdown complete";
|
||||
strings_["appx_n_seconds"] = "%d seconds";
|
||||
strings_["appx_n_min_n_sec"] = "%d min %d sec";
|
||||
strings_["appx_still_status_prefix"] = "Still \"";
|
||||
strings_["appx_still_status_suffix"] = "\" — force quitting now may corrupt chain data.";
|
||||
strings_["appx_rebuilding_witness_cache_blocks_left"] = "Rebuilding witness cache %.0f%% — %d blocks left";
|
||||
strings_["appx_rebuilding_witness_cache_pct"] = "Rebuilding witness cache %.0f%%";
|
||||
strings_["appx_setting_initial_sapling_witnesses"] = "Setting initial Sapling witnesses %.0f%%";
|
||||
|
||||
Reference in New Issue
Block a user