From a0770fc073d8aad7e3426930fa6699e164c291b1 Mon Sep 17 00:00:00 2001 From: dvdandroid <6277172+DVDAndroid@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:30:59 +0200 Subject: [PATCH 1/6] feat: notification trigger options for up or down status --- ...-10-0000-feat-notification-trigger-type.js | 27 +++++++++ server/model/monitor.js | 17 ++++-- server/notification.js | 10 +++- server/server.js | 4 +- src/components/NotificationDialog.vue | 21 ++++++- src/lang/en.json | 6 +- src/pages/EditMonitor.vue | 56 ++++++++++++++----- 7 files changed, 119 insertions(+), 22 deletions(-) create mode 100644 db/knex_migrations/2025-04-10-0000-feat-notification-trigger-type.js 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..d3642edcb --- /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("type").notNullable().defaultTo("both"); + }) + .alterTable("notification", function (table) { + table.text("default_type").notNullable().defaultTo("both"); + }) + .then(() => { + knex("monitor_notification").whereNull("type").update({ + type: "both", + }); + knex("notification").whereNull("default_type").update({ + default_type: "both", + }); + }); +}; + +exports.down = function (knex) { + return knex.schema + .alterTable("monitor_notification", function (table) { + table.dropColumn("type"); + }) + .alterTable("notification", function (table) { + table.dropColumn("default_type"); + }); +}; diff --git a/server/model/monitor.js b/server/model/monitor.js index 5999d93e7..ac97e87cc 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1340,7 +1340,13 @@ 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.type === "both" || + (notification.type === "up" && bean.status === UP) || + (notification.type === "down" && 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 +1361,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.type FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [ monitor.id, ]); return notificationList; @@ -1509,7 +1515,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.type FROM monitor_notification WHERE monitor_notification.monitor_id IN (${monitorIDs.map((_) => "?").join(",")}) `, monitorIDs); @@ -1558,7 +1564,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, + type: row.type, + }; }); tags.forEach(row => { diff --git a/server/notification.js b/server/notification.js index 0c222d932..2d4294f14 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_type = notification.defaultType; 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_type 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.type = checkNotification.default_type; await R.store(relation); } } diff --git a/server/server.js b/server/server.js index ec5ad49f6..6bee2af22 100644 --- a/server/server.js +++ b/server/server.js @@ -1426,6 +1426,7 @@ let needSetup = false; ok: true, msg: "Saved.", msgi18n: true, + defaultType: notificationBean.default_type, 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.type = notificationIDList[notificationID].type; await R.store(relation); } } diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue index 56cae66c8..fd20e27f1 100644 --- a/src/components/NotificationDialog.vue +++ b/src/components/NotificationDialog.vue @@ -41,6 +41,23 @@
+ +
+ {{ $t("enableDefaultTypeNotificationDescription") }} +
+ +
+
@@ -95,6 +112,7 @@ export default { /** @type { null | keyof NotificationFormList } */ type: null, isDefault: false, + defaultType: "both", // Do not set default value here, please scroll to show() } }; @@ -270,6 +288,7 @@ export default { name: "", type: "telegram", isDefault: false, + defaultType: "both", }; } @@ -291,7 +310,7 @@ export default { // Emit added event, doesn't emit edit. if (! this.id) { - this.$emit("added", res.id); + this.$emit("added", res.id, res.defaultType); } } diff --git a/src/lang/en.json b/src/lang/en.json index cb704b0fe..6b9a1f61f 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1067,5 +1067,9 @@ "YZJ Robot Token": "YZJ Robot token", "Plain Text": "Plain Text", "Message Template": "Message Template", - "Template Format": "Template Format" + "Template Format": "Template Format", + "notificationTypeBoth": "Notify on up and down state", + "notificationTypeUp": "Notify on up state", + "notificationTypeDown": "Notify on down state", + "enableDefaultTypeNotificationDescription": "This notification type will be the default for new monitors. You can still edit the notification type separately for each monitor." } diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index a83f91cab..de03a2762 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -733,17 +733,31 @@ {{ $t("Not available, please setup.") }}

-
- +
+
+ - + - {{ $t("Default") }} + {{ $t("Default") }} +
+ +
+
- @@ -1576,9 +1590,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, + type: notification.defaultType, + }; } } else if (this.isEdit || this.isClone) { this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => { @@ -1593,6 +1609,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, + type: notification.defaultType, + }; + } + } + if (this.isClone) { /* * Cloning a monitor will include properties that can not be posted to backend @@ -1794,10 +1820,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} defaultType default notification type (both, up, down) * @returns {void} */ - addedNotification(id) { - this.monitor.notificationIDList[id] = true; + addedNotification(id, defaultType) { + this.monitor.notificationIDList[id] = { + active: true, + type: defaultType, + }; }, /** From 5794739c711f53f1464da1f50d6d0b301eec0a11 Mon Sep 17 00:00:00 2001 From: dvdandroid <6277172+DVDAndroid@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:33:16 +0200 Subject: [PATCH 2/6] feat: notification trigger for certificate expiry only --- ...-04-10-0000-feat-notification-trigger-type.js | 8 ++++---- server/model/monitor.js | 6 +++++- src/components/NotificationDialog.vue | 14 ++++++++++---- src/lang/en.json | 5 ++++- src/pages/EditMonitor.vue | 16 ++++++++++++++-- 5 files changed, 37 insertions(+), 12 deletions(-) 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 index d3642edcb..a6be4327d 100644 --- 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 @@ -1,17 +1,17 @@ exports.up = function (knex) { return knex.schema .alterTable("monitor_notification", function (table) { - table.text("type").notNullable().defaultTo("both"); + table.text("type").notNullable().defaultTo("always"); }) .alterTable("notification", function (table) { - table.text("default_type").notNullable().defaultTo("both"); + table.text("default_type").notNullable().defaultTo("always"); }) .then(() => { knex("monitor_notification").whereNull("type").update({ - type: "both", + type: "always", }); knex("notification").whereNull("default_type").update({ - default_type: "both", + default_type: "always", }); }); }; diff --git a/server/model/monitor.js b/server/model/monitor.js index ac97e87cc..2fcd0f3dc 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1341,7 +1341,8 @@ class Monitor extends BeanModel { heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT); if ( - notification.type === "both" || + notification.type === "always" || + notification.type === "up_down" || (notification.type === "up" && bean.status === UP) || (notification.type === "down" && bean.status === DOWN) ) { @@ -1438,6 +1439,9 @@ class Monitor extends BeanModel { log.debug("monitor", "Send certificate notification"); for (let notification of notificationList) { + if (notification.type !== "always" && notification.type !== "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`); diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue index fd20e27f1..a9de6b9b5 100644 --- a/src/components/NotificationDialog.vue +++ b/src/components/NotificationDialog.vue @@ -42,8 +42,11 @@
{{ $t("enableDefaultTypeNotificationDescription") }} @@ -112,7 +118,7 @@ export default { /** @type { null | keyof NotificationFormList } */ type: null, isDefault: false, - defaultType: "both", + defaultType: "always", // Do not set default value here, please scroll to show() } }; @@ -288,7 +294,7 @@ export default { name: "", type: "telegram", isDefault: false, - defaultType: "both", + defaultType: "always", }; } diff --git a/src/lang/en.json b/src/lang/en.json index 6b9a1f61f..53553a2ee 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1068,8 +1068,11 @@ "Plain Text": "Plain Text", "Message Template": "Message Template", "Template Format": "Template Format", - "notificationTypeBoth": "Notify on up and down state", + "notificationTypeAlways": "Notify on up, down state or certificate expiry", + "notificationTypeUpDown": "Notify on up or down state", "notificationTypeUp": "Notify on up state", "notificationTypeDown": "Notify on down state", + "notificationTypeCertificate": "Notify on certificate expiry only", + "notificationTypeCertificateWarning": "Certificate expiry notification is disabled on this monitor. You will not receive any notifications", "enableDefaultTypeNotificationDescription": "This notification type will be the default for new monitors. You can still edit the notification type separately for each monitor." } diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index de03a2762..46e89a155 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -745,8 +745,11 @@ {{ $t("Default") }}
+
@@ -1611,7 +1611,7 @@ message HealthCheckResponse { let notification = this.$root.notificationList[i]; this.monitor.notificationIDList[notification.id] = { active: notification.isDefault === true, - type: notification.defaultType, + trigger: notification.defaultTrigger, }; } } else if (this.isEdit || this.isClone) { @@ -1632,7 +1632,7 @@ message HealthCheckResponse { if (!this.monitor.notificationIDList[notification.id]) { this.monitor.notificationIDList[notification.id] = { active: notification.isDefault === true, - type: notification.defaultType, + trigger: notification.defaultTrigger, }; } } @@ -1838,13 +1838,13 @@ 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} defaultType default notification type (both, up, down) + * @param {string} defaultTrigger default notification trigger (both, up, down, certificate, up_certificate, down_certificate) * @returns {void} */ - addedNotification(id, defaultType) { + addedNotification(id, defaultTrigger) { this.monitor.notificationIDList[id] = { active: true, - type: defaultType, + trigger: defaultTrigger, }; }, From 6a320127424183f6b1aa5fb720fc334f92e48926 Mon Sep 17 00:00:00 2001 From: dvdandroid <6277172+DVDAndroid@users.noreply.github.com> Date: Sat, 12 Apr 2025 12:36:14 +0200 Subject: [PATCH 5/6] fix notification certificate trigger alert visibility --- src/pages/EditMonitor.vue | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 3998f6763..6e36745a7 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -767,12 +767,12 @@ {{ $t("notificationTriggerDownCertificate") }} - - From e191d1329570dfc2600252e740212f0ffc3e3342 Mon Sep 17 00:00:00 2001 From: dvdandroid <6277172+DVDAndroid@users.noreply.github.com> Date: Sat, 12 Apr 2025 14:23:03 +0200 Subject: [PATCH 6/6] notification trigger better messages --- src/lang/en.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/en.json b/src/lang/en.json index 1e8f7a4e0..365e34243 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1070,10 +1070,10 @@ "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", - "notificationTriggerDown": "Notify on down state", - "notificationTriggerUpCertificate": "Notify on up state and certificate expiry", - "notificationTriggerDownCertificate": "Notify on down state and certificate expiry", + "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."