feat(diagnostics): refresh-staleness badge on the Total Balance card (W6-2)

When the wallet is connected but the balance has quietly stopped refreshing — a busy
daemon can fail z_gettotalbalance without dropping the whole connection (only *both*
core RPCs failing 3x triggers a disconnect) — the old number sits on screen while the
node-status banner stays hidden. The Total Balance card now shows a small pill on its
status line ("Updated 2m ago", amber, escalating to red past 3 min) so the stale value
isn't silently trusted; hovering explains it and points at the node connection.

No refresh-path changes: WalletState::last_balance_update is already stamped only on a
successful fetch (network_refresh_service.cpp), so the badge reads it and computes age
against the same std::time clock via util::formatTimeAgoShort. The decision is a pure,
unit-tested helper (ui/staleness_badge.h::evaluateStalenessBadge, 45s/180s thresholds)
gated on connected so it never contradicts the banner.

Closes P2 (5/5). Build-clean; ctest 1/1 (adds testStalenessBadge).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 20:48:56 -05:00
parent e779ded2e8
commit c7c3440a7b
5 changed files with 109 additions and 2 deletions

View File

@@ -33,6 +33,7 @@
#include "ui/windows/mining_pool_panel.h"
#include "ui/windows/mining_tab_helpers.h"
#include "ui/node_status_banner.h"
#include "ui/staleness_badge.h"
#include "util/address_validation.h"
#include "util/amount_format.h"
#include "util/payment_uri.h"
@@ -2623,6 +2624,37 @@ void testNodeStatusBanner()
}
}
void testStalenessBadge()
{
using namespace dragonx::ui;
const int64_t now = 1'000'000;
// Disconnected → banner's job, never a badge.
EXPECT_TRUE(!evaluateStalenessBadge(now - 999, now, /*connected=*/false).show);
// Never updated this session (0 stamp, e.g. fresh start / reset on disconnect) → nothing.
EXPECT_TRUE(!evaluateStalenessBadge(0, now, true).show);
// Fresh (just under the threshold) → no badge.
EXPECT_TRUE(!evaluateStalenessBadge(now - (kStaleAfterSeconds - 1), now, true).show);
// At the threshold → amber badge, age reported.
{
StalenessBadge b = evaluateStalenessBadge(now - kStaleAfterSeconds, now, true);
EXPECT_TRUE(b.show);
EXPECT_TRUE(b.severity == StalenessSeverity::Warning);
EXPECT_EQ(b.seconds_old, (int64_t)kStaleAfterSeconds);
}
// Past the very-stale threshold → red.
{
StalenessBadge b = evaluateStalenessBadge(now - kVeryStaleAfterSeconds, now, true);
EXPECT_TRUE(b.show);
EXPECT_TRUE(b.severity == StalenessSeverity::Error);
}
// Clock skew (future timestamp) is clamped to age 0 → no badge, no negative age.
{
StalenessBadge b = evaluateStalenessBadge(now + 100, now, true);
EXPECT_TRUE(!b.show);
}
}
void testLoggerFileSink()
{
using dragonx::util::Logger;
@@ -7011,6 +7043,7 @@ int main()
testAllowsPlaintextRemote();
testConsoleSecretRedaction();
testNodeStatusBanner();
testStalenessBadge();
testLoggerFileSink();
testDaemonLifecycleExecution();
testDaemonLifecycleAdapters();