mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 03:36:59 +00:00
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:
@@ -1,6 +1,6 @@
|
||||
import Docker from 'dockerode';
|
||||
import { NodeRegistry } from './NodeRegistry';
|
||||
import { NotificationService } from './NotificationService';
|
||||
import { NotificationCategory, NotificationService } from './NotificationService';
|
||||
import { DatabaseService } from './DatabaseService';
|
||||
import {
|
||||
classifyDie,
|
||||
@@ -187,7 +187,7 @@ export class DockerEventService {
|
||||
this.status = 'connected';
|
||||
|
||||
if (this.disconnectedNoticeEmitted) {
|
||||
await this.emitInfo(`Reconnected to Docker daemon.`);
|
||||
await this.emitInfo('system', `Reconnected to Docker daemon.`);
|
||||
this.disconnectedNoticeEmitted = false;
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ export class DockerEventService {
|
||||
|
||||
if (!this.disconnectedNoticeEmitted) {
|
||||
this.disconnectedNoticeEmitted = true;
|
||||
void this.emitWarning(`Lost connection to Docker daemon; monitoring paused.`);
|
||||
void this.emitWarning('system', `Lost connection to Docker daemon; monitoring paused.`);
|
||||
}
|
||||
|
||||
if (isDebugEnabled()) {
|
||||
@@ -310,6 +310,7 @@ export class DockerEventService {
|
||||
|
||||
if (exitRatio >= MASS_EVENT_THRESHOLD) {
|
||||
await this.emitInfo(
|
||||
'system',
|
||||
`Docker daemon interruption detected: ${newlyExited.length} containers exited during connection gap.`
|
||||
);
|
||||
} else {
|
||||
@@ -450,6 +451,7 @@ export class DockerEventService {
|
||||
const name = state.name ?? id.slice(0, 12);
|
||||
const stackName = state.stackName;
|
||||
void this.emitError(
|
||||
'monitor_alert',
|
||||
`Healthcheck failed: ${name} is unhealthy.`,
|
||||
stackName,
|
||||
state.name,
|
||||
@@ -562,7 +564,7 @@ export class DockerEventService {
|
||||
// rate-suppressed alerts don't silently lock out the next real crash.
|
||||
if (state) state.lastCrashAlertAt = Date.now();
|
||||
|
||||
await this.emitError(message, info.stackName, info.name);
|
||||
await this.emitError('monitor_alert', message, info.stackName, info.name);
|
||||
}
|
||||
|
||||
private isCrashAlertsEnabled(): boolean {
|
||||
@@ -607,6 +609,7 @@ export class DockerEventService {
|
||||
this.suppressedCount = 0;
|
||||
if (count > 0) {
|
||||
void this.emitWarning(
|
||||
'monitor_alert',
|
||||
`${count} additional containers crashed in the last minute.`,
|
||||
);
|
||||
}
|
||||
@@ -624,6 +627,7 @@ export class DockerEventService {
|
||||
if (this.parseErrorCount > PARSE_ERROR_THRESHOLD && !this.parseWarningEmitted) {
|
||||
this.parseWarningEmitted = true;
|
||||
void this.emitWarning(
|
||||
'system',
|
||||
`Received malformed Docker event payloads. Monitoring continues but some events may be skipped.`,
|
||||
);
|
||||
}
|
||||
@@ -668,16 +672,16 @@ export class DockerEventService {
|
||||
// Notification wrappers (prefix with node name for multi-node clarity)
|
||||
// ========================================================================
|
||||
|
||||
private async emitError(message: string, stackName?: string, containerName?: string): Promise<void> {
|
||||
return this.notifier.dispatchAlert('error', this.prefix(message), stackName, containerName);
|
||||
private async emitError(category: NotificationCategory, message: string, stackName?: string, containerName?: string): Promise<void> {
|
||||
return this.notifier.dispatchAlert('error', category, this.prefix(message), { stackName, containerName });
|
||||
}
|
||||
|
||||
private async emitWarning(message: string, stackName?: string, containerName?: string): Promise<void> {
|
||||
return this.notifier.dispatchAlert('warning', this.prefix(message), stackName, containerName);
|
||||
private async emitWarning(category: NotificationCategory, message: string, stackName?: string, containerName?: string): Promise<void> {
|
||||
return this.notifier.dispatchAlert('warning', category, this.prefix(message), { stackName, containerName });
|
||||
}
|
||||
|
||||
private async emitInfo(message: string, stackName?: string, containerName?: string): Promise<void> {
|
||||
return this.notifier.dispatchAlert('info', this.prefix(message), stackName, containerName);
|
||||
private async emitInfo(category: NotificationCategory, message: string, stackName?: string, containerName?: string): Promise<void> {
|
||||
return this.notifier.dispatchAlert('info', category, this.prefix(message), { stackName, containerName });
|
||||
}
|
||||
|
||||
private prefix(message: string): string {
|
||||
|
||||
Reference in New Issue
Block a user