fix(startup): surface a "taking too long" notice when the daemon won't come up

F3: the daemon connect loop retried forever with only an animated spinner when the
daemon was reachable-but-never-ready (stuck in RPC warmup / -28, or an external daemon
that never finishes init) -- no error, no guidance, no escape. It now stamps
connect_stall_since_ the moment the daemon first goes "reachable but not ready" (the
warmup branch + applyDaemonInitStatus) and clears it on connect / disconnect /
warmup-complete. A pure, unit-testable util::connectHasStalled() helper (new
util/connect_stall.h, 45s default from ui.toml [screens.loading].stall-timeout-sec)
drives a "Taking longer than expected" notice in renderLoadingOverlay(): a title, a
reassuring body with elapsed seconds, and a full-node hint to Settings > Restart Daemon
or the Console. The background retry keeps running underneath, so the notice self-clears
the instant it connects. Guarded off while the daemon is in State::Error (that case is
owned by the existing crash-count hint).

The overlay is a pure draw-list layer with no interactive widgets, so this follows the
existing crash-hint idiom (guidance text, not injected buttons); the stalled state is
computed locally in the overlay, so the only new App member is connect_stall_since_.

Adds testConnectHasStalled to test_phase4.cpp and three i18n keys to i18n.cpp (English
source of truth; the res/lang/*.json back-fill is deferred to a single
add_missing_translations.py run at the end of the batch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 11:33:26 -05:00
parent 2675b8ab93
commit eb69e491b9
8 changed files with 113 additions and 2 deletions

View File

@@ -396,6 +396,7 @@ void App::tryConnect()
// fail until warmup completes. Set the warmup state so
// 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;
@@ -537,6 +538,7 @@ void App::onConnected()
}
state_.daemon_initializing = false; // RPC is answering now; clear the "initializing" overlay
daemon_wait_attempts_ = 0; // re-arm the port-busy / start-failure notifications
connect_stall_since_ = 0.0; // connected — clear the "taking too long" clock
daemon_start_error_shown_ = false;
daemon_last_seen_crashes_ = 0; // (onConnected resets the daemon's crash count too)
connection_status_ = TR("connected");
@@ -617,6 +619,7 @@ void App::onDisconnected(const std::string& reason)
state_.connected = false;
state_.warming_up = false;
state_.warmup_status.clear();
connect_stall_since_ = 0.0; // reset the "taking too long" clock (App member, untouched by state_.clear())
state_.clear();
connection_status_ = reason;
@@ -671,6 +674,7 @@ void App::onDisconnected(const std::string& reason)
std::string App::applyDaemonInitStatus(bool reachableButBusy)
{
state_.daemon_initializing = true;
if (connect_stall_since_ <= 0.0) connect_stall_since_ = ImGui::GetTime(); // start the "taking too long" clock
// Find the most recent console line that names an init phase, so we can tell the user exactly
// what the node is doing (loading the block index, verifying, activating best chain, …).
@@ -1499,6 +1503,7 @@ void App::refreshCoreData()
state_.warming_up = false;
state_.warmup_status.clear();
state_.warmup_description.clear();
connect_stall_since_ = 0.0; // warmup finished — clear the "taking too long" clock
connection_status_ = TR("connected");
VERBOSE_LOGF("[warmup] Daemon ready, warmup complete\n");