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:
2026-10-02 10:26:32 -05:00
co-authored by Claude Opus 5.5
parent 0f25c67600
commit 1215ded1a0
17 changed files with 822 additions and 133 deletions
+51 -31
View File
@@ -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());
}
};