feat(diagnostics): persistent node/RPC error banner at top of content (Foundation QoL)
A persistent horizontal strip now appears at the top of the content column whenever the
wallet can't reach its node — unlike the transient toasts it stays up for as long as the
fault persists, so an offline wallet is never silently mistaken for a working one.
The show/severity/action decision is a pure, unit-tested function
(ui/node_status_banner.h::evaluateNodeStatusBanner) fed a state snapshot by the new
App::renderNodeStatusBanner(). Three cases:
- full-node offline -> amber, "Reconnect" (App::tryConnect)
- embedded daemon crashed
& auto-restart gave up -> red, "Restart node" (App::restartDaemon)
- lite wallet open failed -> red, message-only
Suppressed during the wizard / wallet-switch / daemon-restart / screenshot-sweep / shutdown,
and while an expected startup phase (warmup / init / connect-in-progress) already owns the
screen. Banner height lives in res/themes/ui.toml (banners.node-status); colours come from the
material semantic palette; the detail text is ellipsis-clipped so it can't push the action
button off-screen. Drawn before the content edge-fade vertex capture so it stays fully opaque.
New i18n keys (node_banner_*). Build-clean both variants; ctest 1/1 (adds testNodeStatusBanner).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
#include "ui/windows/mining_benchmark.h"
|
||||
#include "ui/windows/mining_pool_panel.h"
|
||||
#include "ui/windows/mining_tab_helpers.h"
|
||||
#include "ui/node_status_banner.h"
|
||||
#include "util/address_validation.h"
|
||||
#include "util/amount_format.h"
|
||||
#include "util/payment_uri.h"
|
||||
@@ -2547,6 +2548,81 @@ void testConsoleSecretRedaction()
|
||||
EXPECT_EQ(RedactConsoleCommand("getwalletinfo"), std::string("getwalletinfo"));
|
||||
}
|
||||
|
||||
void testNodeStatusBanner()
|
||||
{
|
||||
using namespace dragonx::ui;
|
||||
|
||||
// Connected full node → no banner.
|
||||
{
|
||||
NodeBannerInputs in; in.connected = true;
|
||||
EXPECT_TRUE(!evaluateNodeStatusBanner(in).show);
|
||||
}
|
||||
// Expected startup phases own the screen (loading/warmup overlay) → no banner.
|
||||
{
|
||||
NodeBannerInputs in; in.warming_up = true;
|
||||
EXPECT_TRUE(!evaluateNodeStatusBanner(in).show);
|
||||
NodeBannerInputs in2; in2.daemon_initializing = true;
|
||||
EXPECT_TRUE(!evaluateNodeStatusBanner(in2).show);
|
||||
NodeBannerInputs in3; in3.connection_in_progress = true;
|
||||
EXPECT_TRUE(!evaluateNodeStatusBanner(in3).show);
|
||||
}
|
||||
// Genuinely offline full node → amber, reconnect offered, detail passed through.
|
||||
{
|
||||
NodeBannerInputs in;
|
||||
in.connection_status = "Lost connection to daemon";
|
||||
NodeBannerState s = evaluateNodeStatusBanner(in);
|
||||
EXPECT_TRUE(s.show);
|
||||
EXPECT_TRUE(s.severity == NodeBannerSeverity::Warning);
|
||||
EXPECT_TRUE(s.reason == NodeBannerReason::FullNodeOffline);
|
||||
EXPECT_TRUE(s.action == NodeBannerAction::Reconnect);
|
||||
EXPECT_EQ(s.detail, std::string("Lost connection to daemon"));
|
||||
}
|
||||
// Embedded daemon crashed and auto-restart gave up → red, restart offered, lastError preferred.
|
||||
{
|
||||
NodeBannerInputs in;
|
||||
in.using_embedded_daemon = true;
|
||||
in.has_daemon_controller = true;
|
||||
in.daemon_running = false;
|
||||
in.daemon_crash_count = kNodeBannerCrashGiveUpCount;
|
||||
in.daemon_last_error = "exit code 134";
|
||||
in.connection_status = "Daemon crashed 3 times";
|
||||
NodeBannerState s = evaluateNodeStatusBanner(in);
|
||||
EXPECT_TRUE(s.show);
|
||||
EXPECT_TRUE(s.severity == NodeBannerSeverity::Error);
|
||||
EXPECT_TRUE(s.reason == NodeBannerReason::DaemonCrashed);
|
||||
EXPECT_TRUE(s.action == NodeBannerAction::RestartNode);
|
||||
EXPECT_EQ(s.detail, std::string("exit code 134"));
|
||||
}
|
||||
// Below the give-up threshold it's still just an offline/reconnect banner, not the crash one.
|
||||
{
|
||||
NodeBannerInputs in;
|
||||
in.using_embedded_daemon = true;
|
||||
in.has_daemon_controller = true;
|
||||
in.daemon_running = false;
|
||||
in.daemon_crash_count = kNodeBannerCrashGiveUpCount - 1;
|
||||
NodeBannerState s = evaluateNodeStatusBanner(in);
|
||||
EXPECT_TRUE(s.show);
|
||||
EXPECT_TRUE(s.reason == NodeBannerReason::FullNodeOffline);
|
||||
EXPECT_TRUE(s.action == NodeBannerAction::Reconnect);
|
||||
}
|
||||
// Lite: an open failure shows a red, action-less banner; no failure → nothing.
|
||||
{
|
||||
NodeBannerInputs in; in.lite = true; in.connected = false;
|
||||
in.lite_open_error = "wallet.dat is corrupt";
|
||||
NodeBannerState s = evaluateNodeStatusBanner(in);
|
||||
EXPECT_TRUE(s.show);
|
||||
EXPECT_TRUE(s.severity == NodeBannerSeverity::Error);
|
||||
EXPECT_TRUE(s.reason == NodeBannerReason::LiteOpenFailed);
|
||||
EXPECT_TRUE(s.action == NodeBannerAction::None);
|
||||
EXPECT_EQ(s.detail, std::string("wallet.dat is corrupt"));
|
||||
|
||||
NodeBannerInputs clean; clean.lite = true; clean.connected = false; // no error yet
|
||||
EXPECT_TRUE(!evaluateNodeStatusBanner(clean).show);
|
||||
NodeBannerInputs open; open.lite = true; open.connected = true;
|
||||
EXPECT_TRUE(!evaluateNodeStatusBanner(open).show);
|
||||
}
|
||||
}
|
||||
|
||||
void testLoggerFileSink()
|
||||
{
|
||||
using dragonx::util::Logger;
|
||||
@@ -6934,6 +7010,7 @@ int main()
|
||||
testIsLocalHost();
|
||||
testAllowsPlaintextRemote();
|
||||
testConsoleSecretRedaction();
|
||||
testNodeStatusBanner();
|
||||
testLoggerFileSink();
|
||||
testDaemonLifecycleExecution();
|
||||
testDaemonLifecycleAdapters();
|
||||
|
||||
Reference in New Issue
Block a user