diff --git a/.dockerignore b/.dockerignore index d439b2db5..825d58038 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,3 +11,4 @@ LICENSE README.md .editorconfig +.vscode diff --git a/.gitignore b/.gitignore index 8d435974f..9caa313cb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist-ssr /data !/data/.gitkeep +.vscode \ No newline at end of file diff --git a/db/kuma.db b/db/kuma.db index 07c93cf89..6e02ccc01 100644 Binary files a/db/kuma.db and b/db/kuma.db differ diff --git a/db/patch3.sql b/db/patch3.sql new file mode 100644 index 000000000..e615632f9 --- /dev/null +++ b/db/patch3.sql @@ -0,0 +1,37 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +-- Add maxretries column to monitor +PRAGMA foreign_keys=off; + +BEGIN TRANSACTION; + +create table monitor_dg_tmp +( + id INTEGER not null + primary key autoincrement, + name VARCHAR(150), + active BOOLEAN default 1 not null, + user_id INTEGER + references user + on update cascade on delete set null, + interval INTEGER default 20 not null, + url TEXT, + type VARCHAR(20), + weight INTEGER default 2000, + hostname VARCHAR(255), + port INTEGER, + created_date DATETIME, + keyword VARCHAR(255), + maxretries INTEGER NOT NULL DEFAULT 0 +); + +insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor; + +drop table monitor; + +alter table monitor_dg_tmp rename to monitor; + +create index user_id on monitor (user_id); + +COMMIT; + +PRAGMA foreign_keys=on; diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index 3bb82e4c9..0e9c109f3 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/server/model/monitor.js b/server/model/monitor.js index 49cc0f0e9..d855ae383 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -6,6 +6,7 @@ var timezone = require('dayjs/plugin/timezone') dayjs.extend(utc) dayjs.extend(timezone) const axios = require("axios"); +const {UP, DOWN, PENDING} = require("../util"); const {debug} = require("../util"); const {tcping, ping, checkCertificate} = require("../util-server"); const {R} = require("redbean-node"); @@ -24,7 +25,6 @@ const customAgent = new https.Agent({ * 1 = UP */ class Monitor extends BeanModel { - async toJSON() { let notificationIDList = {}; @@ -43,6 +43,7 @@ class Monitor extends BeanModel { url: this.url, hostname: this.hostname, port: this.port, + maxretries: this.maxretries, weight: this.weight, active: this.active, type: this.type, @@ -54,21 +55,25 @@ class Monitor extends BeanModel { start(io) { let previousBeat = null; + let retries = 0; const beat = async () => { + if (! previousBeat) { previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ this.id ]) } + const isFirstBeat = !previousBeat; + let bean = R.dispense("heartbeat") bean.monitor_id = this.id; bean.time = R.isoDateTime(dayjs.utc()); - bean.status = 0; + bean.status = DOWN; // Duration - if (previousBeat) { + if (! isFirstBeat) { bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), 'second'); } else { bean.duration = 0; @@ -98,7 +103,7 @@ class Monitor extends BeanModel { debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms") if (this.type === "http") { - bean.status = 1; + bean.status = UP; } else { let data = res.data; @@ -110,7 +115,7 @@ class Monitor extends BeanModel { if (data.includes(this.keyword)) { bean.msg += ", keyword is found" - bean.status = 1; + bean.status = UP; } else { throw new Error(bean.msg + ", but keyword is not found") } @@ -121,30 +126,52 @@ class Monitor extends BeanModel { } else if (this.type === "port") { bean.ping = await tcping(this.hostname, this.port); bean.msg = "" - bean.status = 1; + bean.status = UP; } else if (this.type === "ping") { bean.ping = await ping(this.hostname); bean.msg = "" - bean.status = 1; + bean.status = UP; } + retries = 0; + } catch (error) { + if ((this.maxretries > 0) && (retries < this.maxretries)) { + retries++; + bean.status = PENDING; + } bean.msg = error.message; } - // Mark as important if status changed - if (! previousBeat || previousBeat.status !== bean.status) { + // * ? -> 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 + // DOWN -> PENDING = this case not exists + // DOWN -> DOWN = not important + // * DOWN -> UP = important + let isImportant = isFirstBeat || + (previousBeat.status === UP && bean.status === DOWN) || + (previousBeat.status === DOWN && bean.status === UP) || + (previousBeat.status === PENDING && bean.status === DOWN); + + // Mark as important if status changed, ignore pending pings, + // Don't notify if disrupted changes to up + if (isImportant) { bean.important = true; - // Do not send if first beat is UP - if (previousBeat || bean.status !== 1) { + // Send only if the first beat is DOWN + if (!isFirstBeat || bean.status === DOWN) { let notificationList = await R.getAll(`SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id `, [ this.id ]) let text; - if (bean.status === 1) { + if (bean.status === UP) { text = "✅ Up" } else { text = "🔴 Down" @@ -165,8 +192,10 @@ class Monitor extends BeanModel { bean.important = false; } - if (bean.status === 1) { + if (bean.status === UP) { console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`) + } else if (bean.status === PENDING) { + console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Type: ${this.type}`) } else { console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`) } @@ -293,7 +322,7 @@ class Monitor extends BeanModel { } total += value; - if (row.status === 0) { + if (row.status === 0 || row.status === 2) { downtime += value; } } diff --git a/server/notification.js b/server/notification.js index 3c0e9f2a4..9da8a0dc8 100644 --- a/server/notification.js +++ b/server/notification.js @@ -204,7 +204,7 @@ class Notification { } let data = { - "message": "Uptime Kuma Alert\n\nMessage:" +msg + '\nTime (UTC):' +time, + "message": "Uptime Kuma Alert\n\nMessage:"+msg+ '\nTime (UTC):' +heartbeatJSON["time"], "user":notification.pushoveruserkey, "token": notification.pushoverapptoken, "sound": notification.pushoversounds, diff --git a/server/server.js b/server/server.js index 3c4d00295..6bf48dc13 100644 --- a/server/server.js +++ b/server/server.js @@ -233,6 +233,7 @@ let needSetup = false; bean.url = monitor.url bean.interval = monitor.interval bean.hostname = monitor.hostname; + bean.maxretries = monitor.maxretries; bean.port = monitor.port; bean.keyword = monitor.keyword; diff --git a/server/util.js b/server/util.js index 0282a0276..081561bf9 100644 --- a/server/util.js +++ b/server/util.js @@ -1,6 +1,10 @@ // Common JS cannot be used in frontend sadly // sleep, ucfirst is duplicated in ../src/util-frontend.js +exports.DOWN = 0; +exports.UP = 1; +exports.PENDING = 2; + exports.sleep = function (ms) { return new Promise(resolve => setTimeout(resolve, ms)); } diff --git a/src/assets/vars.scss b/src/assets/vars.scss index 31b0262d8..ebec378a5 100644 --- a/src/assets/vars.scss +++ b/src/assets/vars.scss @@ -1,7 +1,8 @@ $primary: #5CDD8B; $danger: #DC3545; +$warning: #f8a306; $link-color: #111; $border-radius: 50rem; $highlight: #7ce8a4; -$highlight-white: #e7faec; +$highlight-white: #e7faec; \ No newline at end of file diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue index 48ffd2926..03cdceca6 100644 --- a/src/components/HeartbeatBar.vue +++ b/src/components/HeartbeatBar.vue @@ -3,7 +3,7 @@
diff --git a/src/components/Status.vue b/src/components/Status.vue index c5fec2241..1eaf17f2d 100644 --- a/src/components/Status.vue +++ b/src/components/Status.vue @@ -14,6 +14,8 @@ export default { return "danger" } else if (this.status === 1) { return "primary" + } else if (this.status === 2) { + return "warning" } else { return "secondary" } @@ -24,6 +26,8 @@ export default { return "Down" } else if (this.status === 1) { return "Up" + } else if (this.status === 2) { + return "Pending" } else { return "Unknown" } @@ -34,6 +38,6 @@ export default { diff --git a/src/components/Uptime.vue b/src/components/Uptime.vue index ad8114fcb..322b35f70 100644 --- a/src/components/Uptime.vue +++ b/src/components/Uptime.vue @@ -30,6 +30,8 @@ export default { return "danger" } else if (this.lastHeartBeat.status === 1) { return "primary" + } else if (this.lastHeartBeat.status === 2) { + return "warning" } else { return "secondary" } diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 26a4611ed..f36a770e3 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -294,6 +294,11 @@ export default { text: "Down", color: "danger" }; + } else if (lastHeartBeat.status === 2) { + result[monitorID] = { + text: "Pending", + color: "warning" + }; } else { result[monitorID] = unknown; } diff --git a/src/pages/DashboardHome.vue b/src/pages/DashboardHome.vue index 4c6c82e2b..1fa784ff0 100644 --- a/src/pages/DashboardHome.vue +++ b/src/pages/DashboardHome.vue @@ -110,6 +110,8 @@ export default { result.up++; } else if (beat.status === 0) { result.down++; + } else if (beat.status === 2) { + result.up++; } else { result.unknown++; } diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 7a72cd139..75d7d4b9b 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -48,6 +48,12 @@ +Not available, please setup.
-Not available, please setup.
-Please assign the notification to monitor(s) to get it works.
+Please assign a notification to monitor(s) to get it to work.