mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 18:32:52 +00:00
fix(dashboard): scope alert-rule count to stacks on the active node (#1722)
Intersect stack_alerts with FileSystemService.getStacks() so Configuration Status (dashboard and fleet local row) counts only rules for stacks that exist on that node, not orphaned names in the global table.
This commit is contained in:
@@ -6,11 +6,15 @@
|
||||
* - GET /api/dashboard/configuration returns the documented shape and
|
||||
* applies tier-correct `locked` flags for the Community and paid
|
||||
* personas (toggled via LicenseService spies).
|
||||
* - Alert-rule counts are scoped to stacks present on the active node
|
||||
* (dashboard and fleet local-node row agree on exact cardinality).
|
||||
* - GET /api/dashboard/stack-restarts clamps the `days` query parameter
|
||||
* to [1, 30] and falls back to 7 for invalid inputs.
|
||||
* - Neither endpoint leaks secret material (agent URLs, tokens) in the
|
||||
* response payload.
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import { setupTestDb, cleanupTestDb, loginAsTestAdmin } from './helpers/setupTestDb';
|
||||
@@ -192,6 +196,57 @@ describe('GET /api/dashboard/configuration', () => {
|
||||
db.getDb().prepare('DELETE FROM agents WHERE url = ?').run(SECRET_URL);
|
||||
}
|
||||
});
|
||||
|
||||
it('scopes alertRules to stacks on the active node for dashboard and fleet local row', async () => {
|
||||
const db = DatabaseService.getInstance();
|
||||
const composeDir = process.env.COMPOSE_DIR as string;
|
||||
const stackName = 'cfg-alert-scope';
|
||||
const stackDir = path.join(composeDir, stackName);
|
||||
const alertIds: number[] = [];
|
||||
|
||||
fs.mkdirSync(stackDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(stackDir, 'compose.yaml'),
|
||||
'services:\n web:\n image: nginx:latest\n',
|
||||
);
|
||||
|
||||
const baseAlert = {
|
||||
service_name: null as string | null,
|
||||
metric: 'cpu_percent',
|
||||
operator: '>',
|
||||
threshold: 80,
|
||||
duration_mins: 5,
|
||||
cooldown_mins: 15,
|
||||
last_fired_at: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
// Two rules on a discovered stack (exact rule count, not unique stacks)
|
||||
// plus one orphaned rule that must not inflate the scoped count.
|
||||
alertIds.push(db.addStackAlert({ ...baseAlert, stack_name: stackName, threshold: 80 }).id!);
|
||||
alertIds.push(db.addStackAlert({ ...baseAlert, stack_name: stackName, threshold: 90 }).id!);
|
||||
alertIds.push(db.addStackAlert({ ...baseAlert, stack_name: 'cfg-alert-orphan', threshold: 70 }).id!);
|
||||
|
||||
const dash = await request(app).get('/api/dashboard/configuration').set('Cookie', adminCookie);
|
||||
expect(dash.status).toBe(200);
|
||||
expect(dash.body.notifications.alertRules).toBe(2);
|
||||
|
||||
const fleet = await request(app).get('/api/fleet/configuration').set('Cookie', adminCookie);
|
||||
expect(fleet.status).toBe(200);
|
||||
expect(Array.isArray(fleet.body)).toBe(true);
|
||||
const localRow = fleet.body.find(
|
||||
(row: { type: string; configuration: { notifications: { alertRules: number } } | null }) =>
|
||||
row.type === 'local' && row.configuration != null,
|
||||
);
|
||||
expect(localRow).toBeDefined();
|
||||
expect(localRow.configuration.notifications.alertRules).toBe(2);
|
||||
} finally {
|
||||
for (const id of alertIds) {
|
||||
db.deleteStackAlert(id);
|
||||
}
|
||||
fs.rmSync(stackDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/dashboard/stack-restarts', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Router, type Request, type Response } from 'express';
|
||||
import { DatabaseService, type StackRestartSummary } from '../services/DatabaseService';
|
||||
import { CloudBackupService } from '../services/CloudBackupService';
|
||||
import { FileSystemService } from '../services/FileSystemService';
|
||||
import TrivyService from '../services/TrivyService';
|
||||
import { effectiveTier } from '../middleware/tierGates';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
@@ -49,11 +50,11 @@ export interface ConfigurationStatus {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildLocalConfigurationStatus(
|
||||
export async function buildLocalConfigurationStatus(
|
||||
nodeId: number,
|
||||
userId: number,
|
||||
tier: LicenseTier,
|
||||
): ConfigurationStatus {
|
||||
): Promise<ConfigurationStatus> {
|
||||
const db = DatabaseService.getInstance();
|
||||
|
||||
const agents = db.getAgents(nodeId);
|
||||
@@ -62,7 +63,10 @@ export function buildLocalConfigurationStatus(
|
||||
return { configured: !!a?.url, enabled: a?.enabled ?? false };
|
||||
};
|
||||
|
||||
const alertRules = db.getStackAlerts().length;
|
||||
// Scope to stacks that exist on this node. stack_alerts has no node_id;
|
||||
// intersecting with the node's compose directory is the per-node filter.
|
||||
const stackNames = new Set(await FileSystemService.getInstance(nodeId).getStacks());
|
||||
const alertRules = db.getStackAlerts().filter((a) => stackNames.has(a.stack_name)).length;
|
||||
const notifRoutes = db.getNotificationRoutes();
|
||||
|
||||
const healPolicies = db.getAutoHealPolicies(undefined, nodeId);
|
||||
@@ -165,7 +169,7 @@ export function buildLocalConfigurationStatus(
|
||||
}
|
||||
|
||||
// All routes below are protected by the global authGate mounted at app.use('/api', authGate)
|
||||
dashboardRouter.get('/configuration', (req: Request, res: Response): void => {
|
||||
dashboardRouter.get('/configuration', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const debug = isDebugEnabled();
|
||||
const startedAt = debug ? Date.now() : 0;
|
||||
@@ -173,7 +177,7 @@ dashboardRouter.get('/configuration', (req: Request, res: Response): void => {
|
||||
const userId = req.user?.userId ?? 0;
|
||||
const tier = effectiveTier(req);
|
||||
|
||||
const payload = buildLocalConfigurationStatus(nodeId, userId, tier);
|
||||
const payload = await buildLocalConfigurationStatus(nodeId, userId, tier);
|
||||
if (debug) {
|
||||
console.debug(
|
||||
`[Dashboard:debug] /configuration built in ${Date.now() - startedAt} ms (nodeId=${nodeId})`,
|
||||
|
||||
@@ -659,7 +659,7 @@ fleetRouter.get('/configuration', authMiddleware, async (req: Request, res: Resp
|
||||
name: node.name,
|
||||
type: 'local',
|
||||
status: 'online',
|
||||
configuration: buildLocalConfigurationStatus(node.id, userId, localTier),
|
||||
configuration: await buildLocalConfigurationStatus(node.id, userId, localTier),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user