mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-20 15:22:59 +00:00
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:
@@ -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
|
||||
|
||||
@@ -81,6 +81,7 @@ vi.mock('../services/NodeRegistry', () => ({
|
||||
NodeRegistry: {
|
||||
getInstance: () => ({
|
||||
getComposeDir: () => '/tmp/compose',
|
||||
getDefaultNodeId: () => 1,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
@@ -436,7 +437,7 @@ services:
|
||||
|
||||
await (service as any).checkNode(1, 'local', fakeDb());
|
||||
|
||||
expect(mockAddNotificationHistory).toHaveBeenCalledWith(expect.objectContaining({
|
||||
expect(mockAddNotificationHistory).toHaveBeenCalledWith(1, expect.objectContaining({
|
||||
level: 'error',
|
||||
message: expect.stringContaining('webhook timeout'),
|
||||
}));
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user