feat(notifications): add shared notification routing rules (Admiral tier) (#347)

Route stack alerts to specific Discord, Slack, or webhook channels instead
of the single global endpoint. Includes per-rule enable/disable, priority
ordering, and automatic fallback to global agents when no rule matches.

- Add notification_routes table, interface, and CRUD in DatabaseService
- Add routing logic in NotificationService.dispatchAlert with optional stackName
- Pass stack context from MonitorService (crash/health) and SchedulerService
- Add 5 API endpoints gated with requireAdmin + requireAdmiral
- Add NotificationRoutingSection UI with Combobox stack picker, channel tabs
- Parallel webhook dispatch via Promise.allSettled
- 10 unit tests covering routing, fallback, and edge cases
- Documentation with screenshots at docs/features/notification-routing.mdx
This commit is contained in:
Anso
2026-04-02 22:22:27 -04:00
committed by GitHub
parent 08c541ecaf
commit 1b573f542a
22 changed files with 1102 additions and 33 deletions
+6 -3
View File
@@ -142,7 +142,8 @@ export class SchedulerService {
if (task.last_status === 'failure') {
NotificationService.getInstance().dispatchAlert(
'info',
`Scheduled task "${task.name}" (${task.action}) recovered successfully`
`Scheduled task "${task.name}" (${task.action}) recovered successfully`,
task.target_id ?? undefined
);
}
} catch (error: unknown) {
@@ -168,7 +169,8 @@ export class SchedulerService {
console.error(`[SchedulerService] Task "${task.name}" (id=${task.id}) failed:`, errMsg);
NotificationService.getInstance().dispatchAlert(
'error',
`Scheduled task "${task.name}" (${task.action}) failed: ${errMsg}`
`Scheduled task "${task.name}" (${task.action}) failed: ${errMsg}`,
task.target_id ?? undefined
);
}
}
@@ -444,7 +446,8 @@ export class SchedulerService {
NotificationService.getInstance().dispatchAlert(
'info',
`Auto-update: stack "${stackName}" updated with new images`
`Auto-update: stack "${stackName}" updated with new images`,
stackName
);
return `Stack "${stackName}": updated (${updatedImages.join(', ')}).`;