From a86bc59e09b0beaae2308000f68ed1194823aafd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 18 Jun 2025 14:42:00 +0800 Subject: [PATCH 01/13] remove unsued params from hook --- client/src/Hooks/useNotifications.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js index 303453655..69e03fd5e 100644 --- a/client/src/Hooks/useNotifications.js +++ b/client/src/Hooks/useNotifications.js @@ -98,12 +98,9 @@ const useGetNotificationById = (id, setNotification) => { const notification = response?.data?.data ?? null; const notificationData = { - userId: notification?.userId, - teamId: notification?.teamId, address: notification?.address, notificationName: notification?.notificationName, type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id, - config: notification?.config, }; setNotification(notificationData); From 440ce8a12bdca1c6e0df1c6613fd5087a6b71468 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 18 Jun 2025 14:42:26 +0800 Subject: [PATCH 02/13] simplify creatign notifications --- .../src/Pages/Notifications/create/index.jsx | 311 ++---------------- client/src/Pages/Notifications/index.jsx | 18 +- client/src/Pages/Notifications/utils.js | 32 ++ client/src/locales/en.json | 4 +- 4 files changed, 67 insertions(+), 298 deletions(-) diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx index 2fd83e4e1..fba30185f 100644 --- a/client/src/Pages/Notifications/create/index.jsx +++ b/client/src/Pages/Notifications/create/index.jsx @@ -17,15 +17,17 @@ import { useEditNotification, useTestNotification, } from "../../../Hooks/useNotifications"; -import { - notificationEmailValidation, - notificationWebhookValidation, - notificationPagerDutyValidation, -} from "../../../Validation/validation"; +import { notificationValidation } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; -import { NOTIFICATION_TYPES } from "../utils"; +import { + NOTIFICATION_TYPES, + TITLE_MAP, + DESCRIPTION_MAP, + LABEL_MAP, + PLACEHOLDER_MAP, +} from "../utils"; // Setup @@ -49,11 +51,6 @@ const CreateNotifications = () => { notificationName: "", address: "", type: NOTIFICATION_TYPES[0]._id, - config: { - webhookUrl: "", - platform: "", - routingKey: "", - }, }); const [errors, setErrors] = useState({}); const { t } = useTranslation(); @@ -71,42 +68,17 @@ const CreateNotifications = () => { type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value, }; - if (form.type === "slack" || form.type === "discord") { - form.type = "webhook"; - } - let error = null; - if (form.type === "email") { - error = notificationEmailValidation.validate( - { notificationName: form.notificationName, address: form.address }, - { abortEarly: false } - ).error; - } else if (form.type === "webhook") { - form.config = { - platform: form.config.platform, - webhookUrl: form.config.webhookUrl, - }; - error = notificationWebhookValidation.validate( - { notificationName: form.notificationName, config: form.config }, - { abortEarly: false } - ).error; - } else if (form.type === "pager_duty") { - form.config = { - platform: form.config.platform, - routingKey: form.config.routingKey, - }; - error = notificationPagerDutyValidation.validate( - { notificationName: form.notificationName, config: form.config }, - { abortEarly: false } - ).error; - } + error = notificationValidation.validate(form, { abortEarly: false }).error; if (error) { const newErrors = {}; error.details.forEach((err) => { newErrors[err.path[0]] = err.message; }); + console.log(JSON.stringify(newErrors)); + console.log(JSON.stringify(form, null, 2)); createToast({ body: "Please check the form for errors." }); setErrors(newErrors); return; @@ -124,63 +96,10 @@ const CreateNotifications = () => { const newNotification = { ...notification, [name]: value }; - // Handle config/platform initialization if type is webhook - - const type = NOTIFICATION_TYPES.find( - (type) => type._id === newNotification.type - ).value; - - if (type === "email") { - newNotification.config = null; - } else if (type === "slack" || type === "webhook" || type === "discord") { - newNotification.address = ""; - newNotification.config = newNotification.config || {}; - if (name === "config") { - newNotification.config = value; - } - if (type === "webhook") { - newNotification.config.platform = "webhook"; - } - if (type === "slack") { - newNotification.config.platform = "slack"; - } - if (type === "discord") { - newNotification.config.platform = "discord"; - } - } else if (type === "pager_duty") { - newNotification.config = newNotification.config || {}; - if (name === "config") { - newNotification.config = value; - } - newNotification.config.platform = "pager_duty"; - } - - // Field-level validation - let fieldError; - - if (name === "notificationName") { - const { error } = notificationEmailValidation.extract(name).validate(value); - fieldError = error?.message; - } - - if (type === "email" && name === "address") { - const { error } = notificationEmailValidation.extract(name).validate(value); - fieldError = error?.message; - } - - if ( - (type === "slack" || type === "webhook" || type === "discord") && - name === "config" - ) { - // Validate only webhookUrl inside config - const { error } = notificationWebhookValidation.extract("config").validate(value); - fieldError = error?.message; - } - - // Set field-level error + const { error } = notificationValidation.extract(name).validate(value); setErrors((prev) => ({ ...prev, - [name]: fieldError, + [name]: error?.message, })); setNotification(newNotification); @@ -192,36 +111,9 @@ const CreateNotifications = () => { type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value, }; - if (form.type === "slack" || form.type === "discord") { - form.type = "webhook"; - } - let error = null; - if (form.type === "email") { - error = notificationEmailValidation.validate( - { notificationName: form.notificationName, address: form.address }, - { abortEarly: false } - ).error; - } else if (form.type === "webhook") { - form.config = { - platform: form.config.platform, - webhookUrl: form.config.webhookUrl, - }; - error = notificationWebhookValidation.validate( - { notificationName: form.notificationName, config: form.config }, - { abortEarly: false } - ).error; - } else if (form.type === "pager_duty") { - form.config = { - platform: form.config.platform, - routingKey: form.config.routingKey, - }; - error = notificationPagerDutyValidation.validate( - { notificationName: form.notificationName, config: form.config }, - { abortEarly: false } - ).error; - } + error = notificationValidation.validate(form, { abortEarly: false }).error; if (error) { const newErrors = {}; @@ -236,6 +128,7 @@ const CreateNotifications = () => { testNotification(form); }; + const type = NOTIFICATION_TYPES.find((type) => type._id === notification.type).value; return ( @@ -287,164 +180,24 @@ const CreateNotifications = () => { /> - {notification.type === 1 && ( - - - - {t("createNotifications.emailSettings.title")} - - - {t("createNotifications.emailSettings.description")} - - - - - - - )} - {notification.type === 2 && ( - - - - {t("createNotifications.slackSettings.title")} - - - {t("createNotifications.slackSettings.description")} - - - - { - const updatedConfig = { - ...notification.config, - webhookUrl: e.target.value, - }; + + + {t(TITLE_MAP[type])} + {t(DESCRIPTION_MAP[type])} + + + + + - onChange({ - target: { - name: "config", - value: updatedConfig, - }, - }); - }} - /> - - - )} - {notification.type === 3 && ( - - - - {t("createNotifications.pagerdutySettings.title")} - - - {t("createNotifications.pagerdutySettings.description")} - - - - { - const updatedConfig = { - ...notification.config, - routingKey: e.target.value, - }; - - onChange({ - target: { - name: "config", - value: updatedConfig, - }, - }); - }} - /> - - - )} - {notification.type === 4 && ( - - - - {t("createNotifications.genericWebhookSettings.title")} - - - {t("createNotifications.genericWebhookSettings.description")} - - - - { - const updatedConfig = { - ...notification.config, - webhookUrl: e.target.value, - }; - - onChange({ - target: { - name: "config", - value: updatedConfig, - }, - }); - }} - /> - - - )} - {notification.type === 5 && ( - - - - {t("createNotifications.discordSettings.title")} - - - {t("createNotifications.discordSettings.description")} - - - - { - const updatedConfig = { - ...notification.config, - webhookUrl: e.target.value, - }; - - onChange({ - target: { - name: "config", - value: updatedConfig, - }, - }); - }} - /> - - - )} { return row.notificationName; }, }, - { - id: "type", - content: "Type", - render: (row) => { - return row.type; - }, - }, { id: "target", content: "Target", render: (row) => { - const type = row.type; - if (type === "email") { - return row.address; - } - if (type === "webhook") { - return row.config?.webhookUrl; - } - if (type === "pager_duty") { - return row.config?.routingKey; - } + return row.address; }, }, { diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/Notifications/utils.js index 545791246..7ef6e5a60 100644 --- a/client/src/Pages/Notifications/utils.js +++ b/client/src/Pages/Notifications/utils.js @@ -5,3 +5,35 @@ export const NOTIFICATION_TYPES = [ { _id: 4, name: "Webhook", value: "webhook" }, { _id: 5, name: "Discord", value: "discord" }, ]; + +export const TITLE_MAP = { + email: "createNotifications.emailSettings.title", + slack: "createNotifications.slackSettings.title", + pager_duty: "createNotifications.pagerDutySettings.title", + webhook: "createNotifications.webhookSettings.title", + discord: "createNotifications.discordSettings.title", +}; + +export const DESCRIPTION_MAP = { + email: "createNotifications.emailSettings.description", + slack: "createNotifications.slackSettings.description", + pager_duty: "createNotifications.pagerDutySettings.description", + webhook: "createNotifications.webhookSettings.description", + discord: "createNotifications.discordSettings.description", +}; + +export const LABEL_MAP = { + email: "createNotifications.emailSettings.emailLabel", + slack: "createNotifications.slackSettings.webhookLabel", + pager_duty: "createNotifications.pagerDutySettings.integrationKeyLabel", + webhook: "createNotifications.webhookSettings.webhookLabel", + discord: "createNotifications.discordSettings.webhookLabel", +}; + +export const PLACEHOLDER_MAP = { + email: "createNotifications.emailSettings.emailPlaceholder", + slack: "createNotifications.slackSettings.webhookPlaceholder", + pager_duty: "createNotifications.pagerDutySettings.integrationKeyPlaceholder", + webhook: "createNotifications.webhookSettings.webhookPlaceholder", + discord: "createNotifications.discordSettings.webhookPlaceholder", +}; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 7d2cdbfa7..5649f3de7 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -707,13 +707,13 @@ "webhookLabel": "Slack webhook URL", "webhookPlaceholder": "https://hooks.slack.com/services/..." }, - "pagerdutySettings": { + "pagerDutySettings": { "title": "PagerDuty", "description": "Configure your PagerDuty integration here", "integrationKeyLabel": "Integration key", "integrationKeyPlaceholder": "1234567890" }, - "genericWebhookSettings": { + "webhookSettings": { "title": "Webhook", "description": "Configure your webhook here", "webhookLabel": "Webhook URL", From bb921690bf87a8591b75316ac6df6c6339e5605c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 18 Jun 2025 14:42:39 +0800 Subject: [PATCH 03/13] Update validation --- client/src/Validation/validation.js | 58 +++++------------------------ 1 file changed, 9 insertions(+), 49 deletions(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 02d957063..4e6a806c8 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -404,57 +404,19 @@ const infrastructureMonitorValidation = joi.object({ notifications: joi.array().items(joi.string()), }); -const notificationEmailValidation = joi.object({ +const notificationValidation = joi.object({ notificationName: joi.string().required().messages({ "string.empty": "Notification name is required", "any.required": "Notification name is required", }), - address: joi - .string() - .email({ tlds: { allow: false } }) - .required() - .messages({ - "string.empty": "E-mail address cannot be empty", - - "string.email": "Please enter a valid e-mail address", - "string.base": "E-mail address must be a string", - "any.required": "E-mail address is required", - }), -}); -const notificationWebhookValidation = joi.object({ - notificationName: joi.string().required().messages({ - "string.empty": "Notification name is required", - "any.required": "Notification name is required", + address: joi.string().required().messages({ + "string.empty": "This field cannot be empty", + "string.base": "This field must be a string", + "any.required": "This field is required", }), - config: joi - .object({ - webhookUrl: joi.string().uri().required().messages({ - "string.empty": "Webhook URL is required", - "string.uri": "Webhook URL must be a valid URI", - "any.required": "Webhook URL is required", - }), - platform: joi.string().required().messages({ - "string.base": "Platform must be a string", - "any.required": "Platform is required", - }), - }) - .unknown(true), -}); - -const notificationPagerDutyValidation = joi.object({ - notificationName: joi.string().required().messages({ - "string.empty": "Notification name is required", - "any.required": "Notification name is required", - }), - config: joi.object({ - platform: joi.string().required().messages({ - "string.base": "Platform must be a string", - "any.required": "Platform is required", - }), - routingKey: joi.string().required().messages({ - "string.empty": "Routing key is required", - "any.required": "Routing key is required", - }), + type: joi.string().required().messages({ + "string.empty": "This field is required", + "any.required": "This field is required", }), }); @@ -469,7 +431,5 @@ export { infrastructureMonitorValidation, statusPageValidation, logoImageValidation, - notificationEmailValidation, - notificationWebhookValidation, - notificationPagerDutyValidation, + notificationValidation, }; From 7bca60269cc79eb8f56b6139e4877716cdd928e0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 18 Jun 2025 14:42:59 +0800 Subject: [PATCH 04/13] update schema and validation --- server/db/models/Notification.js | 23 +---------------------- server/validation/joi.js | 25 ++++++++----------------- 2 files changed, 9 insertions(+), 39 deletions(-) diff --git a/server/db/models/Notification.js b/server/db/models/Notification.js index 751324131..fe314fc74 100755 --- a/server/db/models/Notification.js +++ b/server/db/models/Notification.js @@ -1,19 +1,5 @@ import mongoose from "mongoose"; -const configSchema = mongoose.Schema( - { - webhookUrl: { type: String }, - botToken: { type: String }, - chatId: { type: String }, - platform: { - type: String, - enum: ["slack", "pager_duty", "webhook", "discord"], - }, - routingKey: { type: String }, - }, - { _id: false } -); - const NotificationSchema = mongoose.Schema( { userId: { @@ -30,14 +16,7 @@ const NotificationSchema = mongoose.Schema( }, type: { type: String, - enum: ["email", "sms", "webhook", "pager_duty"], - }, - platform: { - type: String, - }, - config: { - type: configSchema, - default: () => ({}), + enum: ["email", "slack", "discord", "webhook", "pager_duty"], }, notificationName: { type: String, diff --git a/server/validation/joi.js b/server/validation/joi.js index e684cefa2..448a844f1 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -570,24 +570,15 @@ const createNotificationBodyValidation = joi.object({ "string.email": "Please enter a valid e-mail address", }), }), - type: joi.string().valid("email", "webhook", "pager_duty").required().messages({ - "string.empty": "Notification type is required", - "any.required": "Notification type is required", - "any.only": "Notification type must be email, webhook, or pager_duty", - }), - config: joi.when("type", { - is: "webhook", - then: joi.object({ - webhookUrl: joi.string().uri().required().messages({ - "string.uri": "Webhook URL must be a valid URI", - "any.required": "Webhook URL is required", - }), - platform: joi.string().required().messages({ - "string.empty": "Platform is required", - "any.required": "Platform is required", - }), + type: joi + .string() + .valid("email", "webhook", "slack", "discord", "pager_duty") + .required() + .messages({ + "string.empty": "Notification type is required", + "any.required": "Notification type is required", + "any.only": "Notification type must be email, webhook, or pager_duty", }), - }), }); //**************************************** From 7e5521eb1351f5bf397415362288ecb896758899 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 19 Jun 2025 12:10:07 +0800 Subject: [PATCH 05/13] Move thresholds from Notification to Monitor --- server/db/models/Monitor.js | 69 ++++++++++++++++++++++++++++---- server/db/models/Notification.js | 28 ------------- 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/server/db/models/Monitor.js b/server/db/models/Monitor.js index 7bb38f520..38aeff0c3 100755 --- a/server/db/models/Monitor.js +++ b/server/db/models/Monitor.js @@ -69,6 +69,15 @@ const MonitorSchema = mongoose.Schema( type: Number, default: undefined, }, + notifications: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: "Notification", + }, + ], + secret: { + type: String, + }, thresholds: { type: { usage_cpu: { type: Number }, @@ -78,14 +87,33 @@ const MonitorSchema = mongoose.Schema( }, _id: false, }, - notifications: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Notification", + alertThreshold: { + type: Number, + default: 5, + }, + cpuAlertThreshold: { + type: Number, + default: function () { + return this.alertThreshold; + }, + }, + memoryAlertThreshold: { + type: Number, + default: function () { + return this.alertThreshold; + }, + }, + diskAlertThreshold: { + type: Number, + default: function () { + return this.alertThreshold; + }, + }, + tempAlertThreshold: { + type: Number, + default: function () { + return this.alertThreshold; }, - ], - secret: { - type: String, }, }, { @@ -132,6 +160,33 @@ MonitorSchema.pre("deleteMany", async function (next) { next(); }); +MonitorSchema.pre("save", function (next) { + if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) { + this.cpuAlertThreshold = this.alertThreshold; + } + if (!this.memoryAlertThreshold || this.isModified("alertThreshold")) { + this.memoryAlertThreshold = this.alertThreshold; + } + if (!this.diskAlertThreshold || this.isModified("alertThreshold")) { + this.diskAlertThreshold = this.alertThreshold; + } + if (!this.tempAlertThreshold || this.isModified("alertThreshold")) { + this.tempAlertThreshold = this.alertThreshold; + } + next(); +}); + +MonitorSchema.pre("findOneAndUpdate", function (next) { + const update = this.getUpdate(); + if (update.alertThreshold) { + update.cpuAlertThreshold = update.alertThreshold; + update.memoryAlertThreshold = update.alertThreshold; + update.diskAlertThreshold = update.alertThreshold; + update.tempAlertThreshold = update.alertThreshold; + } + next(); +}); + MonitorSchema.index({ teamId: 1, type: 1 }); export default mongoose.model("Monitor", MonitorSchema); diff --git a/server/db/models/Notification.js b/server/db/models/Notification.js index fe314fc74..a6a3c7c09 100755 --- a/server/db/models/Notification.js +++ b/server/db/models/Notification.js @@ -28,34 +28,6 @@ const NotificationSchema = mongoose.Schema( phone: { type: String, }, - alertThreshold: { - type: Number, - default: 5, - }, - cpuAlertThreshold: { - type: Number, - default: function () { - return this.alertThreshold; - }, - }, - memoryAlertThreshold: { - type: Number, - default: function () { - return this.alertThreshold; - }, - }, - diskAlertThreshold: { - type: Number, - default: function () { - return this.alertThreshold; - }, - }, - tempAlertThreshold: { - type: Number, - default: function () { - return this.alertThreshold; - }, - }, }, { timestamps: true, From dbbe008e23a1f4bf74e46d49b3440fca3ffa494e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 19 Jun 2025 12:10:29 +0800 Subject: [PATCH 06/13] remove unused controller methods --- server/controllers/notificationController.js | 74 +------------------- 1 file changed, 1 insertion(+), 73 deletions(-) diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js index 9fee7803e..e7e2a1f2a 100755 --- a/server/controllers/notificationController.js +++ b/server/controllers/notificationController.js @@ -23,85 +23,13 @@ class NotificationController { this.stringService = stringService; this.statusService = statusService; this.db = db; - this.triggerNotification = this.triggerNotification.bind(this); - } - - async triggerNotification(req, res, next) { - try { - await triggerNotificationBodyValidation.validateAsync(req.body, { - abortEarly: false, - stripUnknown: true, - }); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { - const { monitorId, type, platform, config, status = false } = req.body; - - // Create a simplified networkResponse similar to what would come from monitoring - const networkResponse = { - monitorId, - status, - }; - - // Use the statusService to get monitor details and handle status change logic - // This returns { monitor, statusChanged, prevStatus } exactly like your job queue uses - const statusResult = await this.statusService.updateStatus(networkResponse); - - if (type === NOTIFICATION_TYPES.WEBHOOK) { - const notification = { - type, - platform, - config, - }; - - await this.notificationService.sendWebhookNotification( - statusResult, // Contains monitor, statusChanged, and prevStatus - notification - ); - } - - return res.success({ - msg: this.stringService.webhookSendSuccess, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "triggerNotification")); - } - } - - createTestNetworkResponse() { - return { - monitor: { - _id: "test-monitor-id", - name: "Test Monitor", - url: "https://example.com", - }, - status: true, - statusChanged: true, - prevStatus: false, - }; } testNotification = async (req, res, next) => { try { const notification = req.body; - let success; - if (notification?.type === "email") { - success = await this.notificationService.sendTestEmail(notification); - } - - if (notification?.type === "webhook") { - success = - await this.notificationService.sendTestWebhookNotification(notification); - } - - if (notification?.type === "pager_duty") { - success = - await this.notificationService.sendTestPagerDutyNotification(notification); - } + const success = await this.notificationService.sendTestNotification(notification); if (!success) { return res.error({ From 68aa9a6b2da9a96470278ce7dbcbc16b33abbe0d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 19 Jun 2025 12:10:44 +0800 Subject: [PATCH 07/13] remove unused route --- server/routes/notificationRoute.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/routes/notificationRoute.js b/server/routes/notificationRoute.js index a937975c5..164c0afcc 100755 --- a/server/routes/notificationRoute.js +++ b/server/routes/notificationRoute.js @@ -11,8 +11,6 @@ class NotificationRoutes { initializeRoutes() { this.router.use(verifyJWT); - this.router.post("/trigger", this.notificationController.triggerNotification); - this.router.post("/test", this.notificationController.testNotification); this.router.post("/test/all", this.notificationController.testAllNotifications); From 8ea2053d3731a4aa397db82eb42d86b8f2e343f5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 19 Jun 2025 12:11:11 +0800 Subject: [PATCH 08/13] add a utils file for notifications --- server/service/notificationUtils.js | 146 ++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 server/service/notificationUtils.js diff --git a/server/service/notificationUtils.js b/server/service/notificationUtils.js new file mode 100644 index 000000000..61c3ec36a --- /dev/null +++ b/server/service/notificationUtils.js @@ -0,0 +1,146 @@ +class NotificationUtils { + constructor({ stringService, emailService }) { + this.stringService = stringService; + this.emailService = emailService; + } + + buildTestEmail = async () => { + const context = { testName: "Monitoring System" }; + const html = await this.emailService.buildEmail("testEmailTemplate", context); + return html; + }; + + buildStatusEmail = async (networkResponse) => { + const { monitor, status, prevStatus } = networkResponse; + const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate"; + const context = { monitor: monitor.name, url: monitor.url }; + const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`; + const html = await this.emailService.buildEmail(template, context); + return { subject, html }; + }; + + buildWebhookMessage = (networkResponse) => { + const { monitor, status, code, timestamp } = networkResponse; + // Format timestamp using the local system timezone + const formatTime = (timestamp) => { + const date = new Date(timestamp); + + // Get timezone abbreviation and format the date + const timeZoneAbbr = date + .toLocaleTimeString("en-US", { timeZoneName: "short" }) + .split(" ") + .pop(); + + // Format the date with readable format + return ( + date + .toLocaleString("en-US", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + .replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") + + " " + + timeZoneAbbr + ); + }; + + // Get formatted time + const formattedTime = timestamp + ? formatTime(timestamp) + : formatTime(new Date().getTime()); + + // Create different messages based on status with extra spacing + let messageText; + if (status === true) { + messageText = this.stringService.monitorUpAlert + .replace("{monitorName}", monitor.name) + .replace("{time}", formattedTime) + .replace("{code}", code || "Unknown"); + } else { + messageText = this.stringService.monitorDownAlert + .replace("{monitorName}", monitor.name) + .replace("{time}", formattedTime) + .replace("{code}", code || "Unknown"); + } + return messageText; + }; + + buildHardwareAlerts = async (networkResponse) => { + const monitor = networkResponse?.monitor; + const thresholds = networkResponse?.monitor?.thresholds; + const { + usage_cpu: cpuThreshold = -1, + usage_memory: memoryThreshold = -1, + usage_disk: diskThreshold = -1, + } = thresholds; + + const metrics = networkResponse?.payload?.data; + const { + cpu: { usage_percent: cpuUsage = -1 } = {}, + memory: { usage_percent: memoryUsage = -1 } = {}, + disk = [], + } = metrics; + + const alerts = { + cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, + memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, + disk: + disk?.some( + (d) => + diskThreshold !== -1 && + typeof d?.usage_percent === "number" && + d?.usage_percent > diskThreshold + ) ?? false, + }; + + const alertsToSend = []; + const alertTypes = ["cpu", "memory", "disk"]; + for (const type of alertTypes) { + // Iterate over each alert type to see if any need to be decremented + if (alerts[type] === true) { + monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered + + if (monitor[`${type}AlertThreshold`] <= 0) { + // If threshold drops below 0, reset and send notification + monitor[`${type}AlertThreshold`] = monitor.alertThreshold; + + const formatAlert = { + cpu: () => + `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, + memory: () => + `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, + disk: () => + `Your current disk usage: ${disk + .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`) + .join( + ", " + )} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, + }; + alertsToSend.push(formatAlert[type]()); + } + } + } + await monitor.save(); + return alertsToSend; + }; + + buildHardwareEmail = async (networkResponse, alerts) => { + const { monitor } = networkResponse; + const template = "hardwareIncidentTemplate"; + const context = { monitor: monitor.name, url: monitor.url, alerts }; + const subject = `Monitor ${monitor.name} infrastructure alerts`; + const html = await this.emailService.buildEmail(template, context); + return { subject, html }; + }; + + buildHardwareNotificationMessage = (alerts) => { + return alerts.map((alert) => alert).join("\n"); + }; +} + +export default NotificationUtils; From d4b8eebd647a3f042de4e04f150d2f1cf8e5e84e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 19 Jun 2025 12:11:43 +0800 Subject: [PATCH 09/13] cleanup --- server/service/networkService.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/server/service/networkService.js b/server/service/networkService.js index 5ad28e594..08b2f065e 100755 --- a/server/service/networkService.js +++ b/server/service/networkService.js @@ -432,9 +432,9 @@ class NetworkService { throw err; } - async requestWebhook(platform, url, message) { + async requestWebhook(type, url, body) { try { - const response = await this.axios.post(url, message, { + const response = await this.axios.post(url, body, { headers: { "Content-Type": "application/json", }, @@ -444,7 +444,7 @@ class NetworkService { type: "webhook", status: true, code: response.status, - message: `Successfully sent ${platform} notification`, + message: `Successfully sent ${type} notification`, payload: response.data, }; } catch (error) { @@ -452,19 +452,13 @@ class NetworkService { message: error.message, service: this.SERVICE_NAME, method: "requestWebhook", - url, - platform, - error: error.message, - statusCode: error.response?.status, - responseData: error.response?.data, - requestPayload: message, }); return { type: "webhook", status: false, code: error.response?.status || this.NETWORK_ERROR, - message: `Failed to send ${platform} notification`, + message: `Failed to send ${type} notification`, payload: error.response?.data, }; } From 30a7de5333ef61afff5c4beb64d54732fbef203c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 19 Jun 2025 12:12:09 +0800 Subject: [PATCH 10/13] remove unused import --- server/service/PulseQueue/PulseQueue.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/service/PulseQueue/PulseQueue.js b/server/service/PulseQueue/PulseQueue.js index fd420400f..0452985a0 100644 --- a/server/service/PulseQueue/PulseQueue.js +++ b/server/service/PulseQueue/PulseQueue.js @@ -1,5 +1,4 @@ import { Pulse } from "@pulsecron/pulse"; -import { ObjectId } from "mongodb"; const SERVICE_NAME = "PulseQueue"; class PulseQueue { From 1b4b66e36de3d22ddbca2090f29ac95ff2dad3b2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 19 Jun 2025 12:12:34 +0800 Subject: [PATCH 11/13] inject notification utils --- server/index.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/server/index.js b/server/index.js index 0753af995..99be6de7a 100755 --- a/server/index.js +++ b/server/index.js @@ -73,6 +73,7 @@ import StatusService from "./service/statusService.js"; // Notification Service and dependencies import NotificationService from "./service/notificationService.js"; +import NotificationUtils from "./service/notificationUtils.js"; // Buffer Service and dependencies import BufferService from "./service/bufferService.js"; @@ -173,13 +174,19 @@ const startApp = async () => { ); const bufferService = new BufferService({ db, logger }); const statusService = new StatusService({ db, logger, buffer: bufferService }); - const notificationService = new NotificationService( + const notificationUtils = new NotificationUtils({ + stringService, + emailService, + }); + + const notificationService = new NotificationService({ emailService, db, logger, networkService, - stringService - ); + stringService, + notificationUtils, + }); const redisService = new RedisService({ Redis: IORedis, logger }); From 48b25e0838939319e6561a21454d0c8f3dc1c792 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 19 Jun 2025 12:12:55 +0800 Subject: [PATCH 12/13] refactor notification service --- server/service/notificationService.js | 555 ++++---------------------- 1 file changed, 83 insertions(+), 472 deletions(-) mode change 100755 => 100644 server/service/notificationService.js diff --git a/server/service/notificationService.js b/server/service/notificationService.js old mode 100755 new mode 100644 index dcd629d53..a2453faa9 --- a/server/service/notificationService.js +++ b/server/service/notificationService.js @@ -1,512 +1,123 @@ -const SERVICE_NAME = "NotificationService"; -const TELEGRAM_API_BASE_URL = "https://api.telegram.org/bot"; -const PLATFORM_TYPES = ["telegram", "slack", "discord", "webhook"]; - -const MESSAGE_FORMATTERS = { - telegram: (messageText, chatId) => ({ chat_id: chatId, text: messageText }), - slack: (messageText) => ({ text: messageText }), - discord: (messageText) => ({ content: messageText }), - webhook: (messageText) => ({ text: messageText }), -}; - class NotificationService { - static SERVICE_NAME = SERVICE_NAME; - /** - * Creates an instance of NotificationService. - * - * @param {Object} emailService - The email service used for sending notifications. - * @param {Object} db - The database instance for storing notification data. - * @param {Object} logger - The logger instance for logging activities. - * @param {Object} networkService - The network service for sending webhook notifications. - */ - constructor(emailService, db, logger, networkService, stringService) { - this.SERVICE_NAME = SERVICE_NAME; + constructor({ + emailService, + db, + logger, + networkService, + stringService, + notificationUtils, + }) { this.emailService = emailService; this.db = db; this.logger = logger; this.networkService = networkService; this.stringService = stringService; + this.notificationUtils = notificationUtils; } - /** - * Formats a notification message based on the monitor status and platform. - * - * @param {Object} monitor - The monitor object. - * @param {string} monitor.name - The name of the monitor. - * @param {string} monitor.url - The URL of the monitor. - * @param {boolean} status - The current status of the monitor (true for up, false for down). - * @param {string} platform - The notification platform (e.g., "telegram", "slack", "discord"). - * @param {string} [chatId] - The chat ID for platforms that require it (e.g., Telegram). - * @returns {Object|null} The formatted message object for the specified platform, or null if the platform is unsupported. - */ + sendNotification = async ({ notification, subject, content, html }) => { + const { type, address } = notification; - formatNotificationMessage(monitor, status, platform, chatId, code, timestamp) { - // Format timestamp using the local system timezone - const formatTime = (timestamp) => { - const date = new Date(timestamp); - - // Get timezone abbreviation and format the date - const timeZoneAbbr = date - .toLocaleTimeString("en-US", { timeZoneName: "short" }) - .split(" ") - .pop(); - - // Format the date with readable format - return ( - date - .toLocaleString("en-US", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - }) - .replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") + - " " + - timeZoneAbbr - ); - }; - - // Get formatted time - const formattedTime = timestamp - ? formatTime(timestamp) - : formatTime(new Date().getTime()); - - // Create different messages based on status with extra spacing - let messageText; - if (status === true) { - messageText = this.stringService.monitorUpAlert - .replace("{monitorName}", monitor.name) - .replace("{time}", formattedTime) - .replace("{code}", code || "Unknown"); - } else { - messageText = this.stringService.monitorDownAlert - .replace("{monitorName}", monitor.name) - .replace("{time}", formattedTime) - .replace("{code}", code || "Unknown"); - } - - if (!PLATFORM_TYPES.includes(platform)) { - return undefined; - } - - return MESSAGE_FORMATTERS[platform](messageText, chatId); - } - - formatHardwareNotificationMessage = ( - monitor, - status, - platform, - chatId, - code, - timestamp, - alerts - ) => { - const messageText = alerts.map((alert) => alert).join("\n"); - return MESSAGE_FORMATTERS[platform](messageText, chatId); - }; - - sendTestWebhookNotification = async (notification) => { - const config = notification.config; - const response = await this.networkService.requestWebhook( - config.platform, - config.webhookUrl, - "This is a test notification" - ); - - return response.status; - }; - - /** - * Sends a webhook notification to a specified platform. - * - * @param {Object} networkResponse - The response object from the network. - * @param {Object} networkResponse.monitor - The monitor object. - * @param {boolean} networkResponse.status - The monitor's status (true for up, false for down). - * @param {Object} notification - The notification settings. - * @param {string} notification.platform - The target platform ("telegram", "slack", "discord"). - * @param {Object} notification.config - The configuration object for the webhook. - * @param {string} notification.config.webhookUrl - The webhook URL for the platform. - * @param {string} [notification.config.botToken] - The bot token for Telegram notifications. - * @param {string} [notification.config.chatId] - The chat ID for Telegram notifications. - * @returns {Promise} A promise that resolves to true if the notification was sent successfully, otherwise false. - */ - - async sendWebhookNotification(networkResponse, notification, alerts = []) { - const { monitor, status, code } = networkResponse; - const { webhookUrl, platform, botToken, chatId } = notification.config; - - // Early return if platform is not supported - if (!PLATFORM_TYPES.includes(platform)) { - this.logger.warn({ - message: this.stringService.getWebhookUnsupportedPlatform(platform), - service: this.SERVICE_NAME, - method: "sendWebhookNotification", - details: { platform }, - }); - return false; - } - - // Early return for telegram if required fields are missing - if (platform === "telegram" && (!botToken || !chatId)) { - this.logger.warn({ - message: "Missing required fields for Telegram notification", - service: this.SERVICE_NAME, - method: "sendWebhookNotification", - details: { platform }, - }); - return false; - } - - let url = webhookUrl; - if (platform === "telegram") { - url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`; - } - - let message; - if (monitor.type === "hardware") { - message = this.formatHardwareNotificationMessage( - monitor, - status, - platform, - chatId, - code, - networkResponse.timestamp, - alerts - ); - } else { - message = this.formatNotificationMessage( - monitor, - status, - platform, - chatId, - code, // Pass the code field directly - networkResponse.timestamp - ); - } - - try { - const response = await this.networkService.requestWebhook(platform, url, message); - return response.status; - } catch (error) { - this.logger.error({ - message: this.stringService.getWebhookSendError(platform), - service: this.SERVICE_NAME, - method: "sendWebhookNotification", - stack: error.stack, - }); - return false; - } - } - - /** - * Sends an email notification for hardware infrastructure alerts - * - * @async - * @function sendHardwareEmail - * @param {Object} networkResponse - Response object containing monitor information - * @param {string} address - Email address to send the notification to - * @param {Array} [alerts=[]] - List of hardware alerts to include in the email - * @returns {Promise} - Indicates whether email was sent successfully - * @throws {Error} - */ - async sendHardwareEmail(networkResponse, address, alerts = []) { - if (alerts.length === 0) return false; - const { monitor, status, prevStatus } = networkResponse; - const template = "hardwareIncidentTemplate"; - const context = { monitor: monitor.name, url: monitor.url, alerts }; - const subject = `Monitor ${monitor.name} infrastructure alerts`; - const html = await this.emailService.buildEmail(template, context); - this.emailService.sendEmail(address, subject, html).catch((error) => { - this.logger.warn({ - message: error.message, - service: this.SERVICE_NAME, - method: "sendHardwareEmail", - stack: error.stack, - }); - }); - - return true; - } - - sendTestEmail = async (notification) => { - const to = notification?.address; - if (!to || typeof to !== "string") { - throw new Error(this.stringService.errorForValidEmailAddress); - } - - const subject = this.stringService.testEmailSubject; - const context = { testName: "Monitoring System" }; - - try { - const html = await this.emailService.buildEmail("testEmailTemplate", context); - const messageId = await this.emailService.sendEmail(to, subject, html); - - if (messageId) { - return true; - } - } catch (error) { - this.logger.warn({ - message: error.message, - service: this.SERVICE_NAME, - method: "sendTestEmail", - stack: error.stack, - }); - return false; - } - }; - - /** - * Sends an email notification about monitor status change - * - * @async - * @function sendEmail - * @param {Object} networkResponse - Response object containing monitor status information - * @param {string} address - Email address to send the notification to - * @returns {Promise} - Indicates email was sent successfully - */ - - async sendEmail(networkResponse, address) { - const { monitor, status, prevStatus } = networkResponse; - const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate"; - const context = { monitor: monitor.name, url: monitor.url }; - const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`; - - const html = await this.emailService.buildEmail(template, context); - this.emailService.sendEmail(address, subject, html).catch((error) => { - this.logger.warn({ - message: error.message, - service: this.SERVICE_NAME, - method: "sendEmail", - stack: error.stack, - }); - }); - - return true; - } - - async sendTestPagerDutyNotification(notification) { - const { routingKey } = notification.config; - const response = await this.networkService.requestPagerDuty({ - message: "This is a test notification", - monitorUrl: "Test notification", - routingKey, - }); - - return response; - } - async sendPagerDutyNotification(networkResponse, notification, alerts = []) { - const { monitor, status, code } = networkResponse; - const { routingKey, platform } = notification.config; - - let message; - if (monitor.type === "hardware") { - message = this.formatHardwareNotificationMessage( - monitor, - status, - platform, - null, - code, - networkResponse.timestamp, - alerts - ); - } else { - message = this.formatNotificationMessage( - monitor, - status, - platform, - null, - code, // Pass the code field directly - networkResponse.timestamp - ); - } - try { - const response = await this.networkService.requestPagerDuty({ - message, - routingKey, - monitorUrl: monitor.url, - }); - return response; - } catch (error) { - this.logger.error({ - message: "Failed to send PagerDuty notification", - details: error.details, - service: error.service || this.SERVICE_NAME, - method: error.method || "sendPagerDutyNotification", - stack: error.stack, - }); - return false; - } - } - - async handleStatusNotifications(networkResponse) { - try { - // If status hasn't changed, we're done - if (networkResponse.statusChanged === false) return false; - // if prevStatus is undefined, monitor is resuming, we're done - if (networkResponse.prevStatus === undefined) return false; - - const notificationIDs = networkResponse.monitor?.notifications ?? []; - const notifications = await this.db.getNotificationsByIds(notificationIDs); - - for (const notification of notifications) { - if (notification.type === "email") { - await this.sendEmail(networkResponse, notification.address); - } else if (notification.type === "webhook") { - await this.sendWebhookNotification(networkResponse, notification); - } else if (notification.type === "pager_duty") { - await this.sendPagerDutyNotification(networkResponse, notification); - } - } + if (type === "email") { + const messageId = await this.emailService.sendEmail(address, subject, html); + if (!messageId) return false; return true; - } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "handleNotifications", - stack: error.stack, - }); } - } - /** - * Handles status change notifications for a monitor - * - * @async - * @function handleStatusNotifications - * @param {Object} networkResponse - Response object containing monitor status information - * @returns {Promise} - Indicates whether notifications were processed - * @throws {Error} - */ - async handleHardwareNotifications(networkResponse) { - const thresholds = networkResponse?.monitor?.thresholds; - if (thresholds === undefined) return false; // No thresholds set, we're done - // Get thresholds from monitor - const { - usage_cpu: cpuThreshold = -1, - usage_memory: memoryThreshold = -1, - usage_disk: diskThreshold = -1, - } = thresholds; + // Create a body for webhooks + let body = { text: content }; + if (type === "discord") { + body = { content }; + } - // Get metrics from response - const metrics = networkResponse?.payload?.data ?? null; - if (metrics === null) return false; + if (type === "slack" || type === "discord" || type === "webhook") { + const response = await this.networkService.requestWebhook(type, address, body); + return response.status; + } + if (type === "pager_duty") { + const response = await this.networkService.requestPagerDuty({ + message: content, + monitorUrl: subject, + routingKey: address, + }); - const { - cpu: { usage_percent: cpuUsage = -1 } = {}, - memory: { usage_percent: memoryUsage = -1 } = {}, - disk = [], - } = metrics; + return response; + } + }; - const alerts = { - cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, - memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, - disk: - disk?.some( - (d) => - diskThreshold !== -1 && - typeof d?.usage_percent === "number" && - d?.usage_percent > diskThreshold - ) ?? false, - }; + async handleNotifications(networkResponse) { + const { monitor, statusChanged, prevStatus } = networkResponse; + const { type } = monitor; + if (type !== "hardware" && statusChanged === false) return false; + // if prevStatus is undefined, monitor is resuming, we're done + if (type !== "hardware" && prevStatus === undefined) return false; const notificationIDs = networkResponse.monitor?.notifications ?? []; - const notifications = await this.db.getNotificationsByIds(notificationIDs); + if (notificationIDs.length === 0) return false; - for (const notification of notifications) { - const alertsToSend = []; - const alertTypes = ["cpu", "memory", "disk"]; + if (networkResponse.monitor.type === "hardware") { + const thresholds = networkResponse?.monitor?.thresholds; - for (const type of alertTypes) { - // Iterate over each alert type to see if any need to be decremented - if (alerts[type] === true) { - notification[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered + if (thresholds === undefined) return false; // No thresholds set, we're done + const metrics = networkResponse?.payload?.data ?? null; + if (metrics === null) return false; // No metrics, we're done - if (notification[`${type}AlertThreshold`] <= 0) { - // If threshold drops below 0, reset and send notification - notification[`${type}AlertThreshold`] = notification.alertThreshold; + const alerts = await this.notificationUtils.buildHardwareAlerts(networkResponse); + if (alerts.length === 0) return false; - const formatAlert = { - cpu: () => - `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, - memory: () => - `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, - disk: () => - `Your current disk usage: ${disk - .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`) - .join( - ", " - )} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, - }; - alertsToSend.push(formatAlert[type]()); - } - } - } + const { subject, html } = await this.notificationUtils.buildHardwareEmail( + networkResponse, + alerts + ); + const content = + await this.notificationUtils.buildHardwareNotificationMessage(alerts); - await notification.save(); - - if (alertsToSend.length === 0) continue; // No alerts to send, we're done - - if (notification.type === "email") { - await this.sendHardwareEmail(networkResponse, notification.address, alertsToSend); - } else if (notification.type === "webhook") { - await this.sendWebhookNotification(networkResponse, notification, alertsToSend); - } else if (notification.type === "pager_duty") { - await this.sendPagerDutyNotification(networkResponse, notification, alertsToSend); - } + const success = await this.notifyAll({ notificationIDs, subject, html, content }); + return success; } - return true; + + // Status monitors + const { subject, html } = + await this.notificationUtils.buildStatusEmail(networkResponse); + const content = await this.notificationUtils.buildWebhookMessage(networkResponse); + const success = this.notifyAll({ notificationIDs, subject, html, content }); + return success; } - /** - * Handles notifications for different monitor types - * - * @async - * @function handleNotifications - * @param {Object} networkResponse - Response object containing monitor information - * @returns {Promise} - Indicates whether notifications were processed successfully - */ - async handleNotifications(networkResponse) { - try { - if (networkResponse.monitor.type === "hardware") { - this.handleHardwareNotifications(networkResponse); - } - this.handleStatusNotifications(networkResponse); - return true; - } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "handleNotifications", - stack: error.stack, - }); - } - } - - async testAllNotifications(notificationIDs) { + async notifyAll({ notificationIDs, subject, html, content }) { const notifications = await this.db.getNotificationsByIds(notificationIDs); // Map each notification to a test promise - const testPromises = notifications.map(async (notification) => { + const promises = notifications.map(async (notification) => { try { - if (notification.type === "email") { - await this.sendTestEmail(notification); - } else if (notification.type === "webhook") { - await this.sendTestWebhookNotification(notification); - } else if (notification.type === "pager_duty") { - await this.sendTestPagerDutyNotification(notification); - } + await this.sendNotification({ notification, subject, content, html }); return true; } catch (err) { return false; } }); - const results = await Promise.all(testPromises); + const results = await Promise.all(promises); return results.every((r) => r === true); } + + async getTestNotification() { + const html = await this.notificationUtils.buildTestEmail(); + const content = "This is a test notification"; + const subject = "Test Notification"; + return { subject, html, content }; + } + + async testAllNotifications(notificationIDs) { + const { subject, html, content } = await this.getTestNotification(); + return this.notifyAll({ notificationIDs, subject, html, content }); + } + + async sendTestNotification(notification) { + const { subject, html, content } = await this.getTestNotification(); + const success = await this.sendNotification({ notification, subject, content, html }); + return success; + } } export default NotificationService; From 2b48a72f35b29e2008ced092bdbd01881f5a84eb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 19 Jun 2025 12:17:00 +0800 Subject: [PATCH 13/13] formatting --- client/src/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 3ae8c3d75..a7d9fe069 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -746,4 +746,4 @@ "settingsEmailPool": "Pool - Enable connection pooling", "sendTestNotifications": "Send test notifications", "selectAll": "Select all" -} \ No newline at end of file +}