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
@@ -35,6 +35,17 @@ vi.mock('../services/DatabaseService', () => ({
},
}));
// NodeRegistry is consulted to resolve this instance's default node id so
// internal dispatch writes land on the same key the middleware sets for user
// requests. Mock it to a fixed id so assertions are deterministic.
vi.mock('../services/NodeRegistry', () => ({
NodeRegistry: {
getInstance: () => ({
getDefaultNodeId: () => 1,
}),
},
}));
// Spy on global fetch for webhook dispatch verification
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
vi.stubGlobal('fetch', mockFetch);
@@ -210,7 +221,7 @@ describe('NotificationService - routing logic', () => {
await svc.dispatchAlert('info', 'Should be logged');
expect(mockAddNotificationHistory).toHaveBeenCalledWith({
expect(mockAddNotificationHistory).toHaveBeenCalledWith(1, {
level: 'info',
message: 'Should be logged',
timestamp: expect.any(Number),
@@ -225,7 +236,7 @@ describe('NotificationService - routing logic', () => {
await svc.dispatchAlert('warning', 'Restarted', 'my-app', 'my-app-web-1');
expect(mockAddNotificationHistory).toHaveBeenCalledWith({
expect(mockAddNotificationHistory).toHaveBeenCalledWith(1, {
level: 'warning',
message: 'Restarted',
timestamp: expect.any(Number),