diff --git a/db/knex_migrations/2025-04-10-0000-feat-notification-trigger-type.js b/db/knex_migrations/2025-04-10-0000-feat-notification-trigger-type.js new file mode 100644 index 000000000..009949582 --- /dev/null +++ b/db/knex_migrations/2025-04-10-0000-feat-notification-trigger-type.js @@ -0,0 +1,27 @@ +exports.up = function (knex) { + return knex.schema + .alterTable("monitor_notification", function (table) { + table.text("trigger").notNullable().defaultTo("always"); + }) + .alterTable("notification", function (table) { + table.text("default_trigger").notNullable().defaultTo("always"); + }) + .then(() => { + knex("monitor_notification").whereNull("trigger").update({ + trigger: "always", + }); + knex("notification").whereNull("default_trigger").update({ + default_trigger: "always", + }); + }); +}; + +exports.down = function (knex) { + return knex.schema + .alterTable("monitor_notification", function (table) { + table.dropColumn("trigger"); + }) + .alterTable("notification", function (table) { + table.dropColumn("default_trigger"); + }); +}; diff --git a/server/model/monitor.js b/server/model/monitor.js index 5999d93e7..d31a3930b 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1340,7 +1340,14 @@ class Monitor extends BeanModel { heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset(); heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT); - await Notification.send(JSON.parse(notification.config), msg, monitor.toJSON(preloadData, false), heartbeatJSON); + if ( + notification.trigger === "always" || + notification.trigger === "up_down" || + ((notification.trigger === "up" || notification.trigger === "up_certificate") && bean.status === UP) || + ((notification.trigger === "down" || notification.trigger === "down_certificate") && bean.status === DOWN) + ) { + await Notification.send(JSON.parse(notification.config), msg, monitor.toJSON(preloadData, false), heartbeatJSON); + } } catch (e) { log.error("monitor", "Cannot send notification to " + notification.name); log.error("monitor", e); @@ -1355,7 +1362,7 @@ class Monitor extends BeanModel { * @returns {Promise[]>} List of notifications */ static async getNotificationList(monitor) { - let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [ + let notificationList = await R.getAll("SELECT notification.*, monitor_notification.trigger FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [ monitor.id, ]); return notificationList; @@ -1432,6 +1439,9 @@ class Monitor extends BeanModel { log.debug("monitor", "Send certificate notification"); for (let notification of notificationList) { + if (notification.trigger !== "always" && notification.trigger !== "certificate" && notification.trigger !== "up_certificate" && notification.trigger !== "down_certificate") { + continue; + } try { log.debug("monitor", "Sending to " + notification.name); await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] ${certType} certificate ${certCN} will expire in ${daysRemaining} days`); @@ -1509,7 +1519,7 @@ class Monitor extends BeanModel { */ static async getMonitorNotification(monitorIDs) { return await R.getAll(` - SELECT monitor_notification.monitor_id, monitor_notification.notification_id + SELECT monitor_notification.monitor_id, monitor_notification.notification_id, monitor_notification.trigger FROM monitor_notification WHERE monitor_notification.monitor_id IN (${monitorIDs.map((_) => "?").join(",")}) `, monitorIDs); @@ -1558,7 +1568,10 @@ class Monitor extends BeanModel { if (!notificationsMap.has(row.monitor_id)) { notificationsMap.set(row.monitor_id, {}); } - notificationsMap.get(row.monitor_id)[row.notification_id] = true; + notificationsMap.get(row.monitor_id)[row.notification_id] = { + active: true, + trigger: row.trigger, + }; }); tags.forEach(row => { diff --git a/server/notification.js b/server/notification.js index 0c222d932..5d90ccf2d 100644 --- a/server/notification.js +++ b/server/notification.js @@ -219,6 +219,7 @@ class Notification { bean.user_id = userID; bean.config = JSON.stringify(notification); bean.is_default = notification.isDefault || false; + bean.default_trigger = notification.defaultTrigger; await R.store(bean); if (notification.applyExisting) { @@ -271,15 +272,20 @@ async function applyNotificationEveryMonitor(notificationID, userID) { ]); for (let i = 0; i < monitors.length; i++) { - let checkNotification = await R.findOne("monitor_notification", " monitor_id = ? AND notification_id = ? ", [ + let checkNotification = await R.getRow(` + SELECT n.default_trigger FROM notification AS n + LEFT JOIN monitor_notification mn on n.id = mn.notification_id AND mn.monitor_id = ? AND mn.notification_id = ? + WHERE mn.monitor_id IS NULL + `, [ monitors[i].id, notificationID, ]); - if (! checkNotification) { + if (checkNotification) { let relation = R.dispense("monitor_notification"); relation.monitor_id = monitors[i].id; relation.notification_id = notificationID; + relation.trigger = checkNotification.default_trigger; await R.store(relation); } } diff --git a/server/server.js b/server/server.js index ec5ad49f6..e1448eb4c 100644 --- a/server/server.js +++ b/server/server.js @@ -1426,6 +1426,7 @@ let needSetup = false; ok: true, msg: "Saved.", msgi18n: true, + defaultTrigger: notificationBean.default_trigger, id: notificationBean.id, }); @@ -1636,10 +1637,11 @@ async function updateMonitorNotification(monitorID, notificationIDList) { ]); for (let notificationID in notificationIDList) { - if (notificationIDList[notificationID]) { + if (notificationIDList[notificationID].active) { let relation = R.dispense("monitor_notification"); relation.monitor_id = monitorID; relation.notification_id = notificationID; + relation.trigger = notificationIDList[notificationID].trigger; await R.store(relation); } } diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue index 56cae66c8..6268978fc 100644 --- a/src/components/NotificationDialog.vue +++ b/src/components/NotificationDialog.vue @@ -41,6 +41,35 @@
+ +
+ {{ $t("enableDefaultTriggerNotificationDescription") }} +
+ +
+
@@ -95,6 +124,7 @@ export default { /** @type { null | keyof NotificationFormList } */ type: null, isDefault: false, + defaultTrigger: "always", // Do not set default value here, please scroll to show() } }; @@ -270,6 +300,7 @@ export default { name: "", type: "telegram", isDefault: false, + defaultTrigger: "always", }; } @@ -291,7 +322,7 @@ export default { // Emit added event, doesn't emit edit. if (! this.id) { - this.$emit("added", res.id); + this.$emit("added", res.id, res.defaultTrigger); } } diff --git a/src/lang/en.json b/src/lang/en.json index cb704b0fe..365e34243 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1067,5 +1067,14 @@ "YZJ Robot Token": "YZJ Robot token", "Plain Text": "Plain Text", "Message Template": "Message Template", - "Template Format": "Template Format" + "Template Format": "Template Format", + "notificationTriggerAlways": "Notify on up, down state or certificate expiry", + "notificationTriggerUpDown": "Notify on up or down state", + "notificationTriggerUp": "Notify on up state only", + "notificationTriggerDown": "Notify on down state only", + "notificationTriggerUpCertificate": "Notify on up state or certificate expiry", + "notificationTriggerDownCertificate": "Notify on down state or certificate expiry", + "notificationTriggerCertificate": "Notify on certificate expiry only", + "notificationTriggerCertificateWarning": "Certificate expiry notifications are disabled for this monitor. You will not receive alerts when the certificate is about to expire.", + "enableDefaultTriggerNotificationDescription": "This notification trigger will be the default for new monitors. You can still edit the notification trigger separately for each monitor." } diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index a83f91cab..6e36745a7 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -733,17 +733,49 @@ {{ $t("Not available, please setup.") }}

-
- +
+
+ - + - {{ $t("Default") }} + {{ $t("Default") }} +
+ + +
+
- @@ -1576,9 +1608,11 @@ message HealthCheckResponse { } for (let i = 0; i < this.$root.notificationList.length; i++) { - if (this.$root.notificationList[i].isDefault === true) { - this.monitor.notificationIDList[this.$root.notificationList[i].id] = true; - } + let notification = this.$root.notificationList[i]; + this.monitor.notificationIDList[notification.id] = { + active: notification.isDefault === true, + trigger: notification.defaultTrigger, + }; } } else if (this.isEdit || this.isClone) { this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => { @@ -1593,6 +1627,16 @@ message HealthCheckResponse { this.monitor = res.monitor; + for (let i = 0; i < this.$root.notificationList.length; i++) { + let notification = this.$root.notificationList[i]; + if (!this.monitor.notificationIDList[notification.id]) { + this.monitor.notificationIDList[notification.id] = { + active: notification.isDefault === true, + trigger: notification.defaultTrigger, + }; + } + } + if (this.isClone) { /* * Cloning a monitor will include properties that can not be posted to backend @@ -1794,10 +1838,14 @@ message HealthCheckResponse { * Added a Notification Event * Enable it if the notification is added in EditMonitor.vue * @param {number} id ID of notification to add + * @param {string} defaultTrigger default notification trigger (both, up, down, certificate, up_certificate, down_certificate) * @returns {void} */ - addedNotification(id) { - this.monitor.notificationIDList[id] = true; + addedNotification(id, defaultTrigger) { + this.monitor.notificationIDList[id] = { + active: true, + trigger: defaultTrigger, + }; }, /**