feat(notifications): add structured category enum to dispatcher and history (#774)

Introduce a NotificationCategory string-literal union (11 values) and
thread it through dispatchAlert as a required second argument. All
callers (DockerEventService, AutoHealService, ImageUpdateService,
MonitorService, PolicyEnforcement, policyGate, SchedulerService,
imageUpdates route) pass an explicit category at every call site,
giving TypeScript compile-time enforcement that no new emit site can
be added without choosing a category.

DatabaseService gains an idempotent migration that adds a nullable
category TEXT column to notification_history; existing rows keep
category=NULL (displayed as Uncategorized in the UI). The
getNotificationHistory method accepts an optional category filter
that is forwarded from the GET /api/notifications/history route via
a ?category= query param.

NotificationPanel gains a category Select dropdown so users can
filter history by category. The frontend types mirror the backend
union so API responses are type-safe end-to-end.

All 75 test files (1410 tests) updated to the new 4-arg dispatchAlert
signature and passing.
This commit is contained in:
Anso
2026-04-25 13:55:07 -04:00
committed by GitHub
parent a74564fd61
commit 44dba59cab
20 changed files with 250 additions and 134 deletions
+3 -2
View File
@@ -284,7 +284,7 @@ autoUpdateRouter.post('/execute', authMiddleware, async (req: Request, res: Resp
);
if (!autoUpdateGate.ok) {
const blockedMsg = `Policy "${autoUpdateGate.policy?.name}" blocked auto-update: ${autoUpdateGate.violations.length} image(s) exceed ${autoUpdateGate.policy?.max_severity}`;
NotificationService.getInstance().dispatchAlert('warning', blockedMsg, stackName);
NotificationService.getInstance().dispatchAlert('warning', 'scan_finding', blockedMsg, { stackName });
results.push(`Stack "${stackName}": ${blockedMsg}`);
continue;
}
@@ -294,8 +294,9 @@ autoUpdateRouter.post('/execute', authMiddleware, async (req: Request, res: Resp
NotificationService.getInstance().dispatchAlert(
'info',
'image_update_applied',
`Auto-update: stack "${stackName}" updated with new images`,
stackName,
{ stackName },
);
results.push(`Stack "${stackName}": updated (${updatedImages.join(', ')}).`);
+2 -1
View File
@@ -25,7 +25,8 @@ export const notificationsRouter = Router();
notificationsRouter.get('/', authMiddleware, async (req: Request, res: Response): Promise<void> => {
try {
const nodeId = req.nodeId ?? 0;
const history = DatabaseService.getInstance().getNotificationHistory(nodeId);
const category = typeof req.query.category === 'string' ? req.query.category : undefined;
const history = DatabaseService.getInstance().getNotificationHistory(nodeId, 50, category);
res.json(history);
} catch {
res.status(500).json({ error: 'Failed to fetch notifications' });