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:
2026-10-17 16:33:17 -05:00
co-authored by Claude Opus 4.8
parent a99f71a0cd
commit 750771ebfb
4 changed files with 33 additions and 6 deletions
+9 -2
View File
@@ -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 "; // "· "