From a8ecd44abf44cf80ea1fd6e4787046da6109400b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 16 Feb 2026 00:39:34 +0000 Subject: [PATCH] inject settings service, extract client host --- server/src/config/services.ts | 1 + .../INotificationProvider.ts | 3 ++- .../notificationProviders/discord.ts | 19 +++++++++++--- .../notificationProviders/email.ts | 14 +++++++--- .../notificationProviders/matrix.ts | 12 ++++++--- .../notificationProviders/pagerduty.ts | 7 ++--- .../notificationProviders/slack.ts | 7 ++--- .../notificationProviders/webhook.ts | 12 ++++++--- .../infrastructure/notificationsService.ts | 26 ++++++++++++++----- 9 files changed, 73 insertions(+), 28 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index f88bc2584..67c16ff6a 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -182,6 +182,7 @@ export const initializeServices = async ({ discordProvider, pagerDutyProvider, matrixProvider, + settingsService, logger ); diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index 0a1dbf876..21a221c9c 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -6,7 +6,8 @@ export interface INotificationProvider { notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, - decision: MonitorActionDecision + decision: MonitorActionDecision, + clientHost: string ) => Promise; sendTestAlert(notification: Notification): Promise; } diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index 200b8af2c..560c18aae 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -11,20 +11,31 @@ export class DiscordProvider implements INotificationProvider { constructor(logger: any) { this.logger = logger; } - private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => { + private getHardwareContent = ( + clientHost: string, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision + ) => { // For status changes (recovery), use standard format if (decision.notificationReason === "status_change") { return buildDiscordBody(monitor, monitorStatusResponse); } // For threshold breaches, use hardware alert format - const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const { discordPayload } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse); return discordPayload; }; - sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => { + sendAlert = async ( + notification: Notification, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision, + clientHost: string + ) => { let body; if (monitor.type === "hardware") { - body = this.getHardwareContent(monitor, monitorStatusResponse, decision); + body = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision); } else { body = buildDiscordBody(monitor, monitorStatusResponse); } diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index 802c44b80..fd15034d1 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -13,13 +13,18 @@ export class EmailProvider implements INotificationProvider { this.logger = logger; } - private buildHardwareEmail = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => { + private buildHardwareEmail = async ( + clientHost: string, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision + ) => { // For status changes (recovery), use standard email format if (decision.notificationReason === "status_change") { return await buildEmail(this.emailService, monitor); } // For threshold breaches, use hardware alert format - const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse); const html = buildHardwareEmail(this.emailService, monitor, alertsToSend); return html; }; @@ -28,7 +33,8 @@ export class EmailProvider implements INotificationProvider { notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, - decision: MonitorActionDecision + decision: MonitorActionDecision, + clientHost: string ): Promise { // For grouped notifications (identified by ":" in name), customize subject to indicate multiple services. // Example: "2 services: Service A, Service B" becomes "Alert: 2 services are down" @@ -48,7 +54,7 @@ export class EmailProvider implements INotificationProvider { let html; if (monitor.type === "hardware") { - html = await this.buildHardwareEmail(monitor, monitorStatusResponse, decision); + html = await this.buildHardwareEmail(clientHost, monitor, monitorStatusResponse, decision); } else { html = await buildEmail(this.emailService, monitor); } diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts index afca6da95..f2543886d 100644 --- a/server/src/service/infrastructure/notificationProviders/matrix.ts +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -27,7 +27,7 @@ export class MatrixProvider implements INotificationProvider { return buildWebhookBody(monitor, monitorStatusResponse); } // For threshold breaches, use hardware alert format - const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse); const body = buildHardwareNotificationMessage(clientHost, alertsToSend, monitor); return body; }; @@ -37,12 +37,18 @@ export class MatrixProvider implements INotificationProvider { return body; }; - sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => { + sendAlert = async ( + notification: Notification, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision, + clientHost: string + ) => { const { homeserverUrl, accessToken, roomId } = notification; let content; if (monitor.type === "hardware") { - content = this.getHardwareContent("HOST_PLACEHOLDER", monitor, monitorStatusResponse, decision); + content = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision); } else { content = this.getContent(monitor, monitorStatusResponse); } diff --git a/server/src/service/infrastructure/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts index 0634f46b4..291cd7de5 100644 --- a/server/src/service/infrastructure/notificationProviders/pagerduty.ts +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -27,7 +27,7 @@ export class PagerDutyProvider implements INotificationProvider { return buildWebhookBody(monitor, monitorStatusResponse); } // For threshold breaches, use hardware alert format - const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse); const body = buildHardwareNotificationMessage(clientHost, alertsToSend, monitor); return body; }; @@ -41,11 +41,12 @@ export class PagerDutyProvider implements INotificationProvider { notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, - decision: MonitorActionDecision + decision: MonitorActionDecision, + clientHost: string ): Promise { let body; if (monitor.type === "hardware") { - body = this.getHardwareContent("HOST_PLACEHOLDER", monitor, monitorStatusResponse, decision); + body = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision); } else { body = this.getContent(monitor, monitorStatusResponse); } diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index da7338a75..f99af48d4 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -27,7 +27,7 @@ export class SlackProvider implements INotificationProvider { return buildWebhookBody(monitor, monitorStatusResponse); } // For threshold breaches, use hardware alert format - const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse); const body = buildHardwareNotificationMessage(clientHost, alertsToSend, monitor); return body; }; @@ -41,11 +41,12 @@ export class SlackProvider implements INotificationProvider { notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, - decision: MonitorActionDecision + decision: MonitorActionDecision, + clientHost: string ): Promise { let body; if (monitor.type === "hardware") { - body = this.getHardwareContent("HOST_PLACEHOLDER", monitor, monitorStatusResponse, decision); + body = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision); } else { body = this.getContent(monitor, monitorStatusResponse); } diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 13a0ae8ef..17d871950 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -27,7 +27,7 @@ export class WebhookProvider implements INotificationProvider { return buildWebhookBody(monitor, monitorStatusResponse); } // For threshold breaches, use hardware alert format - const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse); const body = buildHardwareNotificationMessage(clientHost, alertsToSend, monitor); return body; }; @@ -37,10 +37,16 @@ export class WebhookProvider implements INotificationProvider { return body; }; - sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => { + sendAlert = async ( + notification: Notification, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision, + clientHost: string + ) => { let body; if (monitor.type === "hardware") { - body = this.getHardwareContent("HOST_PLACEHOLDER", monitor, monitorStatusResponse, decision); + body = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision); } else { body = this.getContent(monitor, monitorStatusResponse); } diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index f43402f30..e3dc36747 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -2,6 +2,7 @@ import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notificatio import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js"; import { INotificationProvider } from "./notificationProviders/INotificationProvider.js"; import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import type { ISettingsService } from "@/service/system/settingsService.js"; export interface INotificationsService { createNotification: (notificationData: Partial) => Promise; @@ -29,6 +30,7 @@ export class NotificationsService implements INotificationsService { private pagerDutyProvider: INotificationProvider; private matrixProvider: INotificationProvider; private logger: any; + private settingsService: ISettingsService; // Email grouping (batching) configuration private emailGroupingWindowMs: number; @@ -52,6 +54,7 @@ export class NotificationsService implements INotificationsService { discordProvider: INotificationProvider, pagerDutyProvider: INotificationProvider, matrixProvider: INotificationProvider, + settingsService: ISettingsService, logger: any ) { this.notificationsRepository = notificationsRepository; @@ -62,6 +65,7 @@ export class NotificationsService implements INotificationsService { this.discordProvider = discordProvider; this.pagerDutyProvider = pagerDutyProvider; this.matrixProvider = matrixProvider; + this.settingsService = settingsService; this.logger = logger; // Configure email grouping window (in milliseconds). @@ -82,19 +86,21 @@ export class NotificationsService implements INotificationsService { } private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise => { + const settings = this.settingsService.getSettings(); + const clientHost = settings.clientHost || "Host not defined"; switch (notification.type) { case "email": - return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!); + return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!, clientHost); case "slack": - return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!); + return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!, clientHost); case "discord": - return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!); + return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!, clientHost); case "pager_duty": - return await this.pagerDutyProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!); + return await this.pagerDutyProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!, clientHost); case "matrix": - return await this.matrixProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!); + return await this.matrixProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!, clientHost); case "webhook": - return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!); + return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!, clientHost); default: return false; } @@ -215,6 +221,12 @@ export class NotificationsService implements INotificationsService { return false; } + const { clientHost } = this.settingsService.getSettings(); + if (!clientHost) { + this.logger.warn({ message: "CLIENT_HOST not configured", service: SERVICE_NAME, method: "flushEmailGroup" }); + return false; + } + // Build a combined monitor name listing all affected services. // Example: "Service A, Service B" (2 services) or "Service A" (1 service) const uniqueNames = Array.from(new Set(monitors.map((m) => m.name))); @@ -235,7 +247,7 @@ export class NotificationsService implements INotificationsService { }; // Reuse existing email provider to send grouped notification. - return await this.emailProvider.sendAlert(notification, syntheticMonitor, baseStatus, decision); + return await this.emailProvider.sendAlert(notification, syntheticMonitor, baseStatus, decision, clientHost); }; handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => {