mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-10-31 19:39:20 +08:00 
			
		
		
		
	Merge branch 'master' into feature/add_prometheus_metrics
# Conflicts: # server/model/monitor.js
This commit is contained in:
		| @@ -11,3 +11,4 @@ | ||||
| LICENSE | ||||
| README.md | ||||
| .editorconfig | ||||
| .vscode | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -7,3 +7,4 @@ dist-ssr | ||||
|  | ||||
| /data | ||||
| !/data/.gitkeep | ||||
| .vscode | ||||
| @@ -15,7 +15,7 @@ It is a self-hosted monitoring tool like "Uptime Robot". | ||||
|  | ||||
| * Monitoring uptime for HTTP(s) / TCP / Ping. | ||||
| * Fancy, Reactive, Fast UI/UX. | ||||
| * Notifications via Webhook, Telegram, Discord and email (SMTP).  | ||||
| * Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise.  | ||||
| * 20 seconds interval. | ||||
|  | ||||
| # How to Use | ||||
| @@ -80,7 +80,7 @@ PS: For every new release, it takes some time to build the docker image, please | ||||
|  | ||||
| ```bash | ||||
| git fetch --all | ||||
| git checkout 1.0.6 --force | ||||
| git checkout 1.0.7 --force | ||||
| npm install | ||||
| npm run build | ||||
| pm2 restart uptime-kuma | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								db/kuma.db
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								db/kuma.db
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										9
									
								
								db/patch2.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								db/patch2.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| BEGIN TRANSACTION; | ||||
|  | ||||
| CREATE TABLE monitor_tls_info ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | ||||
| 	monitor_id INTEGER NOT NULL, | ||||
| 	info_json TEXT | ||||
| ); | ||||
|  | ||||
| COMMIT; | ||||
							
								
								
									
										37
									
								
								db/patch3.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								db/patch3.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
							
								
								
									
										36
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										36
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "uptime-kuma", | ||||
|   "version": "1.0.6", | ||||
|   "version": "1.0.7", | ||||
|   "lockfileVersion": 1, | ||||
|   "requires": true, | ||||
|   "dependencies": { | ||||
| @@ -29,6 +29,40 @@ | ||||
|         "to-fast-properties": "^2.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "@fortawesome/fontawesome-common-types": { | ||||
|       "version": "0.2.35", | ||||
|       "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.35.tgz", | ||||
|       "integrity": "sha512-IHUfxSEDS9dDGqYwIW7wTN6tn/O8E0n5PcAHz9cAaBoZw6UpG20IG/YM3NNLaGPwPqgjBAFjIURzqoQs3rrtuw==" | ||||
|     }, | ||||
|     "@fortawesome/fontawesome-svg-core": { | ||||
|       "version": "1.2.35", | ||||
|       "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.35.tgz", | ||||
|       "integrity": "sha512-uLEXifXIL7hnh2sNZQrIJWNol7cTVIzwI+4qcBIq9QWaZqUblm0IDrtSqbNg+3SQf8SMGHkiSigD++rHmCHjBg==", | ||||
|       "requires": { | ||||
|         "@fortawesome/fontawesome-common-types": "^0.2.35" | ||||
|       } | ||||
|     }, | ||||
|     "@fortawesome/free-regular-svg-icons": { | ||||
|       "version": "5.15.3", | ||||
|       "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.3.tgz", | ||||
|       "integrity": "sha512-q4/p8Xehy9qiVTdDWHL4Z+o5PCLRChePGZRTXkl+/Z7erDVL8VcZUuqzJjs6gUz6czss4VIPBRdCz6wP37/zMQ==", | ||||
|       "requires": { | ||||
|         "@fortawesome/fontawesome-common-types": "^0.2.35" | ||||
|       } | ||||
|     }, | ||||
|     "@fortawesome/free-solid-svg-icons": { | ||||
|       "version": "5.15.3", | ||||
|       "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.3.tgz", | ||||
|       "integrity": "sha512-XPeeu1IlGYqz4VWGRAT5ukNMd4VHUEEJ7ysZ7pSSgaEtNvSo+FLurybGJVmiqkQdK50OkSja2bfZXOeyMGRD8Q==", | ||||
|       "requires": { | ||||
|         "@fortawesome/fontawesome-common-types": "^0.2.35" | ||||
|       } | ||||
|     }, | ||||
|     "@fortawesome/vue-fontawesome": { | ||||
|       "version": "3.0.0-4", | ||||
|       "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.0-4.tgz", | ||||
|       "integrity": "sha512-dQVhhMRcUPCb0aqk5ohm0KGk5OJ7wFZ9aYapLzJB3Z+xs7LhkRWLTb87reelUAG5PFDjutDAXuloT9hi6cz72A==" | ||||
|     }, | ||||
|     "@popperjs/core": { | ||||
|       "version": "2.9.2", | ||||
|       "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz", | ||||
|   | ||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "uptime-kuma", | ||||
|     "version": "1.0.6", | ||||
|     "version": "1.0.7", | ||||
|     "license": "MIT", | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
| @@ -12,14 +12,18 @@ | ||||
|         "update": "", | ||||
|         "build": "vite build", | ||||
|         "vite-preview-dist": "vite preview --host", | ||||
|         "build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.6 --target release . --push", | ||||
|         "build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.7 --target release . --push", | ||||
|         "build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push", | ||||
|         "build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push", | ||||
|         "setup": "git checkout 1.0.6 && npm install && npm run build", | ||||
|         "setup": "git checkout 1.0.7 && npm install && npm run build", | ||||
|         "version-global-replace": "node extra/version-global-replace.js", | ||||
|         "mark-as-nightly": "node extra/mark-as-nightly.js" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@fortawesome/fontawesome-svg-core": "^1.2.35", | ||||
|         "@fortawesome/free-regular-svg-icons": "^5.15.3", | ||||
|         "@fortawesome/free-solid-svg-icons": "^5.15.3", | ||||
|         "@fortawesome/vue-fontawesome": "^3.0.0-4", | ||||
|         "@popperjs/core": "^2.9.2", | ||||
|         "args-parser": "^1.3.0", | ||||
|         "axios": "^0.21.1", | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.5 KiB | 
| @@ -8,7 +8,7 @@ class Database { | ||||
|  | ||||
|     static templatePath = "./db/kuma.db" | ||||
|     static path =  './data/kuma.db'; | ||||
|     static latestVersion = 1; | ||||
|     static latestVersion = 3; | ||||
|     static noReject = true; | ||||
|  | ||||
|     static async patch() { | ||||
|   | ||||
| @@ -1,15 +1,23 @@ | ||||
| const Prometheus = require('prom-client'); | ||||
| const https = require('https'); | ||||
| const dayjs = require("dayjs"); | ||||
| const utc = require('dayjs/plugin/utc') | ||||
| var timezone = require('dayjs/plugin/timezone') | ||||
| dayjs.extend(utc) | ||||
| dayjs.extend(timezone) | ||||
| const axios = require("axios"); | ||||
| const {tcping, ping} = require("../util-server"); | ||||
| const {debug, UP, DOWN, PENDING} = require("../util"); | ||||
| const {tcping, ping, checkCertificate} = require("../util-server"); | ||||
| const {R} = require("redbean-node"); | ||||
| const {BeanModel} = require("redbean-node/dist/bean-model"); | ||||
| const {Notification} = require("../notification") | ||||
|  | ||||
| //  Use Custom agent to disable session reuse | ||||
| //  https://github.com/nodejs/node/issues/3940 | ||||
| const customAgent = new https.Agent({ | ||||
|     maxCachedSessions: 0 | ||||
| }); | ||||
|  | ||||
| const commonLabels = [ | ||||
|     'monitor_name', | ||||
|     'monitor_type', | ||||
| @@ -18,24 +26,24 @@ const commonLabels = [ | ||||
|     'monitor_port', | ||||
| ] | ||||
|  | ||||
|  | ||||
| const monitor_response_time = new Prometheus.Gauge({ | ||||
|     name: 'monitor_response_time', | ||||
|     help: 'Monitor Response Time (ms)', | ||||
|     labelNames: commonLabels | ||||
| }); | ||||
|  | ||||
| const monitor_status = new Prometheus.Gauge({ | ||||
|     name: 'monitor_status', | ||||
|     help: 'Monitor Status (1 = UP, 0= DOWN)', | ||||
|     labelNames: commonLabels | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * status: | ||||
|  *      0 = DOWN | ||||
|  *      1 = UP | ||||
|  */ | ||||
| class Monitor extends BeanModel { | ||||
|  | ||||
|     async toJSON() { | ||||
|  | ||||
|         let notificationIDList = {}; | ||||
| @@ -54,6 +62,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, | ||||
| @@ -65,6 +74,7 @@ class Monitor extends BeanModel { | ||||
|  | ||||
|     start(io) { | ||||
|         let previousBeat = null; | ||||
|         let retries = 0; | ||||
|  | ||||
|         const monitorLabelValues = { | ||||
|                 monitor_name: this.name, | ||||
| @@ -74,21 +84,23 @@ class Monitor extends BeanModel { | ||||
|                 monitor_port: this.port | ||||
|         } | ||||
|  | ||||
|  | ||||
|         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,13 +110,27 @@ class Monitor extends BeanModel { | ||||
|                 if (this.type === "http" || this.type === "keyword") { | ||||
|                     let startTime = dayjs().valueOf(); | ||||
|                     let res = await axios.get(this.url, { | ||||
|                         headers: { 'User-Agent':'Uptime-Kuma' } | ||||
|                     }) | ||||
|                         headers: { "User-Agent": "Uptime-Kuma" }, | ||||
|                         httpsAgent: customAgent, | ||||
|                     }); | ||||
|                     bean.msg = `${res.status} - ${res.statusText}` | ||||
|                     bean.ping = dayjs().valueOf() - startTime; | ||||
|  | ||||
|                     // Check certificate if https is used | ||||
|  | ||||
|                     let certInfoStartTime = dayjs().valueOf(); | ||||
|                     if (this.getUrl()?.protocol === "https:") { | ||||
|                         try { | ||||
|                             await this.updateTlsInfo(checkCertificate(res)); | ||||
|                         } catch (e) { | ||||
|                             console.error(e.message) | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms") | ||||
|  | ||||
|                     if (this.type === "http") { | ||||
|                         bean.status = 1; | ||||
|                         bean.status = UP; | ||||
|                     } else { | ||||
|  | ||||
|                         let data = res.data; | ||||
| @@ -116,7 +142,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") | ||||
|                         } | ||||
| @@ -127,30 +153,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" | ||||
| @@ -171,11 +219,12 @@ class Monitor extends BeanModel { | ||||
|                 bean.important = false; | ||||
|             } | ||||
|  | ||||
|  | ||||
|             monitor_status.set(monitorLabelValues, bean.status) | ||||
|  | ||||
|             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}`) | ||||
|             } | ||||
| @@ -198,10 +247,35 @@ class Monitor extends BeanModel { | ||||
|         clearInterval(this.heartbeatInterval) | ||||
|     } | ||||
|  | ||||
|     // Helper Method: | ||||
|     // returns URL object for further usage | ||||
|     // returns null if url is invalid | ||||
|     getUrl() { | ||||
|         try { | ||||
|             return new URL(this.url); | ||||
|         } catch (_) { | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Store TLS info to database | ||||
|     async updateTlsInfo(checkCertificateResult) { | ||||
|         let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ | ||||
|             this.id | ||||
|         ]); | ||||
|         if (tls_info_bean == null) { | ||||
|             tls_info_bean = R.dispense("monitor_tls_info"); | ||||
|             tls_info_bean.monitor_id = this.id; | ||||
|         } | ||||
|         tls_info_bean.info_json = JSON.stringify(checkCertificateResult); | ||||
|         await R.store(tls_info_bean); | ||||
|     } | ||||
|  | ||||
|     static async sendStats(io, monitorID, userID) { | ||||
|         Monitor.sendAvgPing(24, io, monitorID, userID); | ||||
|         Monitor.sendUptime(24, io, monitorID, userID); | ||||
|         Monitor.sendUptime(24 * 30, io, monitorID, userID); | ||||
|         Monitor.sendCertInfo(io, monitorID, userID); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -222,6 +296,15 @@ class Monitor extends BeanModel { | ||||
|         io.to(userID).emit("avgPing", monitorID, avgPing); | ||||
|     } | ||||
|  | ||||
|     static async sendCertInfo(io, monitorID, userID) { | ||||
|          let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [ | ||||
|             monitorID | ||||
|         ]); | ||||
|         if (tls_info != null) { | ||||
|             io.to(userID).emit("certInfo", monitorID, tls_info.info_json); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Uptime with calculation | ||||
|      * Calculation based on: | ||||
| @@ -270,7 +353,7 @@ class Monitor extends BeanModel { | ||||
|                 } | ||||
|  | ||||
|                 total += value; | ||||
|                 if (row.status === 0) { | ||||
|                 if (row.status === 0 || row.status === 2) { | ||||
|                     downtime += value; | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -204,7 +204,7 @@ class Notification { | ||||
|                 } | ||||
|  | ||||
|                 let data = { | ||||
|                     "message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" +msg + '\n<b>Time (UTC)</b>:' +time, | ||||
|                     "message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:"+msg+ '\n<b>Time (UTC)</b>:' +heartbeatJSON["time"], | ||||
|                     "user":notification.pushoveruserkey, | ||||
|                     "token": notification.pushoverapptoken, | ||||
|                     "sound": notification.pushoversounds, | ||||
|   | ||||
| @@ -235,6 +235,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; | ||||
|  | ||||
| @@ -544,12 +545,12 @@ async function afterLogin(socket, user) { | ||||
|     let monitorList = await sendMonitorList(socket) | ||||
|  | ||||
|     for (let monitorID in monitorList) { | ||||
|         await sendHeartbeatList(socket, monitorID); | ||||
|         await sendImportantHeartbeatList(socket, monitorID); | ||||
|         await Monitor.sendStats(io, monitorID, user.id) | ||||
|         sendHeartbeatList(socket, monitorID); | ||||
|         sendImportantHeartbeatList(socket, monitorID); | ||||
|         Monitor.sendStats(io, monitorID, user.id) | ||||
|     } | ||||
|  | ||||
|     await sendNotificationList(socket) | ||||
|     sendNotificationList(socket) | ||||
| } | ||||
|  | ||||
| async function getMonitorJSONList(userID) { | ||||
|   | ||||
| @@ -70,3 +70,52 @@ exports.getSettings = async function (type) { | ||||
|  | ||||
|     return result; | ||||
| } | ||||
|  | ||||
|  | ||||
| // ssl-checker by @dyaa | ||||
| // param: res - response object from axios | ||||
| // return an object containing the certificate information | ||||
|  | ||||
| const getDaysBetween = (validFrom, validTo) => | ||||
|     Math.round(Math.abs(+validFrom - +validTo) / 8.64e7); | ||||
|  | ||||
| const getDaysRemaining = (validFrom, validTo) => { | ||||
|     const daysRemaining = getDaysBetween(validFrom, validTo); | ||||
|     if (new Date(validTo).getTime() < new Date().getTime()) { | ||||
|         return -daysRemaining; | ||||
|     } | ||||
|     return daysRemaining; | ||||
| }; | ||||
|  | ||||
| exports.checkCertificate = function (res) { | ||||
|     const { | ||||
|         valid_from, | ||||
|         valid_to, | ||||
|         subjectaltname, | ||||
|         issuer, | ||||
|         fingerprint, | ||||
|     } = res.request.res.socket.getPeerCertificate(false); | ||||
|  | ||||
|     if (!valid_from || !valid_to || !subjectaltname) { | ||||
|         throw { message: 'No TLS certificate in response' }; | ||||
|     } | ||||
|  | ||||
|     const valid = res.request.res.socket.authorized || false; | ||||
|  | ||||
|     const validTo = new Date(valid_to); | ||||
|  | ||||
|     const validFor = subjectaltname | ||||
|         .replace(/DNS:|IP Address:/g, "") | ||||
|         .split(", "); | ||||
|  | ||||
|     const daysRemaining = getDaysRemaining(new Date(), validTo); | ||||
|  | ||||
|     return { | ||||
|         valid, | ||||
|         validFor, | ||||
|         validTo, | ||||
|         daysRemaining, | ||||
|         issuer, | ||||
|         fingerprint, | ||||
|     }; | ||||
| } | ||||
| @@ -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)); | ||||
| } | ||||
| @@ -14,3 +18,9 @@ exports.ucfirst = function (str) { | ||||
|     return firstLetter.toUpperCase() + str.substr(1); | ||||
| } | ||||
|  | ||||
| exports.debug = (msg) => { | ||||
|     if (process.env.NODE_ENV === "development") { | ||||
|         console.log(msg) | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| $primary: #5CDD8B; | ||||
| $danger: #DC3545; | ||||
| $warning: #f8a306; | ||||
| $link-color: #111; | ||||
| $border-radius: 50rem; | ||||
|  | ||||
| $highlight: #7ce8a4; | ||||
| $highlight-white: #e7faec; | ||||
| $highlight-white: #e7faec; | ||||
| @@ -14,12 +14,23 @@ dayjs.extend(relativeTime) | ||||
| export default { | ||||
|     props: { | ||||
|         value: String, | ||||
|         dateOnly: { | ||||
|             type: Boolean, | ||||
|             default: false, | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     computed: { | ||||
|         displayText() { | ||||
|             let format = "YYYY-MM-DD HH:mm:ss"; | ||||
|             return dayjs.utc(this.value).tz(this.$root.timezone).format(format) | ||||
|             if (this.value !== undefined && this.value !== "") { | ||||
|                 let format = "YYYY-MM-DD HH:mm:ss"; | ||||
|                 if (this.dateOnly) { | ||||
|                     format = "YYYY-MM-DD"; | ||||
|                 } | ||||
|                 return dayjs.utc(this.value).tz(this.$root.timezone).format(format); | ||||
|             } else { | ||||
|                 return ""; | ||||
|             } | ||||
|         }, | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|         <div class="hp-bar-big" :style="barStyle"> | ||||
|             <div | ||||
|                 class="beat" | ||||
|                 :class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0) }" | ||||
|                 :class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }" | ||||
|                 :style="beatStyle" | ||||
|                 v-for="(beat, index) in shortBeatList" | ||||
|                 :key="index" | ||||
| @@ -166,6 +166,10 @@ export default { | ||||
|             background-color: $danger; | ||||
|         } | ||||
|  | ||||
|         &.pending { | ||||
|             background-color: $warning; | ||||
|         } | ||||
|  | ||||
|         &:not(.empty):hover { | ||||
|             transition: all ease-in-out 0.15s; | ||||
|             opacity: 0.8; | ||||
|   | ||||
| @@ -223,16 +223,22 @@ | ||||
|  | ||||
|                         <template v-if="notification.type === 'pushover'"> | ||||
|                             <div class="mb-3"> | ||||
|                                 <label for="pushover-app-token" class="form-label">Application Token<span style="color:red;"><sup>*</sup></span></label> | ||||
|                                 <input type="text" class="form-control" id="pushover-app-token" required v-model="notification.pushoverapptoken"> | ||||
|                                 <label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label> | ||||
|                                 <input type="text" class="form-control" id="pushover-user" required v-model="notification.pushoveruserkey"> | ||||
|                                 <label for="pushover-app-token" class="form-label">Application Token<span style="color:red;"><sup>*</sup></span></label> | ||||
|                                 <input type="text" class="form-control" id="pushover-app-token" required v-model="notification.pushoverapptoken"> | ||||
|                                 <label for="pushover-device" class="form-label">Device</label> | ||||
|                                 <input type="text" class="form-control" id="pushover-device" v-model="notification.pushoverdevice"> | ||||
|                                 <label for="pushover-device" class="form-label">Message Title</label> | ||||
|                                 <input type="text" class="form-control" id="pushover-title" v-model="notification.pushovertitle"> | ||||
|                                 <label for="pushover-priority" class="form-label">Priority</label> | ||||
|                                 <input type="text" class="form-control" id="pushover-priority" v-model="notification.pushoverpriority"> | ||||
|                                 <select class="form-select"  id="pushover-priority" v-model="notification.pushoverpriority"> | ||||
|                                     <option>-2</option> | ||||
|                                     <option>-1</option> | ||||
|                                     <option>0</option> | ||||
|                                     <option>1</option> | ||||
|                                     <option>2</option> | ||||
|                                 </select> | ||||
|                                 <label for="pushover-sound" class="form-label">Notification Sound</label> | ||||
|                                 <select class="form-select"  id="pushover-sound" v-model="notification.pushoversounds"> | ||||
|                                     <option>pushover</option> | ||||
| @@ -264,17 +270,19 @@ | ||||
|                                         More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a> | ||||
|                                 </p> | ||||
|                                  <p style="margin-top: 8px;"> | ||||
|                                         Emergency priority(2) has default 30 second timeout between retries and will expire after 1 hour. | ||||
|                                         Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour. | ||||
|                                 </p> | ||||
|                                  <p style="margin-top: 8px;"> | ||||
|                                         If you want to send notifications to different devices, fill out Device field. | ||||
|                                 </p> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </template> | ||||
|  | ||||
|                         <template v-if="notification.type === 'apprise'"> | ||||
|  | ||||
|                             <div class="mb-3"> | ||||
|                                 <label for="gotify-application-token" class="form-label">Apprise URL</label> | ||||
|                                 <input type="text" class="form-control" id="gotify-application-token" required v-model="notification.appriseURL"> | ||||
|                                 <label for="apprise-url" class="form-label">Apprise URL</label> | ||||
|                                 <input type="text" class="form-control" id="apprise-url" required v-model="notification.appriseURL"> | ||||
|                                 <div class="form-text"> | ||||
|                                     <p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p> | ||||
|                                     <p> | ||||
| @@ -282,7 +290,6 @@ | ||||
|                                     </p> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <div class="mb-3"> | ||||
|                                 <p> | ||||
|                                     Status: | ||||
| @@ -290,7 +297,6 @@ | ||||
|                                     <span class="text-danger" v-else>Apprise is not installed. <a href="https://github.com/caronc/apprise">Read more</a></span> | ||||
|                                 </p> | ||||
|                             </div> | ||||
|  | ||||
|                         </template> | ||||
|  | ||||
|                     </div> | ||||
|   | ||||
| @@ -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 { | ||||
|  | ||||
| <style scoped> | ||||
|     span { | ||||
|         width: 45px; | ||||
|         width: 54px; | ||||
|     } | ||||
| </style> | ||||
|   | ||||
| @@ -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" | ||||
|             } | ||||
|   | ||||
							
								
								
									
										12
									
								
								src/icon.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/icon.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import { library } from '@fortawesome/fontawesome-svg-core' | ||||
| //import { fa } from '@fortawesome/free-regular-svg-icons' | ||||
| import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' | ||||
| import { faCog, faTachometerAlt, faEdit, faPlus, faPause, faPlay, faTrash, faList } from '@fortawesome/free-solid-svg-icons' | ||||
|  | ||||
| // Add Free Font Awesome Icons here | ||||
| // https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free | ||||
| library.add(faCog, faTachometerAlt, faEdit, faPlus, faPause, faPlay, faTrash, faList) | ||||
|  | ||||
| export { | ||||
|     FontAwesomeIcon | ||||
| } | ||||
| @@ -14,8 +14,8 @@ | ||||
|         </router-link> | ||||
|  | ||||
|         <ul class="nav nav-pills" > | ||||
|             <li class="nav-item"><router-link to="/dashboard" class="nav-link">📊 Dashboard</router-link></li> | ||||
|             <li class="nav-item"><router-link to="/settings" class="nav-link">🔧 Settings</router-link></li> | ||||
|             <li class="nav-item"><router-link to="/dashboard" class="nav-link"><font-awesome-icon icon="tachometer-alt" /> Dashboard</router-link></li> | ||||
|             <li class="nav-item"><router-link to="/settings" class="nav-link"><font-awesome-icon icon="cog" /> Settings</router-link></li> | ||||
|         </ul> | ||||
|     </header> | ||||
|  | ||||
| @@ -44,10 +44,27 @@ | ||||
|     <!-- Mobile Only --> | ||||
|     <div style="width: 100%;height: 60px;" v-if="$root.isMobile"></div> | ||||
|     <nav class="bottom-nav" v-if="$root.isMobile"> | ||||
|         <router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList"><div>📊</div>Dashboard</router-link> | ||||
|         <a href="#" :class=" { 'router-link-exact-active' : $root.showListMobile } " @click="$root.showListMobile = ! $root.showListMobile"><div>📃</div>List</a> | ||||
|         <router-link to="/add" class="nav-link" @click="$root.cancelActiveList"><div>➕</div>Add</router-link> | ||||
|         <router-link to="/settings" class="nav-link" @click="$root.cancelActiveList"><div>🔧</div>Settings</router-link> | ||||
|  | ||||
|         <router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList"> | ||||
|             <div><font-awesome-icon icon="tachometer-alt" /></div> | ||||
|             Dashboard | ||||
|         </router-link> | ||||
|  | ||||
|         <a href="#" :class=" { 'router-link-exact-active' : $root.showListMobile } " @click="$root.showListMobile = ! $root.showListMobile"> | ||||
|             <div><font-awesome-icon icon="list" /></div> | ||||
|             List | ||||
|         </a> | ||||
|  | ||||
|         <router-link to="/add" class="nav-link" @click="$root.cancelActiveList"> | ||||
|             <div><font-awesome-icon icon="plus" /></div> | ||||
|             Add | ||||
|         </router-link> | ||||
|  | ||||
|         <router-link to="/settings" class="nav-link" @click="$root.cancelActiveList"> | ||||
|             <div><font-awesome-icon icon="cog" /></div> | ||||
|             Settings | ||||
|         </router-link> | ||||
|  | ||||
|     </nav> | ||||
| </template> | ||||
|  | ||||
| @@ -99,7 +116,7 @@ export default { | ||||
|     box-shadow: 0 15px 47px 0 rgba(0, 0, 0, 0.05), 0 5px 14px 0 rgba(0, 0, 0, 0.05); | ||||
|     text-align: center; | ||||
|     white-space: nowrap; | ||||
|     padding: 0 35px; | ||||
|     padding: 0 10px; | ||||
|  | ||||
|     a { | ||||
|         text-align: center; | ||||
| @@ -144,6 +161,7 @@ main { | ||||
| footer { | ||||
|     color: #AAA; | ||||
|     font-size: 13px; | ||||
|     margin-top: 10px; | ||||
|     margin-bottom: 30px; | ||||
|     margin-left: 10px; | ||||
|     text-align: center; | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import Toast from "vue-toastification"; | ||||
| import "vue-toastification/dist/index.css"; | ||||
| import "bootstrap" | ||||
| import Setup from "./pages/Setup.vue"; | ||||
| import {FontAwesomeIcon} from "./icon.js" | ||||
|  | ||||
| const routes = [ | ||||
|     { | ||||
| @@ -88,5 +89,7 @@ const options = { | ||||
|  | ||||
| app.use(Toast, options); | ||||
|  | ||||
| app.component('font-awesome-icon', FontAwesomeIcon) | ||||
|  | ||||
| app.mount('#app') | ||||
|  | ||||
|   | ||||
| @@ -25,6 +25,7 @@ export default { | ||||
|             importantHeartbeatList: { }, | ||||
|             avgPingList: { }, | ||||
|             uptimeList: { }, | ||||
|             certInfoList: {}, | ||||
|             notificationList: [], | ||||
|             windowWidth: window.innerWidth, | ||||
|             showListMobile: false, | ||||
| @@ -58,7 +59,17 @@ export default { | ||||
|             this.$router.push("/setup") | ||||
|         }); | ||||
|  | ||||
|         socket.on('monitorList', (data) => { | ||||
|         socket.on("monitorList", (data) => { | ||||
|             // Add Helper function | ||||
|             Object.entries(data).forEach(([monitorID, monitor]) => { | ||||
|                 monitor.getUrl = () => { | ||||
|                     try { | ||||
|                         return new URL(monitor.url); | ||||
|                     } catch (_) { | ||||
|                         return null; | ||||
|                     } | ||||
|                 }; | ||||
|             }); | ||||
|             this.monitorList = data; | ||||
|         }); | ||||
|  | ||||
| @@ -114,6 +125,10 @@ export default { | ||||
|             this.uptimeList[`${monitorID}_${type}`] = data | ||||
|         }); | ||||
|  | ||||
|         socket.on('certInfo', (monitorID, data) => { | ||||
|             this.certInfoList[monitorID] = JSON.parse(data) | ||||
|         }); | ||||
|  | ||||
|         socket.on('importantHeartbeatList', (monitorID, data) => { | ||||
|             if (! (monitorID in this.importantHeartbeatList)) { | ||||
|                 this.importantHeartbeatList[monitorID] = data; | ||||
| @@ -279,6 +294,11 @@ export default { | ||||
|                         text: "Down", | ||||
|                         color: "danger" | ||||
|                     }; | ||||
|                 } else if (lastHeartBeat.status === 2) { | ||||
|                     result[monitorID] = { | ||||
|                         text: "Pending", | ||||
|                         color: "warning" | ||||
|                     }; | ||||
|                 } else { | ||||
|                     result[monitorID] = unknown; | ||||
|                 } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
|         <div class="row"> | ||||
|             <div class="col-12 col-md-5 col-xl-4"> | ||||
|                 <div v-if="! $root.isMobile"> | ||||
|                     <router-link to="/add" class="btn btn-primary">Add New Monitor</router-link> | ||||
|                     <router-link to="/add" class="btn btn-primary"><font-awesome-icon icon="plus" /> Add New Monitor</router-link> | ||||
|                 </div> | ||||
|  | ||||
|                 <div class="shadow-box list mb-4" v-if="showList"> | ||||
|   | ||||
| @@ -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++; | ||||
|                     } | ||||
|   | ||||
| @@ -11,10 +11,10 @@ | ||||
|     </p> | ||||
|  | ||||
|     <div class="functions"> | ||||
|         <button class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button> | ||||
|         <button class="btn btn-primary" @click="resumeMonitor" v-if="! monitor.active">Resume</button> | ||||
|         <router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">Edit</router-link> | ||||
|         <button class="btn btn-danger" @click="deleteDialog">Delete</button> | ||||
|         <button class="btn btn-light" @click="pauseDialog" v-if="monitor.active"><font-awesome-icon icon="pause" /> Pause</button> | ||||
|         <button class="btn btn-primary" @click="resumeMonitor" v-if="! monitor.active"><font-awesome-icon icon="pause" /> Resume</button> | ||||
|         <router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary"><font-awesome-icon icon="edit" /> Edit</router-link> | ||||
|         <button class="btn btn-danger" @click="deleteDialog"><font-awesome-icon icon="trash" /> Delete</button> | ||||
|     </div> | ||||
|  | ||||
|     <div class="shadow-box"> | ||||
| @@ -51,6 +51,46 @@ | ||||
|                 <p>(30-day)</p> | ||||
|                 <span class="num"><Uptime :monitor="monitor" type="720" /></span> | ||||
|             </div> | ||||
|  | ||||
|             <div class="col" v-if="certInfo"> | ||||
|                 <h4>CertExp.</h4> | ||||
|                 <p>(<Datetime :value="certInfo.validTo" date-only />)</p> | ||||
|                 <span class="num" > | ||||
|                     <a href="#"  @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{certInfo.daysRemaining}} days</a> | ||||
|                 </span> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="shadow-box big-padding text-center" v-if="showCertInfoBox"> | ||||
|         <div class="row"> | ||||
|             <div class="col"> | ||||
|                 <h4>Certificate Info</h4> | ||||
|                 <table class="text-start"> | ||||
|                     <tbody> | ||||
|                         <tr class="my-3"> | ||||
|                             <td class="px-3">Valid: </td> | ||||
|                             <td>{{ certInfo.valid }}</td> | ||||
|                         </tr> | ||||
|                         <tr class="my-3"> | ||||
|                             <td class="px-3">Valid To: </td> | ||||
|                             <td><Datetime :value="certInfo.validTo" /></td> | ||||
|                         </tr> | ||||
|                         <tr class="my-3"> | ||||
|                             <td class="px-3">Days Remaining: </td> | ||||
|                             <td>{{ certInfo.daysRemaining }}</td> | ||||
|                         </tr> | ||||
|                         <tr class="my-3"> | ||||
|                             <td class="px-3">Issuer: </td> | ||||
|                             <td>{{ certInfo.issuer }}</td> | ||||
|                         </tr> | ||||
|                         <tr class="my-3"> | ||||
|                             <td class="px-3">Fingerprint: </td> | ||||
|                             <td>{{ certInfo.fingerprint }}</td> | ||||
|                         </tr> | ||||
|                     </tbody> | ||||
|                 </table> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
| @@ -122,6 +162,7 @@ export default { | ||||
|             page: 1, | ||||
|             perPage: 25, | ||||
|             heartBeatList: [], | ||||
|             toggleCertInfoBox: false, | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -180,6 +221,18 @@ export default { | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         certInfo() { | ||||
|             if (this.$root.certInfoList[this.monitor.id]) { | ||||
|                 return this.$root.certInfoList[this.monitor.id] | ||||
|             } else { | ||||
|                 return null | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         showCertInfoBox() { | ||||
|             return this.certInfo != null && this.toggleCertInfoBox; | ||||
|         }, | ||||
|  | ||||
|         displayedRecords() { | ||||
|             const startIndex = this.perPage * (this.page - 1); | ||||
|             const endIndex = startIndex + this.perPage; | ||||
| @@ -268,4 +321,12 @@ table { | ||||
|     font-size: 13px; | ||||
|     color: #AAA; | ||||
| } | ||||
|  | ||||
| .stats { | ||||
|     padding: 10px; | ||||
|  | ||||
|     .col { | ||||
|         margin: 20px 0; | ||||
|     } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -48,6 +48,12 @@ | ||||
|                         <input type="number" class="form-control" id="interval" v-model="monitor.interval" required min="20" step="1"> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="mb-3"> | ||||
|                         <label for="maxRetries" class="form-label">Retries</label> | ||||
|                         <input type="number" class="form-control" id="maxRetries" v-model="monitor.maxretries" required min="0" step="1"> | ||||
|                         <div class="form-text">Maximum retries before the service is marked as down and a notification is sent</div> | ||||
|                     </div> | ||||
|  | ||||
|                     <div> | ||||
|                         <button class="btn btn-primary" type="submit" :disabled="processing">Save</button> | ||||
|                     </div> | ||||
| @@ -61,7 +67,7 @@ | ||||
|                 <h2>Notifications</h2> | ||||
|                 <p v-if="$root.notificationList.length === 0">Not available, please setup.</p> | ||||
|  | ||||
|                 <div class="form-check form-switch mb-3" v-for="(notification, index) in $root.notificationList" :key="index"> | ||||
|                 <div class="form-check form-switch mb-3" :key="notification.id" v-for="notification in $root.notificationList"> | ||||
|                     <input class="form-check-input" type="checkbox" :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]"> | ||||
|  | ||||
|                     <label class="form-check-label" :for=" 'notification' + notification.id"> | ||||
| @@ -119,6 +125,7 @@ export default { | ||||
|                     name: "", | ||||
|                     url: "https://", | ||||
|                     interval: 60, | ||||
|                     maxretries: 0, | ||||
|                     notificationIDList: {}, | ||||
|                 } | ||||
|             } else if (this.isEdit) { | ||||
|   | ||||
| @@ -36,7 +36,7 @@ | ||||
|                         <label for="repeat-new-password" class="form-label">Repeat New Password</label> | ||||
|                         <input type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" id="repeat-new-password" required v-model="password.repeatNewPassword"> | ||||
|                         <div class="invalid-feedback"> | ||||
|                             The repeat password is not match. | ||||
|                             The repeat password does not match. | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
| @@ -56,7 +56,7 @@ | ||||
|  | ||||
|                 <h2>Notifications</h2> | ||||
|                 <p v-if="$root.notificationList.length === 0">Not available, please setup.</p> | ||||
|                 <p v-else>Please assign the notification to monitor(s) to get it works.</p> | ||||
|                 <p v-else>Please assign a notification to monitor(s) to get it to work.</p> | ||||
|  | ||||
|                 <ul class="list-group mb-3" style="border-radius: 1rem;"> | ||||
|                     <li class="list-group-item" v-for="(notification, index) in $root.notificationList" :key="index"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user