feat(settings): surface security, notifications, and app store on remote nodes (#716)

Flip Security (Trivy), Notifications (agents + history), and App Store from
global-and-hidden-on-remote to node-scoped so operators can manage them when a
remote node is selected in the node picker. The primary instance proxies the
calls to each remote, which resolves the correct per-instance binary state,
agent config, and template registry.

Backend: key `agents` and `notification_history` by `node_id` with idempotent
column-add migrations and a `(node_id, type)` unique index on agents, matching
the Labels pattern. Thread `req.nodeId` through the /api/agents and
/api/notifications routes. Internal NotificationService and ImageUpdateService
writes resolve the middleware default via `NodeRegistry.getDefaultNodeId()` so
monitor-emitted rows share a bucket with user-facing ones (avoids split-brain
where the UI sees test notifications but not internal alerts).

Frontend: split Security on remote to render only the scanner card and hide
scan policies and CVE suppressions (those remain control-plane-only). Drop the
misleading "Always Local" badge on Developer since retention windows govern
backend jobs, not UI state. Flip the App Store registry to node-scoped.

Docs: add a "What Settings apply per node" table to multi-node, clarify
remote alert setup in alerts-notifications, and note Trivy's per-host install
in vulnerability-scanning.
This commit is contained in:
Anso
2026-04-20 21:04:09 -04:00
committed by GitHub
parent a42cc5bf03
commit 08f57c7141
14 changed files with 172 additions and 95 deletions
@@ -170,12 +170,12 @@ describe('DatabaseService - cleanupOldNotifications', () => {
const oldTimestamp = Date.now() - 60 * 24 * 60 * 60 * 1000; // 60 days ago
const recentTimestamp = Date.now() - 1 * 24 * 60 * 60 * 1000; // 1 day ago
db.addNotificationHistory({ level: 'info', message: 'old notification', timestamp: oldTimestamp });
db.addNotificationHistory({ level: 'info', message: 'recent notification', timestamp: recentTimestamp });
db.addNotificationHistory(0, { level: 'info', message: 'old notification', timestamp: oldTimestamp });
db.addNotificationHistory(0, { level: 'info', message: 'recent notification', timestamp: recentTimestamp });
db.cleanupOldNotifications(30);
const history = db.getNotificationHistory(200);
const history = db.getNotificationHistory(0, 200);
const old = history.find((n: any) => n.message === 'old notification');
const recent = history.find((n: any) => n.message === 'recent notification');
expect(old).toBeUndefined();
@@ -223,7 +223,7 @@ describe('DatabaseService - notification history cap', () => {
it('auto-prunes to 100 entries when adding notifications', () => {
// Insert 105 notifications
for (let i = 0; i < 105; i++) {
db.addNotificationHistory({
db.addNotificationHistory(0, {
level: 'info',
message: `cap-test-${i}`,
timestamp: Date.now() + i,
@@ -231,23 +231,23 @@ describe('DatabaseService - notification history cap', () => {
}
// The table should have at most 100 rows
const all = db.getNotificationHistory(200);
const all = db.getNotificationHistory(0, 200);
expect(all.length).toBeLessThanOrEqual(100);
});
it('keeps the most recent entries after pruning', () => {
// Clear all first
db.deleteAllNotifications();
db.deleteAllNotifications(0);
for (let i = 0; i < 105; i++) {
db.addNotificationHistory({
db.addNotificationHistory(0, {
level: 'info',
message: `order-test-${i}`,
timestamp: Date.now() + i * 10,
});
}
const all = db.getNotificationHistory(200);
const all = db.getNotificationHistory(0, 200);
// The newest entries should survive (ordered DESC by timestamp)
expect(all[0].message).toContain('order-test-');
// The oldest entries (0-4) should have been pruned