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
+9 -2
View File
@@ -89,6 +89,7 @@ export class SchedulerService {
await trivy.detectTrivy();
this.safeDispatch(
'info',
'system',
`Trivy updated from v${previous} to v${check.latest}`,
);
db.updateGlobalSetting('trivy_last_notified_version', check.latest);
@@ -100,6 +101,7 @@ export class SchedulerService {
if (lastNotified === check.latest) return;
this.safeDispatch(
'info',
'system',
`Trivy update available: v${check.latest} (currently v${check.current ?? 'unknown'})`,
);
db.updateGlobalSetting('trivy_last_notified_version', check.latest);
@@ -159,9 +161,9 @@ export class SchedulerService {
* Fire a notification without awaiting completion, catching any promise
* rejection so the scheduler never crashes on a failed dispatch.
*/
private safeDispatch(level: 'info' | 'warning' | 'error', message: string, stackName?: string): void {
private safeDispatch(level: 'info' | 'warning' | 'error', category: import('./NotificationService').NotificationCategory, message: string, stackName?: string): void {
NotificationService.getInstance()
.dispatchAlert(level, message, stackName)
.dispatchAlert(level, category, message, { stackName })
.catch(err => console.error('[SchedulerService] Notification dispatch failed:', getErrorMessage(err, 'unknown error')));
}
@@ -314,12 +316,14 @@ export class SchedulerService {
}
this.safeDispatch(
scanLevel,
'scan_finding',
`Scheduled scan "${task.name}" completed: ${output}`,
task.target_id ?? undefined
);
} else if (task.last_status === 'failure') {
this.safeDispatch(
'info',
'system',
`Scheduled task "${task.name}" (${task.action}) recovered successfully`,
task.target_id ?? undefined
);
@@ -355,6 +359,7 @@ export class SchedulerService {
console.error(`[SchedulerService] Task "${task.name}" (id=${task.id}) failed:`, errMsg);
this.safeDispatch(
'error',
'system',
`Scheduled task "${task.name}" (${task.action}) failed: ${errMsg}`,
task.target_id ?? undefined
);
@@ -647,6 +652,7 @@ export class SchedulerService {
this.safeDispatch(
'info',
'image_update_applied',
`Auto-update: stack "${stackName}" updated with new images`,
stackName
);
@@ -684,6 +690,7 @@ export class SchedulerService {
for (const v of summary.violations ?? []) {
NotificationService.getInstance().dispatchAlert(
'warning',
'scan_finding',
`Policy "${v.policyName}" violated by ${v.imageRef}: ${v.severity} exceeds ${v.maxSeverity}`,
);
}