diff --git a/src/app.cpp b/src/app.cpp index 2c0f72d..6dfdbf3 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -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 "; // "· " diff --git a/src/ui/node_status_banner.h b/src/ui/node_status_banner.h index 5c644ca..ac23261 100644 --- a/src/ui/node_status_banner.h +++ b/src/ui/node_status_banner.h @@ -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; } diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index eab6dde..13333b7 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -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"; diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index c9a68da..28dfaad 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -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). {