feat(diagnostics): persistent alert history with a status-bar bell (Foundation QoL)

Toasts fade in 1-4s, so anything that scrolled past was gone. Notifications now retains
every pushed alert in a capped (100) ring buffer with a wall-clock epoch (AlertRecord) —
separate from the 5-item live-toast deque — plus a monotonic total_pushed_ counter.

A bell in the status-bar right cluster opens an upward popup listing recent alerts
newest-first: severity icon + colour (reusing the toast palette), the message, and a
relative age (formatTimeAgoShort), with a Clear-all action. An unread dot on the bell,
coloured by the most-severe unseen alert, marks alerts that arrived since the panel was
last opened — driven by totalPushed() deltas so it survives capping/clearing.

Thread note: every push is on the UI thread (RPC results run as main-thread MainCb
callbacks), matching this class's existing lock-free model; documented as a
no-raw-worker-thread invariant.

New i18n keys (alerts_*). Build-clean; ctest 1/1 (adds testNotificationHistory: retention,
order, cap, monotonic counter, clear). Closes the QoL bundle and the Foundation tier.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 20:57:02 -05:00
parent c7c3440a7b
commit 4b3f0fa92b
6 changed files with 239 additions and 7 deletions

View File

@@ -1030,6 +1030,7 @@ private:
double connect_stall_since_ = 0.0; // ImGui::GetTime() when the daemon first went "reachable but not ready"; 0 = not stalling (see util/connect_stall.h)
bool encryption_incomplete_warned_ = false; // W2-2: once-per-session guard for the "encryption didn't complete" warning
bool lock_failure_warned_ = false; // W2-4: guard so a repeatedly-failing auto-lock warns once, not every retry
std::uint64_t alerts_seen_total_ = 0; // Notifications::totalPushed() at last alert-panel open; drives the bell's unread dot
// Current page (sidebar navigation)
ui::NavPage current_page_ = ui::NavPage::Overview;
@@ -1299,6 +1300,9 @@ private:
// reach its node (or the embedded daemon gave up crashing). Decision logic is the pure
// evaluateNodeStatusBanner() in ui/node_status_banner.h; this draws it and wires the action.
void renderNodeStatusBanner();
// Body of the status-bar alert-history popup: recent alerts (incl. ones whose toast faded),
// newest first, with severity icon + relative age + a Clear action. See src/ui/notifications.h.
void renderAlertHistoryPanel();
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
void renderImportKeyDialog();