feat(network): give the "no peers" banner an honest detail and a Restart action
The no-peers banner headline ("Connected, but no network peers") was followed
by a detail line that just repeated connection_status ("Connected"/"Peers: 0"),
reading as a contradiction, and it offered no action at the exact moment the
wallet looks synced but is silently stalled at a stale height.
- NoPeers now clears the passthrough detail; App renders a translated,
actionable hint (node_banner_no_peers_detail) for this reason instead.
- When the embedded daemon is the node, offer a Restart button (re-triggers
peer discovery via the -addnode seeds); an external node stays action-less
since it is the user's own to restart.
- Update + extend the evaluator unit tests (external = no action/empty detail;
embedded = RestartNode).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+9
-2
@@ -2705,6 +2705,13 @@ void App::renderNodeStatusBanner()
|
||||
if (banner.action == ui::NodeBannerAction::Reconnect) actionLabel = TR("node_banner_reconnect");
|
||||
else if (banner.action == ui::NodeBannerAction::RestartNode) actionLabel = TR("node_banner_restart");
|
||||
|
||||
// The NoPeers case deliberately carries no passthrough detail (connection_status is just
|
||||
// "Connected" there, which reads as a contradiction under the "no peers" headline). Show a
|
||||
// translated, actionable hint instead; every other reason keeps its live status/error text.
|
||||
const std::string detailText = (banner.reason == ui::NodeBannerReason::NoPeers)
|
||||
? std::string(TR("node_banner_no_peers_detail"))
|
||||
: banner.detail;
|
||||
|
||||
const float padX = ui::Layout::spacingLg();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(bgCol));
|
||||
@@ -2741,7 +2748,7 @@ void App::renderNodeStatusBanner()
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Detail (dim) on the same row, clipped with an ellipsis so it can't push the button off-screen.
|
||||
if (!banner.detail.empty()) {
|
||||
if (!detailText.empty()) {
|
||||
ImGui::SameLine(0.0f, ui::Layout::spacingSm());
|
||||
ImGui::SetCursorPosY(txtCy);
|
||||
const float budget = winW - ImGui::GetCursorPosX() - actionReserve;
|
||||
@@ -2752,7 +2759,7 @@ void App::renderNodeStatusBanner()
|
||||
static util::textfit::EllipsisFitCache s_detailFit; // UI thread only (render path)
|
||||
ImFont* detailFont = ImGui::GetFont();
|
||||
const std::string& shown = s_detailFit.get(
|
||||
banner.detail, budget, detailFont->FontId, ImGui::GetFontSize(),
|
||||
detailText, budget, detailFont->FontId, ImGui::GetFontSize(),
|
||||
detailFont->CurrentRasterizerDensity,
|
||||
[&](const std::string& detail) {
|
||||
const std::string prefix = "\xC2\xB7 "; // "· "
|
||||
|
||||
@@ -116,14 +116,20 @@ inline NodeBannerState evaluateNodeStatusBanner(const NodeBannerInputs& in) {
|
||||
|
||||
// Connected to the daemon (RPC works) but with no peer-derived tip → the node has no peers and
|
||||
// cannot learn about new blocks; it silently stalls at a stale height while everything looks
|
||||
// synced. Surface it as a recoverable warning (reconnect won't help, so no action button).
|
||||
// synced. Surface it as a recoverable warning. A plain reconnect won't help, but restarting the
|
||||
// embedded daemon re-triggers peer discovery (it re-reads the -addnode seeds), so offer that when
|
||||
// we own the node; an external node is the user's own to restart, so no button there. The detail
|
||||
// is left empty: here connection_status is just "Connected", which would contradict the
|
||||
// "no peers" headline — App renders a translated, actionable hint for this reason instead.
|
||||
if (in.connected) {
|
||||
if (!in.tip_known && in.blocks > 0) {
|
||||
s.show = true;
|
||||
s.severity = NodeBannerSeverity::Warning;
|
||||
s.reason = NodeBannerReason::NoPeers;
|
||||
s.action = NodeBannerAction::None;
|
||||
s.detail = in.connection_status;
|
||||
s.action = (in.using_embedded_daemon && in.has_daemon_controller)
|
||||
? NodeBannerAction::RestartNode
|
||||
: NodeBannerAction::None;
|
||||
s.detail.clear();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -1543,6 +1543,7 @@ void I18n::loadBuiltinEnglish()
|
||||
// Persistent node-status banner (App::renderNodeStatusBanner).
|
||||
strings_["node_banner_offline_title"] = "Not connected to the DragonX node";
|
||||
strings_["node_banner_no_peers_title"] = "Connected, but no network peers — the node can't sync";
|
||||
strings_["node_banner_no_peers_detail"] = "Check your internet connection and firewall, then restart the node to find peers.";
|
||||
strings_["node_banner_crashed_title"] = "The node stopped unexpectedly";
|
||||
strings_["node_banner_lite_open_failed"] = "Couldn't open your wallet";
|
||||
strings_["node_banner_reconnect"] = "Reconnect";
|
||||
|
||||
+14
-1
@@ -3535,6 +3535,9 @@ void testNodeStatusBanner()
|
||||
EXPECT_TRUE(!evaluateNodeStatusBanner(in).show);
|
||||
}
|
||||
// Connected but no peer-derived tip (0 peers) once chain info is in → amber "no peers" banner.
|
||||
// An external node (no embedded controller) gets no action button, and the detail is cleared:
|
||||
// connection_status ("Peers: 0"/"Connected") would contradict the headline, so App renders a
|
||||
// translated actionable hint for this reason instead.
|
||||
{
|
||||
NodeBannerInputs in;
|
||||
in.connected = true; in.tip_known = false; in.blocks = 3262059;
|
||||
@@ -3544,7 +3547,17 @@ void testNodeStatusBanner()
|
||||
EXPECT_TRUE(s.severity == NodeBannerSeverity::Warning);
|
||||
EXPECT_TRUE(s.reason == NodeBannerReason::NoPeers);
|
||||
EXPECT_TRUE(s.action == NodeBannerAction::None);
|
||||
EXPECT_EQ(s.detail, std::string("Peers: 0"));
|
||||
EXPECT_TRUE(s.detail.empty());
|
||||
}
|
||||
// ...but with the embedded daemon running, offer a Restart (re-triggers peer discovery).
|
||||
{
|
||||
NodeBannerInputs in;
|
||||
in.connected = true; in.tip_known = false; in.blocks = 3262059;
|
||||
in.using_embedded_daemon = true; in.has_daemon_controller = true; in.daemon_running = true;
|
||||
NodeBannerState s = evaluateNodeStatusBanner(in);
|
||||
EXPECT_TRUE(s.reason == NodeBannerReason::NoPeers);
|
||||
EXPECT_TRUE(s.action == NodeBannerAction::RestartNode);
|
||||
EXPECT_TRUE(s.detail.empty());
|
||||
}
|
||||
// Connected, tip unknown, but no chain info yet (blocks==0) → no banner (avoid startup flicker).
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user