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
co-authored by Claude Opus 4.8
parent e779ded2e8
commit c7c3440a7b
5 changed files with 109 additions and 2 deletions
+20
View File
@@ -26,6 +26,7 @@
#include "../effects/imgui_acrylic.h"
#include "../sidebar.h"
#include "../notifications.h"
#include "../staleness_badge.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <toml++/toml.hpp>
@@ -421,6 +422,25 @@ static void RenderBalanceClassic(App* app)
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx + 12 * dp, cy),
WithAlpha(Success(), 200), buf);
} else {
// Refresh-staleness badge (W6-2): connected, not syncing/mining, but the balance
// hasn't refreshed in a while (a busy daemon can fail z_gettotalbalance without
// dropping the whole connection). The node banner only covers full disconnects, so
// this pill is the sole signal that the shown number may be out of date.
StalenessBadge badge = evaluateStalenessBadge(
state.last_balance_update, std::time(nullptr), state.connected);
if (badge.show) {
const bool err = (badge.severity == StalenessSeverity::Error);
ImU32 fg = err ? Error() : Warning();
ImU32 bg = WithAlpha(fg, 38);
ImU32 bd = WithAlpha(fg, 90);
snprintf(buf, sizeof(buf), "%s %s",
TR("data_stale_prefix"), timeAgo(state.last_balance_update).c_str());
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd);
// Hover → explain what stale means and how old the data actually is.
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y)))
Tooltip("%s", TR("data_stale_tooltip"));
}
}
// Hover glow