From d01cf6e3759e6d1195c96859a378cfc6fd477394 Mon Sep 17 00:00:00 2001 From: saber04414 Date: Tue, 13 Jan 2026 11:45:12 +0200 Subject: [PATCH] fix: Send webhook notification when monitor transitions from PENDING to UP after being DOWN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #6025 When a monitor transitions DOWN → PENDING → UP, the UP webhook notification was not being sent because PENDING → UP transitions were marked as 'not important' for notifications. This fix: - Adds getLastNonPendingStatus() helper to check if monitor was DOWN before PENDING - Modifies isImportantForNotification() to check history when PENDING → UP transition occurs - If monitor was DOWN before PENDING, PENDING → UP is now treated as important for notifications - Maintains backward compatibility: UP → PENDING → UP still doesn't trigger notifications Tested with Docker container monitor and HTTP monitor scenarios. --- server/model/monitor.js | 39 ++++++++++++++++++++++++++++++++---- server/routers/api-router.js | 2 +- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index 01d655f23..0dd5a6497 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1009,7 +1009,7 @@ class Monitor extends BeanModel { if (isImportant) { bean.important = true; - if (Monitor.isImportantForNotification(isFirstBeat, previousBeat?.status, bean.status)) { + if (await Monitor.isImportantForNotification(isFirstBeat, previousBeat?.status, bean.status, this.id)) { log.debug("monitor", `[${this.name}] sendNotification`); await Monitor.sendNotification(isFirstBeat, this, bean); } else { @@ -1450,16 +1450,17 @@ class Monitor extends BeanModel { * @param {boolean} isFirstBeat Is this the first beat of this monitor? * @param {const} previousBeatStatus Status of the previous beat * @param {const} currentBeatStatus Status of the current beat - * @returns {boolean} True if is an important beat else false + * @param {number} [monitorID] Optional monitor ID to check history for PENDING->UP transitions + * @returns {Promise} True if is an important beat else false */ - static isImportantForNotification(isFirstBeat, previousBeatStatus, currentBeatStatus) { + static async isImportantForNotification(isFirstBeat, previousBeatStatus, currentBeatStatus, monitorID = null) { // * ? -> ANY STATUS = important [isFirstBeat] // UP -> PENDING = not important // * UP -> DOWN = important // UP -> UP = not important // PENDING -> PENDING = not important // * PENDING -> DOWN = important - // PENDING -> UP = not important + // PENDING -> UP = important if monitor was DOWN before PENDING (fix for issue #6025) // DOWN -> PENDING = this case not exists // DOWN -> DOWN = not important // * DOWN -> UP = important @@ -1468,6 +1469,21 @@ class Monitor extends BeanModel { // * MAINTENANCE -> DOWN = important // DOWN -> MAINTENANCE = not important // UP -> MAINTENANCE = not important + + // Check for PENDING -> UP transition + if (previousBeatStatus === PENDING && currentBeatStatus === UP) { + // If monitorID is provided, check if the monitor was DOWN before entering PENDING + if (monitorID) { + const lastNonPendingStatus = await Monitor.getLastNonPendingStatus(monitorID); + // If the last non-PENDING status was DOWN, this transition is important + if (lastNonPendingStatus === DOWN) { + return true; + } + } + // Otherwise, PENDING -> UP is not important (original behavior) + return false; + } + return ( isFirstBeat || (previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) || @@ -1657,6 +1673,21 @@ class Monitor extends BeanModel { return await R.findOne("heartbeat", " id = (select MAX(id) from heartbeat where monitor_id = ?)", [monitorID]); } + /** + * Get the last non-PENDING heartbeat status for a monitor + * This is useful to determine if a monitor was DOWN before entering PENDING state + * @param {number} monitorID ID of monitor to check + * @returns {Promise} Last non-PENDING status or null if not found + */ + static async getLastNonPendingStatus(monitorID) { + const heartbeat = await R.findOne( + "heartbeat", + " monitor_id = ? AND status != ? ORDER BY time DESC LIMIT 1", + [monitorID, PENDING] + ); + return heartbeat?.status || null; + } + /** * Check if monitor is under maintenance * @param {number} monitorID ID of monitor to check diff --git a/server/routers/api-router.js b/server/routers/api-router.js index 05c953756..c2a9bc7dc 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -99,7 +99,7 @@ router.all("/api/push/:pushToken", async (request, response) => { bean.important = Monitor.isImportantBeat(isFirstBeat, previousHeartbeat?.status, bean.status); - if (Monitor.isImportantForNotification(isFirstBeat, previousHeartbeat?.status, bean.status)) { + if (await Monitor.isImportantForNotification(isFirstBeat, previousHeartbeat?.status, bean.status, monitor.id)) { // Reset down count bean.downCount = 0;