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

@@ -34,6 +34,7 @@
#include "ui/windows/mining_tab_helpers.h"
#include "ui/node_status_banner.h"
#include "ui/staleness_badge.h"
#include "ui/notifications.h"
#include "util/address_validation.h"
#include "util/amount_format.h"
#include "util/payment_uri.h"
@@ -2655,6 +2656,37 @@ void testStalenessBadge()
}
}
void testNotificationHistory()
{
using dragonx::ui::Notifications;
using dragonx::ui::NotificationType;
auto& n = Notifications::instance();
n.clearHistory();
auto base = n.totalPushed(); // monotonic counter is NOT reset by clearHistory()
n.push("first", NotificationType::Info, 5.0f);
n.push("second", NotificationType::Error, 5.0f);
EXPECT_EQ((int)n.history().size(), 2);
EXPECT_TRUE(n.hasHistory());
// Oldest first, newest last; type + wall-clock stamp retained.
EXPECT_EQ(n.history().front().message, std::string("first"));
EXPECT_EQ(n.history().back().message, std::string("second"));
EXPECT_TRUE(n.history().back().type == NotificationType::Error);
EXPECT_TRUE(n.history().back().epoch > 0);
EXPECT_EQ((int)(n.totalPushed() - base), 2);
// Cap at 100: push past it → size caps, oldest entries drop, totalPushed keeps counting.
for (int i = 0; i < 150; ++i) n.push("bulk", NotificationType::Info, 5.0f);
EXPECT_EQ((int)n.history().size(), 100);
EXPECT_EQ((int)(n.totalPushed() - base), 152);
EXPECT_EQ(n.history().front().message, std::string("bulk")); // the two originals fell off
n.clearHistory();
EXPECT_TRUE(!n.hasHistory());
EXPECT_EQ((int)n.history().size(), 0);
EXPECT_EQ((int)(n.totalPushed() - base), 152); // clearing doesn't rewind the counter
}
void testLoggerFileSink()
{
using dragonx::util::Logger;
@@ -7044,6 +7076,7 @@ int main()
testConsoleSecretRedaction();
testNodeStatusBanner();
testStalenessBadge();
testNotificationHistory();
testLoggerFileSink();
testDaemonLifecycleExecution();
testDaemonLifecycleAdapters();