mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-27 16:54:04 +00:00
A complete maintenance planning system has been created
This commit is contained in:
parent
c3c4db52ec
commit
0d3414c6d6
32 changed files with 1121 additions and 51 deletions
25
db/patch-maintenance-table.sql
Normal file
25
db/patch-maintenance-table.sql
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE maintenance
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title VARCHAR(150),
|
||||||
|
description TEXT,
|
||||||
|
user_id INTEGER REFERENCES user ON UPDATE CASCADE ON DELETE SET NULL,
|
||||||
|
start_date DATETIME,
|
||||||
|
end_date DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE monitor_maintenance
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
monitor_id INTEGER NOT NULL,
|
||||||
|
maintenance_id INTEGER NOT NULL,
|
||||||
|
CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
create index maintenance_user_id on maintenance (user_id);
|
||||||
|
|
||||||
|
COMMIT;
|
54
package-lock.json
generated
54
package-lock.json
generated
|
@ -14914,7 +14914,8 @@
|
||||||
"@fortawesome/vue-fontawesome": {
|
"@fortawesome/vue-fontawesome": {
|
||||||
"version": "3.0.0-5",
|
"version": "3.0.0-5",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.0-5.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.0-5.tgz",
|
||||||
"integrity": "sha512-aNmBT4bOecrFsZTog1l6AJDQHPP3ocXV+WQ3Ogy8WZCqstB/ahfhH4CPu5i4N9Hw0MBKXqE+LX+NbUxcj8cVTw=="
|
"integrity": "sha512-aNmBT4bOecrFsZTog1l6AJDQHPP3ocXV+WQ3Ogy8WZCqstB/ahfhH4CPu5i4N9Hw0MBKXqE+LX+NbUxcj8cVTw==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@gar/promisify": {
|
"@gar/promisify": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
|
@ -16117,7 +16118,8 @@
|
||||||
"version": "1.9.4",
|
"version": "1.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.9.4.tgz",
|
||||||
"integrity": "sha512-0CZqaCoChriPTTtGkERy1LGPcYjGFpi2uYRhBPIkqJqUGV5JnJFhQAgh6oH9j5XZHfrRaisX8W0xSpO4T7S78A==",
|
"integrity": "sha512-0CZqaCoChriPTTtGkERy1LGPcYjGFpi2uYRhBPIkqJqUGV5JnJFhQAgh6oH9j5XZHfrRaisX8W0xSpO4T7S78A==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@vue/compiler-core": {
|
"@vue/compiler-core": {
|
||||||
"version": "3.2.22",
|
"version": "3.2.22",
|
||||||
|
@ -16277,7 +16279,8 @@
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
||||||
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"acorn-walk": {
|
"acorn-walk": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
|
@ -16766,7 +16769,8 @@
|
||||||
"bootstrap": {
|
"bootstrap": {
|
||||||
"version": "5.1.3",
|
"version": "5.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
|
||||||
"integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q=="
|
"integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
|
@ -16958,7 +16962,8 @@
|
||||||
"chartjs-adapter-dayjs": {
|
"chartjs-adapter-dayjs": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-dayjs/-/chartjs-adapter-dayjs-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chartjs-adapter-dayjs/-/chartjs-adapter-dayjs-1.0.0.tgz",
|
||||||
"integrity": "sha512-EnbVqTJGFKLpg1TROLdCEufrzbmIa2oeLGx8O2Wdjw2EoMudoOo9+YFu+6CM0Z0hQ/v3yq/e/Y6efQMu22n8Jg=="
|
"integrity": "sha512-EnbVqTJGFKLpg1TROLdCEufrzbmIa2oeLGx8O2Wdjw2EoMudoOo9+YFu+6CM0Z0hQ/v3yq/e/Y6efQMu22n8Jg==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"check-password-strength": {
|
"check-password-strength": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
|
@ -17548,7 +17553,8 @@
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "8.2.3",
|
"version": "8.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
||||||
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA=="
|
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
||||||
|
"requires": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -17571,7 +17577,8 @@
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "8.2.3",
|
"version": "8.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
||||||
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA=="
|
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
||||||
|
"requires": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -20015,7 +20022,8 @@
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
|
||||||
"integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
|
"integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"jest-puppeteer": {
|
"jest-puppeteer": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
|
@ -21774,12 +21782,14 @@
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz",
|
||||||
"integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==",
|
"integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"postcss-scss": {
|
"postcss-scss": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.2.tgz",
|
||||||
"integrity": "sha512-xfdkU128CkKKKVAwkyt0M8OdnelJ3MRcIRAPPQkRpoPeuzWY3RIeg7piRCpZ79MK7Q16diLXMMAD9dN5mauPlQ=="
|
"integrity": "sha512-xfdkU128CkKKKVAwkyt0M8OdnelJ3MRcIRAPPQkRpoPeuzWY3RIeg7piRCpZ79MK7Q16diLXMMAD9dN5mauPlQ==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"postcss-selector-parser": {
|
"postcss-selector-parser": {
|
||||||
"version": "6.0.8",
|
"version": "6.0.8",
|
||||||
|
@ -21979,7 +21989,8 @@
|
||||||
"version": "7.4.6",
|
"version": "7.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -23080,7 +23091,8 @@
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-6.0.0.tgz",
|
||||||
"integrity": "sha512-ZorSSdyMcxWpROYUvLEMm0vSZud2uB7tX1hzBZwvVY9SV/uly4AvvJPPhCcymZL3fcQhEQG5AELmrxWqtmzacw==",
|
"integrity": "sha512-ZorSSdyMcxWpROYUvLEMm0vSZud2uB7tX1hzBZwvVY9SV/uly4AvvJPPhCcymZL3fcQhEQG5AELmrxWqtmzacw==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"stylelint-config-standard": {
|
"stylelint-config-standard": {
|
||||||
"version": "24.0.0",
|
"version": "24.0.0",
|
||||||
|
@ -23653,17 +23665,20 @@
|
||||||
"vue-confirm-dialog": {
|
"vue-confirm-dialog": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-confirm-dialog/-/vue-confirm-dialog-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/vue-confirm-dialog/-/vue-confirm-dialog-1.0.2.tgz",
|
||||||
"integrity": "sha512-gTo1bMDWOLd/6ihmWv8VlPxhc9QaKoE5YqlsKydUOfrrQ3Q3taljF6yI+1TMtAtJLrvZ8DYrePhgBhY1VCJzbQ=="
|
"integrity": "sha512-gTo1bMDWOLd/6ihmWv8VlPxhc9QaKoE5YqlsKydUOfrrQ3Q3taljF6yI+1TMtAtJLrvZ8DYrePhgBhY1VCJzbQ==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"vue-contenteditable": {
|
"vue-contenteditable": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/vue-contenteditable/-/vue-contenteditable-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/vue-contenteditable/-/vue-contenteditable-3.0.4.tgz",
|
||||||
"integrity": "sha512-CmtqT4zHQwLoJEyNVaXUjjUFPUVYlXXBHfSbRCHCUjODMqrn6L293LM1nc1ELx8epitZZvecTfIqOLlSzB3d+w=="
|
"integrity": "sha512-CmtqT4zHQwLoJEyNVaXUjjUFPUVYlXXBHfSbRCHCUjODMqrn6L293LM1nc1ELx8epitZZvecTfIqOLlSzB3d+w==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"vue-demi": {
|
"vue-demi": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.10.1.tgz",
|
||||||
"integrity": "sha512-L6Oi+BvmMv6YXvqv5rJNCFHEKSVu7llpWWJczqmAQYOdmPPw5PNYoz1KKS//Fxhi+4QP64dsPjtmvnYGo1jemA=="
|
"integrity": "sha512-L6Oi+BvmMv6YXvqv5rJNCFHEKSVu7llpWWJczqmAQYOdmPPw5PNYoz1KKS//Fxhi+4QP64dsPjtmvnYGo1jemA==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"vue-eslint-parser": {
|
"vue-eslint-parser": {
|
||||||
"version": "7.11.0",
|
"version": "7.11.0",
|
||||||
|
@ -23735,7 +23750,8 @@
|
||||||
"vue-demi": {
|
"vue-demi": {
|
||||||
"version": "0.11.4",
|
"version": "0.11.4",
|
||||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.11.4.tgz",
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.11.4.tgz",
|
||||||
"integrity": "sha512-/3xFwzSykLW2HiiLie43a+FFgNOcokbBJ+fzvFXd0r2T8MYohqvphUyDQ8lbAwzQ3Dlcrb1c9ykifGkhSIAk6A=="
|
"integrity": "sha512-/3xFwzSykLW2HiiLie43a+FFgNOcokbBJ+fzvFXd0r2T8MYohqvphUyDQ8lbAwzQ3Dlcrb1c9ykifGkhSIAk6A==",
|
||||||
|
"requires": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -23750,7 +23766,8 @@
|
||||||
"vue-toastification": {
|
"vue-toastification": {
|
||||||
"version": "2.0.0-rc.5",
|
"version": "2.0.0-rc.5",
|
||||||
"resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz",
|
"resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz",
|
||||||
"integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA=="
|
"integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"vuedraggable": {
|
"vuedraggable": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
|
@ -23929,7 +23946,8 @@
|
||||||
"version": "7.5.5",
|
"version": "7.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
|
||||||
"integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
|
"integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"xml-name-validator": {
|
"xml-name-validator": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
|
|
|
@ -53,6 +53,7 @@ class Database {
|
||||||
"patch-2fa-invalidate-used-token.sql": true,
|
"patch-2fa-invalidate-used-token.sql": true,
|
||||||
"patch-notification_sent_history.sql": true,
|
"patch-notification_sent_history.sql": true,
|
||||||
"patch-monitor-basic-auth.sql": true,
|
"patch-monitor-basic-auth.sql": true,
|
||||||
|
"patch-maintenance-table.sql": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,6 +10,7 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
* 0 = DOWN
|
* 0 = DOWN
|
||||||
* 1 = UP
|
* 1 = UP
|
||||||
* 2 = PENDING
|
* 2 = PENDING
|
||||||
|
* 3 = MAINTENANCE
|
||||||
*/
|
*/
|
||||||
class Heartbeat extends BeanModel {
|
class Heartbeat extends BeanModel {
|
||||||
|
|
||||||
|
|
38
server/model/maintenance.js
Normal file
38
server/model/maintenance.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const utc = require("dayjs/plugin/utc");
|
||||||
|
let timezone = require("dayjs/plugin/timezone");
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
class Maintenance extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
*/
|
||||||
|
async toPublicJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
start_date: this.start_date,
|
||||||
|
end_date: this.end_date
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a object that ready to parse to JSON
|
||||||
|
*/
|
||||||
|
async toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
start_date: this.start_date,
|
||||||
|
end_date: this.end_date
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Maintenance;
|
|
@ -6,7 +6,7 @@ dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
const { debug, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger} = require("../../src/util");
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, errorLog } = require("../util-server");
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, errorLog } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
@ -20,6 +20,7 @@ const apicache = require("../modules/apicache");
|
||||||
* 0 = DOWN
|
* 0 = DOWN
|
||||||
* 1 = UP
|
* 1 = UP
|
||||||
* 2 = PENDING
|
* 2 = PENDING
|
||||||
|
* 3 = MAINTENANCE
|
||||||
*/
|
*/
|
||||||
class Monitor extends BeanModel {
|
class Monitor extends BeanModel {
|
||||||
|
|
||||||
|
@ -28,9 +29,12 @@ class Monitor extends BeanModel {
|
||||||
* Only show necessary data to public
|
* Only show necessary data to public
|
||||||
*/
|
*/
|
||||||
async toPublicJSON() {
|
async toPublicJSON() {
|
||||||
|
const maintenance = await R.getAll("SELECT mm.*, maintenance.start_date, maintenance.end_date FROM monitor_maintenance mm JOIN maintenance ON mm.maintenance_id = maintenance.id WHERE mm.monitor_id = ? AND datetime(maintenance.start_date) <= datetime('now', 'localtime') AND datetime(maintenance.end_date) >= datetime('now', 'localtime')", [this.id]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
|
maintenance: (maintenance.length !== 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +54,7 @@ class Monitor extends BeanModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
||||||
|
const maintenance = await R.getAll("SELECT mm.*, maintenance.start_date, maintenance.end_date FROM monitor_maintenance mm JOIN maintenance ON mm.maintenance_id = maintenance.id WHERE mm.monitor_id = ? AND datetime(maintenance.start_date) <= datetime('now', 'localtime') AND datetime(maintenance.end_date) >= datetime('now', 'localtime')", [this.id]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
@ -79,6 +84,7 @@ class Monitor extends BeanModel {
|
||||||
pushToken: this.pushToken,
|
pushToken: this.pushToken,
|
||||||
notificationIDList,
|
notificationIDList,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
|
maintenance: (maintenance.length !== 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,6 +142,8 @@ class Monitor extends BeanModel {
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTime(dayjs.utc());
|
||||||
bean.status = DOWN;
|
bean.status = DOWN;
|
||||||
|
|
||||||
|
const maintenance = await R.getAll("SELECT mm.*, maintenance.start_date, maintenance.end_date FROM monitor_maintenance mm JOIN maintenance ON mm.maintenance_id = maintenance.id WHERE mm.monitor_id = ? AND datetime(maintenance.start_date) <= datetime('now', 'localtime') AND datetime(maintenance.end_date) >= datetime('now', 'localtime')", [this.id]);
|
||||||
|
|
||||||
if (this.isUpsideDown()) {
|
if (this.isUpsideDown()) {
|
||||||
bean.status = flipStatus(bean.status);
|
bean.status = flipStatus(bean.status);
|
||||||
}
|
}
|
||||||
|
@ -148,7 +156,11 @@ class Monitor extends BeanModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.type === "http" || this.type === "keyword") {
|
if (maintenance.length !== 0) {
|
||||||
|
bean.msg = "Monitor under maintenance";
|
||||||
|
bean.status = MAINTENANCE;
|
||||||
|
}
|
||||||
|
else if (this.type === "http" || this.type === "keyword") {
|
||||||
// Do not do any queries/high loading things before the "bean.ping"
|
// Do not do any queries/high loading things before the "bean.ping"
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
|
@ -387,8 +399,13 @@ class Monitor extends BeanModel {
|
||||||
if (isImportant) {
|
if (isImportant) {
|
||||||
bean.important = true;
|
bean.important = true;
|
||||||
|
|
||||||
debug(`[${this.name}] sendNotification`);
|
if (Monitor.isImportantForNotification(isFirstBeat, previousBeat?.status, bean.status)) {
|
||||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
debug(`[${this.name}] sendNotification`);
|
||||||
|
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
debug(`[${this.name}] will not sendNotification because it is (or was) under maintenance`);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear Status Page Cache
|
// Clear Status Page Cache
|
||||||
debug(`[${this.name}] apicache clear`);
|
debug(`[${this.name}] apicache clear`);
|
||||||
|
@ -405,6 +422,8 @@ class Monitor extends BeanModel {
|
||||||
beatInterval = this.retryInterval;
|
beatInterval = this.retryInterval;
|
||||||
}
|
}
|
||||||
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||||
|
} else if (bean.status === MAINTENANCE) {
|
||||||
|
console.warn(`Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||||
}
|
}
|
||||||
|
@ -598,7 +617,7 @@ class Monitor extends BeanModel {
|
||||||
-- SUM all uptime duration, also trim off the beat out of time window
|
-- SUM all uptime duration, also trim off the beat out of time window
|
||||||
SUM(
|
SUM(
|
||||||
CASE
|
CASE
|
||||||
WHEN (status = 1)
|
WHEN (status = 1 OR status = 3)
|
||||||
THEN
|
THEN
|
||||||
CASE
|
CASE
|
||||||
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
||||||
|
@ -659,11 +678,42 @@ class Monitor extends BeanModel {
|
||||||
// DOWN -> PENDING = this case not exists
|
// DOWN -> PENDING = this case not exists
|
||||||
// DOWN -> DOWN = not important
|
// DOWN -> DOWN = not important
|
||||||
// * DOWN -> UP = important
|
// * DOWN -> UP = important
|
||||||
let isImportant = isFirstBeat ||
|
// MAINTENANCE -> MAINTENANCE = not important
|
||||||
|
// * MAINTENANCE -> UP = important
|
||||||
|
// * MAINTENANCE -> DOWN = important
|
||||||
|
// * DOWN -> MAINTENANCE = important
|
||||||
|
// * UP -> MAINTENANCE = important
|
||||||
|
return isFirstBeat ||
|
||||||
|
(previousBeatStatus === DOWN && currentBeatStatus === MAINTENANCE) ||
|
||||||
|
(previousBeatStatus === UP && currentBeatStatus === MAINTENANCE) ||
|
||||||
|
(previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) ||
|
||||||
|
(previousBeatStatus === MAINTENANCE && currentBeatStatus === UP) ||
|
||||||
|
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
|
||||||
|
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
|
||||||
|
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
static isImportantForNotification(isFirstBeat, previousBeatStatus, currentBeatStatus) {
|
||||||
|
// * ? -> 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
|
||||||
|
// MAINTENANCE -> MAINTENANCE = not important
|
||||||
|
// MAINTENANCE -> UP = not important
|
||||||
|
// * MAINTENANCE -> DOWN = important
|
||||||
|
// DOWN -> MAINTENANCE = not important
|
||||||
|
// UP -> MAINTENANCE = not important
|
||||||
|
return isFirstBeat ||
|
||||||
|
(previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) ||
|
||||||
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
|
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
|
||||||
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
|
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
|
||||||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
|
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
|
||||||
return isImportant;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async sendNotification(isFirstBeat, monitor, bean) {
|
static async sendNotification(isFirstBeat, monitor, bean) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ const server = require("../server");
|
||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
const Monitor = require("../model/monitor");
|
const Monitor = require("../model/monitor");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { UP, flipStatus, debug } = require("../../src/util");
|
const { UP, MAINTENANCE, flipStatus, debug} = require("../../src/util");
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
let cache = apicache.middleware;
|
let cache = apicache.middleware;
|
||||||
|
@ -51,6 +51,12 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maintenance = await R.getAll("SELECT mm.*, maintenance.start_date, maintenance.end_date FROM monitor_maintenance mm JOIN maintenance ON mm.maintenance_id = maintenance.id WHERE mm.monitor_id = ? AND datetime(maintenance.start_date) <= datetime('now', 'localtime') AND datetime(maintenance.end_date) >= datetime('now', 'localtime')", [monitor.id]);
|
||||||
|
if (maintenance.length !== 0) {
|
||||||
|
msg = "Monitor under maintenance";
|
||||||
|
status = MAINTENANCE;
|
||||||
|
}
|
||||||
|
|
||||||
debug("PreviousStatus: " + previousStatus);
|
debug("PreviousStatus: " + previousStatus);
|
||||||
debug("Current Status: " + status);
|
debug("Current Status: " + status);
|
||||||
|
|
||||||
|
@ -70,7 +76,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
ok: true,
|
ok: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (bean.important) {
|
if (Monitor.isImportantForNotification(isFirstBeat, previousStatus, status)) {
|
||||||
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +137,34 @@ router.get("/api/status-page/incident", async (_, response) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Status Page - Maintenance List
|
||||||
|
// Can fetch only if published
|
||||||
|
router.get("/api/status-page/maintenance-list", async (_request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await checkPublished();
|
||||||
|
const publicMaintenanceList = [];
|
||||||
|
|
||||||
|
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
|
||||||
|
SELECT maintenance.*
|
||||||
|
FROM maintenance
|
||||||
|
WHERE datetime(maintenance.start_date) <= datetime('now', 'localtime')
|
||||||
|
AND datetime(maintenance.end_date) >= datetime('now', 'localtime')
|
||||||
|
ORDER BY maintenance.end_date
|
||||||
|
`));
|
||||||
|
|
||||||
|
for (const bean of maintenanceBeanList) {
|
||||||
|
publicMaintenanceList.push(await bean.toPublicJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
response.json(publicMaintenanceList);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Status Page - Monitor List
|
// Status Page - Monitor List
|
||||||
// Can fetch only if published
|
// Can fetch only if published
|
||||||
router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => {
|
router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => {
|
||||||
|
|
217
server/server.js
217
server/server.js
|
@ -132,6 +132,7 @@ const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sen
|
||||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||||
const TwoFA = require("./2fa");
|
const TwoFA = require("./2fa");
|
||||||
|
const apicache = require("./modules/apicache");
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
@ -162,6 +163,12 @@ let jwtSecret = null;
|
||||||
*/
|
*/
|
||||||
let monitorList = {};
|
let monitorList = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main maintenance list
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
|
let maintenanceList = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show Setup Page
|
* Show Setup Page
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
@ -625,6 +632,101 @@ exports.entryPage = "dashboard";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add a new maintenance
|
||||||
|
socket.on("addMaintenance", async (maintenance, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
let bean = R.dispense("maintenance");
|
||||||
|
|
||||||
|
bean.import(maintenance);
|
||||||
|
bean.user_id = socket.userID;
|
||||||
|
let maintenanceID = await R.store(bean);
|
||||||
|
|
||||||
|
await sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Added Successfully.",
|
||||||
|
maintenanceID,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit a maintenance
|
||||||
|
socket.on("editMaintenance", async (maintenance, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let bean = await R.findOne("maintenance", " id = ? ", [ maintenance.id ]);
|
||||||
|
|
||||||
|
if (bean.user_id !== socket.userID) {
|
||||||
|
throw new Error("Permission denied.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.title = maintenance.title;
|
||||||
|
bean.description = maintenance.description;
|
||||||
|
bean.start_date = maintenance.start_date;
|
||||||
|
bean.end_date = maintenance.end_date;
|
||||||
|
|
||||||
|
await R.store(bean);
|
||||||
|
|
||||||
|
await sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Saved.",
|
||||||
|
maintenanceID: bean.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a new monitor_maintenance
|
||||||
|
socket.on("addMonitorMaintenance", async (maintenanceID, monitors, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM monitor_maintenance WHERE maintenance_id = ?", [
|
||||||
|
maintenanceID
|
||||||
|
]);
|
||||||
|
|
||||||
|
for await (const monitor of monitors) {
|
||||||
|
let bean = R.dispense("monitor_maintenance");
|
||||||
|
|
||||||
|
bean.import({
|
||||||
|
monitor_id: monitor.id,
|
||||||
|
maintenance_id: maintenanceID
|
||||||
|
});
|
||||||
|
await R.store(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
apicache.clear();
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Added Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("getMonitorList", async (callback) => {
|
socket.on("getMonitorList", async (callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
@ -641,6 +743,22 @@ exports.entryPage = "dashboard";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("getMaintenanceList", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
await sendMaintenanceList(socket);
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("getMonitor", async (monitorID, callback) => {
|
socket.on("getMonitor", async (monitorID, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
@ -665,6 +783,54 @@ exports.entryPage = "dashboard";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("getMaintenance", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
console.log(`Get Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
let bean = await R.findOne("maintenance", " id = ? AND user_id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
maintenance: await bean.toJSON(),
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("getMonitorMaintenance", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
console.log(`Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
let monitors = await R.getAll("SELECT monitor.id, monitor.name FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
monitors,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("getMonitorBeats", async (monitorID, period, callback) => {
|
socket.on("getMonitorBeats", async (monitorID, period, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
@ -769,6 +935,36 @@ exports.entryPage = "dashboard";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("deleteMaintenance", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
console.log(`Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
if (maintenanceID in maintenanceList) {
|
||||||
|
delete maintenanceList[maintenanceID];
|
||||||
|
}
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM maintenance WHERE id = ? AND user_id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Deleted Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("getTags", async (callback) => {
|
socket.on("getTags", async (callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
@ -1394,11 +1590,18 @@ async function sendMonitorList(socket) {
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendMaintenanceList(socket) {
|
||||||
|
let list = await getMaintenanceJSONList(socket.userID);
|
||||||
|
io.to(socket.userID).emit("maintenanceList", list);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
async function afterLogin(socket, user) {
|
async function afterLogin(socket, user) {
|
||||||
socket.userID = user.id;
|
socket.userID = user.id;
|
||||||
socket.join(user.id);
|
socket.join(user.id);
|
||||||
|
|
||||||
let monitorList = await sendMonitorList(socket);
|
let monitorList = await sendMonitorList(socket);
|
||||||
|
sendMaintenanceList(socket);
|
||||||
sendNotificationList(socket);
|
sendNotificationList(socket);
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
@ -1430,6 +1633,20 @@ async function getMonitorJSONList(userID) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getMaintenanceJSONList(userID) {
|
||||||
|
let result = {};
|
||||||
|
|
||||||
|
let maintenanceList = await R.find("maintenance", " user_id = ? ORDER BY end_date DESC, title", [
|
||||||
|
userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let maintenance of maintenanceList) {
|
||||||
|
result[maintenance.id] = await maintenance.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async function initDatabase(testMode = false) {
|
async function initDatabase(testMode = false) {
|
||||||
if (! fs.existsSync(Database.path)) {
|
if (! fs.existsSync(Database.path)) {
|
||||||
console.log("Copying Database");
|
console.log("Copying Database");
|
||||||
|
|
|
@ -273,6 +273,7 @@ textarea.form-control {
|
||||||
&.bg-info,
|
&.bg-info,
|
||||||
&.bg-warning,
|
&.bg-warning,
|
||||||
&.bg-danger,
|
&.bg-danger,
|
||||||
|
&.bg-maintenance,
|
||||||
&.bg-light {
|
&.bg-light {
|
||||||
color: $dark-font-color2;
|
color: $dark-font-color2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
$primary: #5cdd8b;
|
$primary: #5cdd8b;
|
||||||
$danger: #dc3545;
|
$danger: #dc3545;
|
||||||
$warning: #f8a306;
|
$warning: #f8a306;
|
||||||
|
$maintenance: #1747f5;
|
||||||
$link-color: #111;
|
$link-color: #111;
|
||||||
$border-radius: 50rem;
|
$border-radius: 50rem;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
v-for="(beat, index) in shortBeatList"
|
v-for="(beat, index) in shortBeatList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="beat"
|
class="beat"
|
||||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
|
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2), 'maintenance' : (beat.status === 3) }"
|
||||||
:style="beatStyle"
|
:style="beatStyle"
|
||||||
:title="getBeatTitle(beat)"
|
:title="getBeatTitle(beat)"
|
||||||
/>
|
/>
|
||||||
|
@ -200,6 +200,10 @@ export default {
|
||||||
background-color: $warning;
|
background-color: $warning;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.maintenance {
|
||||||
|
background-color: $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.empty):hover {
|
&:not(.empty):hover {
|
||||||
transition: all ease-in-out 0.15s;
|
transition: all ease-in-out 0.15s;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="shadow-box mb-3">
|
<div class="shadow-box mb-3">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div class="placeholder"></div>
|
<div class="search-wrapper float-start">
|
||||||
|
<select v-model="selectedList" class="form-control">
|
||||||
|
<option value="monitor" selected>{{$t('Monitor List')}}</option>
|
||||||
|
<option value="maintenance">{{$t('Maintenance List')}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="search-wrapper">
|
<div class="search-wrapper">
|
||||||
<a v-if="searchText == ''" class="search-icon">
|
<a v-if="searchText == ''" class="search-icon">
|
||||||
<font-awesome-icon icon="search" />
|
<font-awesome-icon icon="search" />
|
||||||
|
@ -13,11 +18,25 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
||||||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
<div v-if="Object.keys($root.monitorList).length === 0 && selectedList === 'monitor'" class="text-center mt-3">
|
||||||
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="Object.keys($root.maintenanceList).length === 0 && selectedList === 'maintenance'" class="text-center mt-3">
|
||||||
|
{{ $t("No Maintenance, please") }} <router-link to="/addMaintenance">{{ $t("add one") }}</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
<router-link v-if="selectedList === 'maintenance'" v-for="(item, index) in sortedMaintenanceList" :key="index" :to="maintenanceURL(item.id)" class="item" :class="{ 'disabled': (Date.parse(item.end_date) < Date.now()) }">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-9 col-md-8 small-padding">
|
||||||
|
<div class="info">
|
||||||
|
<Uptime :monitor="null" type="maintenance" :pill="true" />
|
||||||
|
{{ item.title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<router-link v-if="selectedList === 'monitor'" v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-9 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
<div class="col-9 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
|
@ -47,7 +66,7 @@
|
||||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||||
import Uptime from "../components/Uptime.vue";
|
import Uptime from "../components/Uptime.vue";
|
||||||
import Tag from "../components/Tag.vue";
|
import Tag from "../components/Tag.vue";
|
||||||
import { getMonitorRelativeURL } from "../util.ts";
|
import {getMaintenanceRelativeURL, getMonitorRelativeURL} from "../util.ts";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -63,9 +82,60 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchText: "",
|
searchText: "",
|
||||||
|
selectedList: "monitor"
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
sortedMaintenanceList() {
|
||||||
|
let result = Object.values(this.$root.maintenanceList);
|
||||||
|
|
||||||
|
result.sort((m1, m2) => {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (Date.parse(m1.end_date) >= now !== Date.parse(m2.end_date) >= now) {
|
||||||
|
if (Date.parse(m2.end_date) < now) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (Date.parse(m1.end_date) < now) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.parse(m1.end_date) >= now && Date.parse(m2.end_date) >= now) {
|
||||||
|
if (Date.parse(m1.end_date) < Date.parse(m2.end_date)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.parse(m2.end_date) < Date.parse(m1.end_date)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.parse(m1.end_date) < now && Date.parse(m2.end_date) < now) {
|
||||||
|
if (Date.parse(m1.end_date) < Date.parse(m2.end_date)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.parse(m2.end_date) < Date.parse(m1.end_date)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m1.title.localeCompare(m2.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simple filter by search text
|
||||||
|
// finds maintenance name
|
||||||
|
if (this.searchText !== "") {
|
||||||
|
const loweredSearchText = this.searchText.toLowerCase();
|
||||||
|
result = result.filter(maintenance => {
|
||||||
|
return maintenance.title.toLowerCase().includes(loweredSearchText)
|
||||||
|
|| maintenance.description.toLowerCase().includes(loweredSearchText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
sortedMonitorList() {
|
sortedMonitorList() {
|
||||||
let result = Object.values(this.$root.monitorList);
|
let result = Object.values(this.$root.monitorList);
|
||||||
|
|
||||||
|
@ -96,7 +166,7 @@ export default {
|
||||||
|
|
||||||
// Simple filter by search text
|
// Simple filter by search text
|
||||||
// finds monitor name, tag name or tag value
|
// finds monitor name, tag name or tag value
|
||||||
if (this.searchText != "") {
|
if (this.searchText !== "") {
|
||||||
const loweredSearchText = this.searchText.toLowerCase();
|
const loweredSearchText = this.searchText.toLowerCase();
|
||||||
result = result.filter(monitor => {
|
result = result.filter(monitor => {
|
||||||
return monitor.name.toLowerCase().includes(loweredSearchText)
|
return monitor.name.toLowerCase().includes(loweredSearchText)
|
||||||
|
@ -112,6 +182,9 @@ export default {
|
||||||
monitorURL(id) {
|
monitorURL(id) {
|
||||||
return getMonitorRelativeURL(id);
|
return getMonitorRelativeURL(id);
|
||||||
},
|
},
|
||||||
|
maintenanceURL(id) {
|
||||||
|
return getMaintenanceRelativeURL(id);
|
||||||
|
},
|
||||||
clearSearchText() {
|
clearSearchText() {
|
||||||
this.searchText = "";
|
this.searchText = "";
|
||||||
}
|
}
|
||||||
|
@ -174,4 +247,12 @@ export default {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-maintenance {
|
||||||
|
background-color: $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -24,7 +24,7 @@ import timezone from "dayjs/plugin/timezone";
|
||||||
import "chartjs-adapter-dayjs";
|
import "chartjs-adapter-dayjs";
|
||||||
import { LineChart } from "vue-chart-3";
|
import { LineChart } from "vue-chart-3";
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
import { UP, DOWN, PENDING } from "../util.ts";
|
import { UP, DOWN, PENDING, MAINTENANCE } from "../util.ts";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
@ -162,7 +162,8 @@ export default {
|
||||||
},
|
},
|
||||||
chartData() {
|
chartData() {
|
||||||
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
||||||
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down, 0 if target is up
|
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down (red color), under maintenance (blue color) or pending (orange color), 0 if target is up
|
||||||
|
let colorData = []; // Color Data for Bar Chart
|
||||||
|
|
||||||
let heartbeatList = this.heartbeatList ||
|
let heartbeatList = this.heartbeatList ||
|
||||||
(this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
|
(this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
|
||||||
|
@ -184,8 +185,9 @@ export default {
|
||||||
});
|
});
|
||||||
downData.push({
|
downData.push({
|
||||||
x,
|
x,
|
||||||
y: beat.status === DOWN ? 1 : 0,
|
y: (beat.status === DOWN || beat.status === MAINTENANCE || beat.status === PENDING) ? 1 : 0,
|
||||||
});
|
});
|
||||||
|
colorData.push((beat.status === MAINTENANCE) ? "rgba(23,71,245,0.41)" : ((beat.status === PENDING) ? "rgba(245,182,23,0.41)" : "#DC354568"))
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -204,7 +206,7 @@ export default {
|
||||||
type: "bar",
|
type: "bar",
|
||||||
data: downData,
|
data: downData,
|
||||||
borderColor: "#00000000",
|
borderColor: "#00000000",
|
||||||
backgroundColor: "#DC354568",
|
backgroundColor: colorData,
|
||||||
yAxisID: "y1",
|
yAxisID: "y1",
|
||||||
barThickness: "flex",
|
barThickness: "flex",
|
||||||
barPercentage: 1,
|
barPercentage: 1,
|
||||||
|
|
|
@ -146,4 +146,8 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-maintenance {
|
||||||
|
background-color: $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -22,6 +22,10 @@ export default {
|
||||||
return "warning";
|
return "warning";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.status === 3) {
|
||||||
|
return "maintenance";
|
||||||
|
}
|
||||||
|
|
||||||
return "secondary";
|
return "secondary";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -38,6 +42,10 @@ export default {
|
||||||
return this.$t("Pending");
|
return this.$t("Pending");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.status === 3) {
|
||||||
|
return this.$t("Maintenance");
|
||||||
|
}
|
||||||
|
|
||||||
return this.$t("Unknown");
|
return this.$t("Unknown");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,6 +16,10 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
uptime() {
|
uptime() {
|
||||||
|
|
||||||
|
if (this.type === "maintenance") {
|
||||||
|
return this.$t("Maintenance");
|
||||||
|
}
|
||||||
|
|
||||||
let key = this.monitor.id + "_" + this.type;
|
let key = this.monitor.id + "_" + this.type;
|
||||||
|
|
||||||
if (this.$root.uptimeList[key] !== undefined) {
|
if (this.$root.uptimeList[key] !== undefined) {
|
||||||
|
@ -26,6 +30,10 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
color() {
|
color() {
|
||||||
|
if (this.type === "maintenance" || this.monitor.maintenance) {
|
||||||
|
return "maintenance"
|
||||||
|
}
|
||||||
|
|
||||||
if (this.lastHeartBeat.status === 0) {
|
if (this.lastHeartBeat.status === 0) {
|
||||||
return "danger"
|
return "danger"
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import {
|
||||||
faAward,
|
faAward,
|
||||||
faLink,
|
faLink,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
|
faWrench,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
@ -67,6 +68,7 @@ library.add(
|
||||||
faAward,
|
faAward,
|
||||||
faLink,
|
faLink,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
|
faWrench,
|
||||||
);
|
);
|
||||||
|
|
||||||
export { FontAwesomeIcon };
|
export { FontAwesomeIcon };
|
||||||
|
|
|
@ -7,11 +7,13 @@ export default {
|
||||||
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
|
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
|
||||||
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
||||||
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
|
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
|
||||||
|
affectedMonitorsDescription: "Select monitors that are affected by current maintenance",
|
||||||
passwordNotMatchMsg: "The repeat password does not match.",
|
passwordNotMatchMsg: "The repeat password does not match.",
|
||||||
notificationDescription: "Notifications must be assigned to a monitor to function.",
|
notificationDescription: "Notifications must be assigned to a monitor to function.",
|
||||||
keywordDescription: "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
|
keywordDescription: "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
|
||||||
pauseDashboardHome: "Pause",
|
pauseDashboardHome: "Pause",
|
||||||
deleteMonitorMsg: "Are you sure want to delete this monitor?",
|
deleteMonitorMsg: "Are you sure want to delete this monitor?",
|
||||||
|
deleteMaintenanceMsg: "Are you sure want to delete this maintenance?",
|
||||||
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?",
|
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?",
|
||||||
resoverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.",
|
resoverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.",
|
||||||
rrtypeDescription: "Select the RR type you want to monitor",
|
rrtypeDescription: "Select the RR type you want to monitor",
|
||||||
|
|
|
@ -340,7 +340,6 @@ export default {
|
||||||
"No monitors available.": "沒有可用的監測器。",
|
"No monitors available.": "沒有可用的監測器。",
|
||||||
"Add one": "新增一個",
|
"Add one": "新增一個",
|
||||||
"No Monitors": "無監測器",
|
"No Monitors": "無監測器",
|
||||||
"Add one": "新增一個",
|
|
||||||
"Untitled Group": "未命名群組",
|
"Untitled Group": "未命名群組",
|
||||||
Services: "服務",
|
Services: "服務",
|
||||||
Discard: "捨棄",
|
Discard: "捨棄",
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
|
|
||||||
<!-- Mobile Only -->
|
<!-- Mobile Only -->
|
||||||
<div v-if="$root.isMobile" style="width: 100%; height: 60px;" />
|
<div v-if="$root.isMobile" style="width: 100%; height: 60px;" />
|
||||||
<nav v-if="$root.isMobile" class="bottom-nav">
|
<nav v-if="$root.isMobile" class="bottom-nav scroll">
|
||||||
<router-link to="/dashboard" class="nav-link">
|
<router-link to="/dashboard" class="nav-link">
|
||||||
<div><font-awesome-icon icon="tachometer-alt" /></div>
|
<div><font-awesome-icon icon="tachometer-alt" /></div>
|
||||||
{{ $t("Dashboard") }}
|
{{ $t("Dashboard") }}
|
||||||
|
@ -64,7 +64,12 @@
|
||||||
|
|
||||||
<router-link to="/add" class="nav-link">
|
<router-link to="/add" class="nav-link">
|
||||||
<div><font-awesome-icon icon="plus" /></div>
|
<div><font-awesome-icon icon="plus" /></div>
|
||||||
{{ $t("Add") }}
|
{{ $t("Add Monitor") }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<router-link to="/addMaintenance" class="nav-link">
|
||||||
|
<div><font-awesome-icon icon="wrench" /></div>
|
||||||
|
{{ $t("Add Maintenance") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/settings" class="nav-link">
|
<router-link to="/settings" class="nav-link">
|
||||||
|
@ -201,4 +206,21 @@ main {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scroll {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll a {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -22,6 +22,16 @@ export default {
|
||||||
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
|
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
datetimeMaintenance(value) {
|
||||||
|
const inputDate = new Date(value);
|
||||||
|
const now = new Date(Date.now());
|
||||||
|
|
||||||
|
if (inputDate.getFullYear() === now.getFullYear() && inputDate.getMonth() === now.getMonth() && inputDate.getDay() === now.getDay())
|
||||||
|
return this.datetimeMaintenanceFormat(value, "HH:mm");
|
||||||
|
else
|
||||||
|
return this.datetimeMaintenanceFormat(value, "YYYY-MM-DD HH:mm");
|
||||||
|
},
|
||||||
|
|
||||||
date(value) {
|
date(value) {
|
||||||
return this.datetimeFormat(value, "YYYY-MM-DD");
|
return this.datetimeFormat(value, "YYYY-MM-DD");
|
||||||
},
|
},
|
||||||
|
@ -41,6 +51,13 @@ export default {
|
||||||
return dayjs.utc(value).tz(this.timezone).format(format);
|
return dayjs.utc(value).tz(this.timezone).format(format);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
|
},
|
||||||
|
|
||||||
|
datetimeMaintenanceFormat(value, format) {
|
||||||
|
if (value !== undefined && value !== "") {
|
||||||
|
return dayjs(value).format(format);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ export default {
|
||||||
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
monitorList: { },
|
monitorList: { },
|
||||||
|
maintenanceList: { },
|
||||||
heartbeatList: { },
|
heartbeatList: { },
|
||||||
importantHeartbeatList: { },
|
importantHeartbeatList: { },
|
||||||
avgPingList: { },
|
avgPingList: { },
|
||||||
|
@ -99,6 +100,10 @@ export default {
|
||||||
this.monitorList = data;
|
this.monitorList = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("maintenanceList", (data) => {
|
||||||
|
this.maintenanceList = data;
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("notificationList", (data) => {
|
socket.on("notificationList", (data) => {
|
||||||
this.notificationList = data;
|
this.notificationList = data;
|
||||||
});
|
});
|
||||||
|
@ -309,14 +314,37 @@ export default {
|
||||||
socket.emit("getMonitorList", callback);
|
socket.emit("getMonitorList", callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getMaintenanceList(callback) {
|
||||||
|
if (! callback) {
|
||||||
|
callback = () => { };
|
||||||
|
}
|
||||||
|
socket.emit("getMaintenanceList", callback);
|
||||||
|
},
|
||||||
|
|
||||||
add(monitor, callback) {
|
add(monitor, callback) {
|
||||||
socket.emit("add", monitor, callback);
|
socket.emit("add", monitor, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addMaintenance(maintenance, callback) {
|
||||||
|
socket.emit("addMaintenance", maintenance, callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
addMonitorMaintenance(maintenanceID, monitors, callback) {
|
||||||
|
socket.emit("addMonitorMaintenance", maintenanceID, monitors, callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
getMonitorMaintenance(maintenanceID, callback) {
|
||||||
|
socket.emit("getMonitorMaintenance", maintenanceID, callback);
|
||||||
|
},
|
||||||
|
|
||||||
deleteMonitor(monitorID, callback) {
|
deleteMonitor(monitorID, callback) {
|
||||||
socket.emit("deleteMonitor", monitorID, callback);
|
socket.emit("deleteMonitor", monitorID, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteMaintenance(maintenanceID, callback) {
|
||||||
|
socket.emit("deleteMaintenance", maintenanceID, callback);
|
||||||
|
},
|
||||||
|
|
||||||
clearData() {
|
clearData() {
|
||||||
console.log("reset heartbeat list");
|
console.log("reset heartbeat list");
|
||||||
this.heartbeatList = {};
|
this.heartbeatList = {};
|
||||||
|
@ -368,7 +396,13 @@ export default {
|
||||||
for (let monitorID in this.lastHeartbeatList) {
|
for (let monitorID in this.lastHeartbeatList) {
|
||||||
let lastHeartBeat = this.lastHeartbeatList[monitorID];
|
let lastHeartBeat = this.lastHeartbeatList[monitorID];
|
||||||
|
|
||||||
if (! lastHeartBeat) {
|
if (this.monitorList[monitorID].maintenance) {
|
||||||
|
result[monitorID] = {
|
||||||
|
text: this.$t("Maintenance"),
|
||||||
|
color: "maintenance",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (! lastHeartBeat) {
|
||||||
result[monitorID] = unknown;
|
result[monitorID] = unknown;
|
||||||
} else if (lastHeartBeat.status === 1) {
|
} else if (lastHeartBeat.status === 1) {
|
||||||
result[monitorID] = {
|
result[monitorID] = {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
|
<div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
|
||||||
<div>
|
<div>
|
||||||
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
|
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
|
||||||
|
<router-link to="/addMaintenance" class="btn btn-primary mb-3 float-end"><font-awesome-icon icon="wrench" /> {{ $t("Add New Maintenance") }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
<MonitorList :scrollbar="true" />
|
<MonitorList :scrollbar="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,6 +15,10 @@
|
||||||
<h3>{{ $t("Down") }}</h3>
|
<h3>{{ $t("Down") }}</h3>
|
||||||
<span class="num text-danger">{{ stats.down }}</span>
|
<span class="num text-danger">{{ stats.down }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<h3>{{ $t("Maintenance") }}</h3>
|
||||||
|
<span class="num text-maintenance">{{ stats.maintenance }}</span>
|
||||||
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3>{{ $t("Unknown") }}</h3>
|
<h3>{{ $t("Unknown") }}</h3>
|
||||||
<span class="num text-secondary">{{ stats.unknown }}</span>
|
<span class="num text-secondary">{{ stats.unknown }}</span>
|
||||||
|
@ -38,7 +42,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}">
|
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}">
|
||||||
<td><router-link :to="`/dashboard/${beat.monitorID}`">{{ beat.name }}</router-link></td>
|
<td><router-link :to="`/dashboard/monitor/${beat.monitorID}`">{{ beat.name }}</router-link></td>
|
||||||
<td><Status :status="beat.status" /></td>
|
<td><Status :status="beat.status" /></td>
|
||||||
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
|
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
|
||||||
<td class="border-0">{{ beat.msg }}</td>
|
<td class="border-0">{{ beat.msg }}</td>
|
||||||
|
@ -93,6 +97,7 @@ export default {
|
||||||
let result = {
|
let result = {
|
||||||
up: 0,
|
up: 0,
|
||||||
down: 0,
|
down: 0,
|
||||||
|
maintenance: 0,
|
||||||
unknown: 0,
|
unknown: 0,
|
||||||
pause: 0,
|
pause: 0,
|
||||||
};
|
};
|
||||||
|
@ -101,7 +106,10 @@ export default {
|
||||||
let beat = this.$root.lastHeartbeatList[monitorID];
|
let beat = this.$root.lastHeartbeatList[monitorID];
|
||||||
let monitor = this.$root.monitorList[monitorID];
|
let monitor = this.$root.monitorList[monitorID];
|
||||||
|
|
||||||
if (monitor && ! monitor.active) {
|
if (monitor && monitor.maintenance) {
|
||||||
|
result.maintenance++;
|
||||||
|
}
|
||||||
|
else if (monitor && !monitor.active) {
|
||||||
result.pause++;
|
result.pause++;
|
||||||
} else if (beat) {
|
} else if (beat) {
|
||||||
if (beat.status === 1) {
|
if (beat.status === 1) {
|
||||||
|
@ -173,6 +181,14 @@ export default {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-maintenance {
|
||||||
|
color: $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-maintenance {
|
||||||
|
background-color: $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
.shadow-box {
|
.shadow-box {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -499,4 +499,8 @@ table {
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-maintenance {
|
||||||
|
background-color: $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
247
src/pages/EditMaintenance.vue
Normal file
247
src/pages/EditMaintenance.vue
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
<template>
|
||||||
|
<transition name="slide-fade" appear>
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-3">{{ pageName }}</h1>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<div class="shadow-box">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h2 class="mb-2">{{ $t("General") }}</h2>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="name" class="form-label">{{ $t("Title") }}</label>
|
||||||
|
<input id="name" v-model="maintenance.title" type="text" class="form-control"
|
||||||
|
:placeholder="titlePlaceholder" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="description" class="form-label">{{ $t("Description") }}</label>
|
||||||
|
<textarea id="description" v-model="maintenance.description" class="form-control"
|
||||||
|
:placeholder="descriptionPlaceholder"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Affected Monitors -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="affected_monitors" class="form-label">{{ $t("Affected Monitors") }}</label>
|
||||||
|
|
||||||
|
<VueMultiselect
|
||||||
|
id="affected_monitors"
|
||||||
|
v-model="affectedMonitors"
|
||||||
|
:options="affectedMonitorsOptions"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:multiple="true"
|
||||||
|
:allow-empty="false"
|
||||||
|
:close-on-select="false"
|
||||||
|
:clear-on-select="false"
|
||||||
|
:preserve-search="true"
|
||||||
|
:placeholder="$t('Pick Affected Monitors...')"
|
||||||
|
:preselect-first="false"
|
||||||
|
:max-height="600"
|
||||||
|
:taggable="false"
|
||||||
|
></VueMultiselect>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("affectedMonitorsDescription") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Start Date Time -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="start_date" class="form-label">{{ $t("Start of maintenance") }}</label>
|
||||||
|
<input :type="'datetime-local'" id="start_date" v-model="maintenance.start_date"
|
||||||
|
class="form-control" :class="{'darkCalendar': dark }" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- End Date Time -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="end_date" class="form-label">{{ $t("Expected end of maintenance") }}</label>
|
||||||
|
<input :type="'datetime-local'" id="end_date" v-model="maintenance.end_date"
|
||||||
|
class="form-control" :class="{'darkCalendar': dark }" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 mb-1">
|
||||||
|
<button id="monitor-submit-btn" class="btn btn-primary" type="submit"
|
||||||
|
:disabled="processing">{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CopyableInput from "../components/CopyableInput.vue";
|
||||||
|
|
||||||
|
import {useToast} from "vue-toastification";
|
||||||
|
import VueMultiselect from "vue-multiselect";
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CopyableInput,
|
||||||
|
VueMultiselect,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
maintenance: {},
|
||||||
|
affectedMonitors: [],
|
||||||
|
affectedMonitorsOptions: [],
|
||||||
|
dark: (this.$root.theme === "dark"),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
pageName() {
|
||||||
|
return this.$t((this.isAdd) ? "Schedule maintenance" : "Edit");
|
||||||
|
},
|
||||||
|
|
||||||
|
isAdd() {
|
||||||
|
return this.$route.path === "/addMaintenance";
|
||||||
|
},
|
||||||
|
|
||||||
|
isEdit() {
|
||||||
|
return this.$route.path.startsWith("/editMaintenance");
|
||||||
|
},
|
||||||
|
|
||||||
|
titlePlaceholder() {
|
||||||
|
return this.$t("Network infrastructure maintenance");
|
||||||
|
},
|
||||||
|
|
||||||
|
descriptionPlaceholder() {
|
||||||
|
return this.$t("Example: Network infrastructure maintenance is underway which will affect some of our services.");
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
|
||||||
|
"$route.fullPath"() {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init();
|
||||||
|
|
||||||
|
this.$root.getMonitorList((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
Object.values(this.$root.monitorList).map(monitor => {
|
||||||
|
this.affectedMonitorsOptions.push({
|
||||||
|
id: monitor.id,
|
||||||
|
name: monitor.name
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
this.affectedMonitors = [];
|
||||||
|
|
||||||
|
if (this.isAdd) {
|
||||||
|
this.maintenance = {
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
start_date: "",
|
||||||
|
end_date: "",
|
||||||
|
};
|
||||||
|
} else if (this.isEdit) {
|
||||||
|
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.maintenance = res.maintenance;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
Object.values(res.monitors).map(monitor => {
|
||||||
|
this.affectedMonitors.push(monitor);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
if (this.affectedMonitors.length === 0) {
|
||||||
|
toast.error(this.$t("Select at least one affected monitor"));
|
||||||
|
return this.processing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAdd) {
|
||||||
|
this.$root.addMaintenance(this.maintenance, async (res) => {
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
await this.addMonitorMaintenance(res.maintenanceID, () => {
|
||||||
|
toast.success(res.msg);
|
||||||
|
this.processing = false;
|
||||||
|
this.$root.getMaintenanceList();
|
||||||
|
this.$router.push("/dashboard/maintenance/" + res.maintenanceID);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
this.processing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$root.getSocket().emit("editMaintenance", this.maintenance, async (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
await this.addMonitorMaintenance(res.maintenanceID, () => {
|
||||||
|
this.processing = false;
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.init();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.processing = false;
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async addMonitorMaintenance(maintenanceID, callback) {
|
||||||
|
await this.$root.addMonitorMaintenance(maintenanceID, this.affectedMonitors, async (res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
} else {
|
||||||
|
this.$root.getMonitorList();
|
||||||
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.shadow-box {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkCalendar::-webkit-calendar-picker-indicator {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -509,7 +509,7 @@ export default {
|
||||||
toast.success(res.msg);
|
toast.success(res.msg);
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.$root.getMonitorList();
|
this.$root.getMonitorList();
|
||||||
this.$router.push("/dashboard/" + res.monitorID);
|
this.$router.push("/dashboard/monitor/" + res.monitorID);
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
|
141
src/pages/MaintenanceDetails.vue
Normal file
141
src/pages/MaintenanceDetails.vue
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
<template>
|
||||||
|
<transition name="slide-fade" appear>
|
||||||
|
<div v-if="maintenance">
|
||||||
|
<h1> {{ maintenance.title }}</h1>
|
||||||
|
<p class="url">
|
||||||
|
<span>Start: {{ $root.datetimeMaintenance(maintenance.start_date) }}</span>
|
||||||
|
<br>
|
||||||
|
<span>End: {{ $root.datetimeMaintenance(maintenance.end_date) }}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="functions" style="margin-top: 10px">
|
||||||
|
<router-link :to=" '/editMaintenance/' + maintenance.id " class="btn btn-secondary">
|
||||||
|
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
||||||
|
</router-link>
|
||||||
|
<button class="btn btn-danger" @click="deleteDialog">
|
||||||
|
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="description" class="form-label" style="margin-top: 20px">{{ $t("Description") }}</label>
|
||||||
|
<textarea id="description" class="form-control" disabled>{{ maintenance.description }}</textarea>
|
||||||
|
|
||||||
|
<label for="affected_monitors" class="form-label" style="margin-top: 20px">{{ $t("Affected Monitors") }}</label>
|
||||||
|
<br>
|
||||||
|
<button v-for="monitor in this.affectedMonitors" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: bold">
|
||||||
|
{{ monitor }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMaintenance">
|
||||||
|
{{ $t("deleteMaintenanceMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
const toast = useToast();
|
||||||
|
import Confirm from "../components/Confirm.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
affectedMonitors: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
maintenance() {
|
||||||
|
let id = this.$route.params.id;
|
||||||
|
return this.$root.maintenanceList[id];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.affectedMonitors = Object.values(res.monitors).map(monitor => monitor.name);
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteDialog() {
|
||||||
|
this.$refs.confirmDelete.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteMaintenance() {
|
||||||
|
this.$root.deleteMaintenance(this.maintenance.id, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success(res.msg);
|
||||||
|
this.$router.push("/dashboard");
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
@media (max-width: 550px) {
|
||||||
|
.functions {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
button, a {
|
||||||
|
margin-left: 10px !important;
|
||||||
|
margin-right: 10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.btn {
|
||||||
|
padding-left: 25px;
|
||||||
|
padding-right: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.url {
|
||||||
|
color: $primary;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.functions {
|
||||||
|
button, a {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-monitor {
|
||||||
|
background-color: #5cdd8b;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -144,6 +144,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Maintenance -->
|
||||||
|
<div v-if="maintenance.length !== 0" v-for="maintenanceItem in maintenance" class="shadow-box alert mb-4 p-4 maintenance" role="alert" :class="maintenanceClass">
|
||||||
|
<h4 v-text="maintenanceItem.title" class="alert-heading" />
|
||||||
|
|
||||||
|
<div v-text="maintenanceItem.description" class="content" />
|
||||||
|
|
||||||
|
<!-- Incident Date -->
|
||||||
|
<div class="date mt-3">
|
||||||
|
{{ $t("End") }}: {{ $root.datetimeMaintenance(maintenanceItem.end_date) }} ({{ dateFromNowMaintenance(maintenanceItem.start_date) }})<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Overall Status -->
|
<!-- Overall Status -->
|
||||||
<div class="shadow-box list p-4 overall-status mb-4">
|
<div class="shadow-box list p-4 overall-status mb-4">
|
||||||
<div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData">
|
<div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData">
|
||||||
|
@ -167,6 +179,11 @@
|
||||||
{{ $t("Degraded Service") }}
|
{{ $t("Degraded Service") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="isMaintenance">
|
||||||
|
<font-awesome-icon icon="wrench" class="statusMaintenance" />
|
||||||
|
{{ $t("Maintenance") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<font-awesome-icon icon="question-circle" style="color: #efefef;" />
|
<font-awesome-icon icon="question-circle" style="color: #efefef;" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -217,7 +234,14 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import PublicGroupList from "../components/PublicGroupList.vue";
|
import PublicGroupList from "../components/PublicGroupList.vue";
|
||||||
import ImageCropUpload from "vue-image-crop-upload";
|
import ImageCropUpload from "vue-image-crop-upload";
|
||||||
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts";
|
import {
|
||||||
|
STATUS_PAGE_ALL_DOWN,
|
||||||
|
STATUS_PAGE_ALL_UP,
|
||||||
|
STATUS_PAGE_MAINTENANCE,
|
||||||
|
STATUS_PAGE_PARTIAL_DOWN,
|
||||||
|
UP,
|
||||||
|
MAINTENANCE
|
||||||
|
} from "../util.ts";
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
@ -259,6 +283,7 @@ export default {
|
||||||
loadedTheme: false,
|
loadedTheme: false,
|
||||||
loadedData: false,
|
loadedData: false,
|
||||||
baseURL: "",
|
baseURL: "",
|
||||||
|
maintenance: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -320,6 +345,10 @@ export default {
|
||||||
return "bg-" + this.incident.style;
|
return "bg-" + this.incident.style;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
maintenanceClass() {
|
||||||
|
return "bg-maintenance";
|
||||||
|
},
|
||||||
|
|
||||||
overallStatus() {
|
overallStatus() {
|
||||||
|
|
||||||
if (Object.keys(this.$root.publicLastHeartbeatList).length === 0) {
|
if (Object.keys(this.$root.publicLastHeartbeatList).length === 0) {
|
||||||
|
@ -332,7 +361,10 @@ export default {
|
||||||
for (let id in this.$root.publicLastHeartbeatList) {
|
for (let id in this.$root.publicLastHeartbeatList) {
|
||||||
let beat = this.$root.publicLastHeartbeatList[id];
|
let beat = this.$root.publicLastHeartbeatList[id];
|
||||||
|
|
||||||
if (beat.status === UP) {
|
if (beat.status === MAINTENANCE) {
|
||||||
|
return STATUS_PAGE_MAINTENANCE;
|
||||||
|
}
|
||||||
|
else if (beat.status === UP) {
|
||||||
hasUp = true;
|
hasUp = true;
|
||||||
} else {
|
} else {
|
||||||
status = STATUS_PAGE_PARTIAL_DOWN;
|
status = STATUS_PAGE_PARTIAL_DOWN;
|
||||||
|
@ -358,6 +390,10 @@ export default {
|
||||||
return this.overallStatus === STATUS_PAGE_ALL_DOWN;
|
return this.overallStatus === STATUS_PAGE_ALL_DOWN;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isMaintenance() {
|
||||||
|
return this.overallStatus === STATUS_PAGE_MAINTENANCE;
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
||||||
|
@ -423,6 +459,10 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
axios.get("/api/status-page/maintenance-list").then((res) => {
|
||||||
|
this.maintenance = res.data;
|
||||||
|
});
|
||||||
|
|
||||||
axios.get("/api/status-page/monitor-list").then((res) => {
|
axios.get("/api/status-page/monitor-list").then((res) => {
|
||||||
this.$root.publicGroupList = res.data;
|
this.$root.publicGroupList = res.data;
|
||||||
});
|
});
|
||||||
|
@ -580,6 +620,10 @@ export default {
|
||||||
return dayjs.utc(date).fromNow();
|
return dayjs.utc(date).fromNow();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dateFromNowMaintenance(date) {
|
||||||
|
return dayjs(date).fromNow();
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -671,6 +715,22 @@ footer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maintenance {
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-maintenance {
|
||||||
|
background-color: $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusMaintenance {
|
||||||
|
color: $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile {
|
.mobile {
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Dashboard from "./pages/Dashboard.vue";
|
||||||
import DashboardHome from "./pages/DashboardHome.vue";
|
import DashboardHome from "./pages/DashboardHome.vue";
|
||||||
import Details from "./pages/Details.vue";
|
import Details from "./pages/Details.vue";
|
||||||
import EditMonitor from "./pages/EditMonitor.vue";
|
import EditMonitor from "./pages/EditMonitor.vue";
|
||||||
|
import EditMaintenance from "./pages/EditMaintenance.vue";
|
||||||
import List from "./pages/List.vue";
|
import List from "./pages/List.vue";
|
||||||
const Settings = () => import("./pages/Settings.vue");
|
const Settings = () => import("./pages/Settings.vue");
|
||||||
import Setup from "./pages/Setup.vue";
|
import Setup from "./pages/Setup.vue";
|
||||||
|
@ -18,6 +19,7 @@ import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
||||||
import Security from "./components/settings/Security.vue";
|
import Security from "./components/settings/Security.vue";
|
||||||
import Backup from "./components/settings/Backup.vue";
|
import Backup from "./components/settings/Backup.vue";
|
||||||
import About from "./components/settings/About.vue";
|
import About from "./components/settings/About.vue";
|
||||||
|
import MaintenanceDetails from "./pages/MaintenanceDetails.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
@ -41,7 +43,7 @@ const routes = [
|
||||||
component: DashboardHome,
|
component: DashboardHome,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "/dashboard/:id",
|
path: "/dashboard/monitor/:id",
|
||||||
component: EmptyLayout,
|
component: EmptyLayout,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
@ -54,10 +56,28 @@ const routes = [
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/dashboard/maintenance/:id",
|
||||||
|
component: EmptyLayout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: MaintenanceDetails,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/editMaintenance/:id",
|
||||||
|
component: EditMaintenance,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/add",
|
path: "/add",
|
||||||
component: EditMonitor,
|
component: EditMonitor,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/addMaintenance",
|
||||||
|
component: EditMaintenance,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/list",
|
path: "/list",
|
||||||
component: List,
|
component: List,
|
||||||
|
|
10
src/util.js
10
src/util.js
|
@ -7,7 +7,7 @@
|
||||||
// Backend uses the compiled file util.js
|
// Backend uses the compiled file util.js
|
||||||
// Frontend uses util.ts
|
// Frontend uses util.ts
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
|
exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
|
||||||
const _dayjs = require("dayjs");
|
const _dayjs = require("dayjs");
|
||||||
const dayjs = _dayjs;
|
const dayjs = _dayjs;
|
||||||
exports.isDev = process.env.NODE_ENV === "development";
|
exports.isDev = process.env.NODE_ENV === "development";
|
||||||
|
@ -15,9 +15,11 @@ exports.appName = "Uptime Kuma";
|
||||||
exports.DOWN = 0;
|
exports.DOWN = 0;
|
||||||
exports.UP = 1;
|
exports.UP = 1;
|
||||||
exports.PENDING = 2;
|
exports.PENDING = 2;
|
||||||
|
exports.MAINTENANCE = 3;
|
||||||
exports.STATUS_PAGE_ALL_DOWN = 0;
|
exports.STATUS_PAGE_ALL_DOWN = 0;
|
||||||
exports.STATUS_PAGE_ALL_UP = 1;
|
exports.STATUS_PAGE_ALL_UP = 1;
|
||||||
exports.STATUS_PAGE_PARTIAL_DOWN = 2;
|
exports.STATUS_PAGE_PARTIAL_DOWN = 2;
|
||||||
|
exports.STATUS_PAGE_MAINTENANCE = 3;
|
||||||
function flipStatus(s) {
|
function flipStatus(s) {
|
||||||
if (s === exports.UP) {
|
if (s === exports.UP) {
|
||||||
return exports.DOWN;
|
return exports.DOWN;
|
||||||
|
@ -162,6 +164,10 @@ function genSecret(length = 64) {
|
||||||
}
|
}
|
||||||
exports.genSecret = genSecret;
|
exports.genSecret = genSecret;
|
||||||
function getMonitorRelativeURL(id) {
|
function getMonitorRelativeURL(id) {
|
||||||
return "/dashboard/" + id;
|
return "/dashboard/monitor/" + id;
|
||||||
}
|
}
|
||||||
exports.getMonitorRelativeURL = getMonitorRelativeURL;
|
exports.getMonitorRelativeURL = getMonitorRelativeURL;
|
||||||
|
function getMaintenanceRelativeURL(id) {
|
||||||
|
return "/dashboard/maintenance/" + id;
|
||||||
|
}
|
||||||
|
exports.getMaintenanceRelativeURL = getMaintenanceRelativeURL;
|
||||||
|
|
|
@ -14,10 +14,12 @@ export const appName = "Uptime Kuma";
|
||||||
export const DOWN = 0;
|
export const DOWN = 0;
|
||||||
export const UP = 1;
|
export const UP = 1;
|
||||||
export const PENDING = 2;
|
export const PENDING = 2;
|
||||||
|
export const MAINTENANCE = 3;
|
||||||
|
|
||||||
export const STATUS_PAGE_ALL_DOWN = 0;
|
export const STATUS_PAGE_ALL_DOWN = 0;
|
||||||
export const STATUS_PAGE_ALL_UP = 1;
|
export const STATUS_PAGE_ALL_UP = 1;
|
||||||
export const STATUS_PAGE_PARTIAL_DOWN = 2;
|
export const STATUS_PAGE_PARTIAL_DOWN = 2;
|
||||||
|
export const STATUS_PAGE_MAINTENANCE = 3;
|
||||||
|
|
||||||
|
|
||||||
export function flipStatus(s: number) {
|
export function flipStatus(s: number) {
|
||||||
|
@ -185,5 +187,9 @@ export function genSecret(length = 64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMonitorRelativeURL(id: string) {
|
export function getMonitorRelativeURL(id: string) {
|
||||||
return "/dashboard/" + id;
|
return "/dashboard/monitor/" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMaintenanceRelativeURL(id: string) {
|
||||||
|
return "/dashboard/maintenance/" + id;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue