From 0d3414c6d6089f7b41f6bb4b1729f01ab2cb5e62 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?= <karel.kryda@gmail.com>
Date: Sun, 23 Jan 2022 15:22:00 +0100
Subject: [PATCH 001/134] A complete maintenance planning system has been
 created

---
 db/patch-maintenance-table.sql     |  25 +++
 package-lock.json                  |  54 ++++---
 server/database.js                 |   1 +
 server/model/heartbeat.js          |   1 +
 server/model/maintenance.js        |  38 +++++
 server/model/monitor.js            |  64 +++++++-
 server/routers/api-router.js       |  38 ++++-
 server/server.js                   | 217 +++++++++++++++++++++++++
 src/assets/app.scss                |   1 +
 src/assets/vars.scss               |   1 +
 src/components/HeartbeatBar.vue    |   6 +-
 src/components/MonitorList.vue     |  91 ++++++++++-
 src/components/PingChart.vue       |  10 +-
 src/components/PublicGroupList.vue |   4 +
 src/components/Status.vue          |   8 +
 src/components/Uptime.vue          |   8 +
 src/icon.js                        |   2 +
 src/languages/en.js                |   2 +
 src/languages/zh-TW.js             |   1 -
 src/layouts/Layout.vue             |  26 ++-
 src/mixins/datetime.js             |  17 ++
 src/mixins/socket.js               |  36 ++++-
 src/pages/Dashboard.vue            |   1 +
 src/pages/DashboardHome.vue        |  22 ++-
 src/pages/Details.vue              |   4 +
 src/pages/EditMaintenance.vue      | 247 +++++++++++++++++++++++++++++
 src/pages/EditMonitor.vue          |   2 +-
 src/pages/MaintenanceDetails.vue   | 141 ++++++++++++++++
 src/pages/StatusPage.vue           |  64 +++++++-
 src/router.js                      |  22 ++-
 src/util.js                        |  10 +-
 src/util.ts                        |   8 +-
 32 files changed, 1121 insertions(+), 51 deletions(-)
 create mode 100644 db/patch-maintenance-table.sql
 create mode 100644 server/model/maintenance.js
 create mode 100644 src/pages/EditMaintenance.vue
 create mode 100644 src/pages/MaintenanceDetails.vue

diff --git a/db/patch-maintenance-table.sql b/db/patch-maintenance-table.sql
new file mode 100644
index 00000000..ee4a7f88
--- /dev/null
+++ b/db/patch-maintenance-table.sql
@@ -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;
diff --git a/package-lock.json b/package-lock.json
index fc21a63f..7ab00b75 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14914,7 +14914,8 @@
         "@fortawesome/vue-fontawesome": {
             "version": "3.0.0-5",
             "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": {
             "version": "1.1.2",
@@ -16117,7 +16118,8 @@
             "version": "1.9.4",
             "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.9.4.tgz",
             "integrity": "sha512-0CZqaCoChriPTTtGkERy1LGPcYjGFpi2uYRhBPIkqJqUGV5JnJFhQAgh6oH9j5XZHfrRaisX8W0xSpO4T7S78A==",
-            "dev": true
+            "dev": true,
+            "requires": {}
         },
         "@vue/compiler-core": {
             "version": "3.2.22",
@@ -16277,7 +16279,8 @@
             "version": "5.3.2",
             "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
             "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
-            "dev": true
+            "dev": true,
+            "requires": {}
         },
         "acorn-walk": {
             "version": "7.2.0",
@@ -16766,7 +16769,8 @@
         "bootstrap": {
             "version": "5.1.3",
             "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": {
             "version": "1.1.11",
@@ -16958,7 +16962,8 @@
         "chartjs-adapter-dayjs": {
             "version": "1.0.0",
             "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": {
             "version": "2.0.3",
@@ -17548,7 +17553,8 @@
                 "ws": {
                     "version": "8.2.3",
                     "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": {
                     "version": "8.2.3",
                     "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",
             "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
             "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
-            "dev": true
+            "dev": true,
+            "requires": {}
         },
         "jest-puppeteer": {
             "version": "6.0.0",
@@ -21774,12 +21782,14 @@
             "version": "6.0.0",
             "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz",
             "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==",
-            "dev": true
+            "dev": true,
+            "requires": {}
         },
         "postcss-scss": {
             "version": "4.0.2",
             "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.2.tgz",
-            "integrity": "sha512-xfdkU128CkKKKVAwkyt0M8OdnelJ3MRcIRAPPQkRpoPeuzWY3RIeg7piRCpZ79MK7Q16diLXMMAD9dN5mauPlQ=="
+            "integrity": "sha512-xfdkU128CkKKKVAwkyt0M8OdnelJ3MRcIRAPPQkRpoPeuzWY3RIeg7piRCpZ79MK7Q16diLXMMAD9dN5mauPlQ==",
+            "requires": {}
         },
         "postcss-selector-parser": {
             "version": "6.0.8",
@@ -21979,7 +21989,8 @@
                     "version": "7.4.6",
                     "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
                     "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
-                    "dev": true
+                    "dev": true,
+                    "requires": {}
                 }
             }
         },
@@ -23080,7 +23091,8 @@
             "version": "6.0.0",
             "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-6.0.0.tgz",
             "integrity": "sha512-ZorSSdyMcxWpROYUvLEMm0vSZud2uB7tX1hzBZwvVY9SV/uly4AvvJPPhCcymZL3fcQhEQG5AELmrxWqtmzacw==",
-            "dev": true
+            "dev": true,
+            "requires": {}
         },
         "stylelint-config-standard": {
             "version": "24.0.0",
@@ -23653,17 +23665,20 @@
         "vue-confirm-dialog": {
             "version": "1.0.2",
             "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": {
             "version": "3.0.4",
             "resolved": "https://registry.npmjs.org/vue-contenteditable/-/vue-contenteditable-3.0.4.tgz",
-            "integrity": "sha512-CmtqT4zHQwLoJEyNVaXUjjUFPUVYlXXBHfSbRCHCUjODMqrn6L293LM1nc1ELx8epitZZvecTfIqOLlSzB3d+w=="
+            "integrity": "sha512-CmtqT4zHQwLoJEyNVaXUjjUFPUVYlXXBHfSbRCHCUjODMqrn6L293LM1nc1ELx8epitZZvecTfIqOLlSzB3d+w==",
+            "requires": {}
         },
         "vue-demi": {
             "version": "0.10.1",
             "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": {
             "version": "7.11.0",
@@ -23735,7 +23750,8 @@
                 "vue-demi": {
                     "version": "0.11.4",
                     "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": {
             "version": "2.0.0-rc.5",
             "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": {
             "version": "4.1.0",
@@ -23929,7 +23946,8 @@
             "version": "7.5.5",
             "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
             "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
-            "dev": true
+            "dev": true,
+            "requires": {}
         },
         "xml-name-validator": {
             "version": "3.0.0",
diff --git a/server/database.js b/server/database.js
index afcace70..6645e537 100644
--- a/server/database.js
+++ b/server/database.js
@@ -53,6 +53,7 @@ class Database {
         "patch-2fa-invalidate-used-token.sql": true,
         "patch-notification_sent_history.sql": true,
         "patch-monitor-basic-auth.sql": true,
+        "patch-maintenance-table.sql": true,
     }
 
     /**
diff --git a/server/model/heartbeat.js b/server/model/heartbeat.js
index e0a77c06..617ac598 100644
--- a/server/model/heartbeat.js
+++ b/server/model/heartbeat.js
@@ -10,6 +10,7 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
  *      0 = DOWN
  *      1 = UP
  *      2 = PENDING
+ *      3 = MAINTENANCE
  */
 class Heartbeat extends BeanModel {
 
diff --git a/server/model/maintenance.js b/server/model/maintenance.js
new file mode 100644
index 00000000..4958a203
--- /dev/null
+++ b/server/model/maintenance.js
@@ -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;
diff --git a/server/model/monitor.js b/server/model/monitor.js
index c4441d63..cd62ec6b 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -6,7 +6,7 @@ dayjs.extend(utc);
 dayjs.extend(timezone);
 const axios = require("axios");
 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 { R } = require("redbean-node");
 const { BeanModel } = require("redbean-node/dist/bean-model");
@@ -20,6 +20,7 @@ const apicache = require("../modules/apicache");
  *      0 = DOWN
  *      1 = UP
  *      2 = PENDING
+ *      3 = MAINTENANCE
  */
 class Monitor extends BeanModel {
 
@@ -28,9 +29,12 @@ class Monitor extends BeanModel {
      * Only show necessary data to public
      */
     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 {
             id: this.id,
             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 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 {
             id: this.id,
@@ -79,6 +84,7 @@ class Monitor extends BeanModel {
             pushToken: this.pushToken,
             notificationIDList,
             tags: tags,
+            maintenance: (maintenance.length !== 0),
         };
     }
 
@@ -136,6 +142,8 @@ class Monitor extends BeanModel {
             bean.time = R.isoDateTime(dayjs.utc());
             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()) {
                 bean.status = flipStatus(bean.status);
             }
@@ -148,7 +156,11 @@ class Monitor extends BeanModel {
             }
 
             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"
                     let startTime = dayjs().valueOf();
 
@@ -387,8 +399,13 @@ class Monitor extends BeanModel {
             if (isImportant) {
                 bean.important = true;
 
-                debug(`[${this.name}] sendNotification`);
-                await Monitor.sendNotification(isFirstBeat, this, bean);
+                if (Monitor.isImportantForNotification(isFirstBeat, previousBeat?.status, bean.status)) {
+                    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
                 debug(`[${this.name}] apicache clear`);
@@ -405,6 +422,8 @@ class Monitor extends BeanModel {
                     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}`);
+            } else if (bean.status === MAINTENANCE) {
+                console.warn(`Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
             } else {
                 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(
                     CASE
-                        WHEN (status = 1)
+                        WHEN (status = 1 OR status = 3)
                         THEN
                             CASE
                                 WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
@@ -659,11 +678,42 @@ class Monitor extends BeanModel {
         // DOWN -> PENDING = this case not exists
         // DOWN -> DOWN = not 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 === DOWN && currentBeatStatus === UP) ||
             (previousBeatStatus === PENDING && currentBeatStatus === DOWN);
-        return isImportant;
     }
 
     static async sendNotification(isFirstBeat, monitor, bean) {
diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index 1920cef7..19e4fcad 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -5,7 +5,7 @@ const server = require("../server");
 const apicache = require("../modules/apicache");
 const Monitor = require("../model/monitor");
 const dayjs = require("dayjs");
-const { UP, flipStatus, debug } = require("../../src/util");
+const { UP, MAINTENANCE, flipStatus, debug} = require("../../src/util");
 let router = express.Router();
 
 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");
         }
 
+        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("Current Status: " + status);
 
@@ -70,7 +76,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
             ok: true,
         });
 
-        if (bean.important) {
+        if (Monitor.isImportantForNotification(isFirstBeat, previousStatus, status)) {
             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
 // Can fetch only if published
 router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => {
diff --git a/server/server.js b/server/server.js
index 153cac4f..2b6933d7 100644
--- a/server/server.js
+++ b/server/server.js
@@ -132,6 +132,7 @@ const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sen
 const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
 const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
 const TwoFA = require("./2fa");
+const apicache = require("./modules/apicache");
 
 app.use(express.json());
 
@@ -162,6 +163,12 @@ let jwtSecret = null;
  */
 let monitorList = {};
 
+/**
+* Main maintenance list
+* @type {{}}
+*/
+let maintenanceList = {};
+
 /**
  * Show Setup Page
  * @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) => {
             try {
                 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) => {
             try {
                 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) => {
             try {
                 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) => {
             try {
                 checkLogin(socket);
@@ -1394,11 +1590,18 @@ async function sendMonitorList(socket) {
     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) {
     socket.userID = user.id;
     socket.join(user.id);
 
     let monitorList = await sendMonitorList(socket);
+    sendMaintenanceList(socket);
     sendNotificationList(socket);
 
     await sleep(500);
@@ -1430,6 +1633,20 @@ async function getMonitorJSONList(userID) {
     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) {
     if (! fs.existsSync(Database.path)) {
         console.log("Copying Database");
diff --git a/src/assets/app.scss b/src/assets/app.scss
index cec64467..73b9d631 100644
--- a/src/assets/app.scss
+++ b/src/assets/app.scss
@@ -273,6 +273,7 @@ textarea.form-control {
         &.bg-info,
         &.bg-warning,
         &.bg-danger,
+        &.bg-maintenance,
         &.bg-light {
             color: $dark-font-color2;
         }
diff --git a/src/assets/vars.scss b/src/assets/vars.scss
index 91ab917e..e48a6efb 100644
--- a/src/assets/vars.scss
+++ b/src/assets/vars.scss
@@ -1,6 +1,7 @@
 $primary: #5cdd8b;
 $danger: #dc3545;
 $warning: #f8a306;
+$maintenance: #1747f5;
 $link-color: #111;
 $border-radius: 50rem;
 
diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue
index be0b122e..abeed7cb 100644
--- a/src/components/HeartbeatBar.vue
+++ b/src/components/HeartbeatBar.vue
@@ -5,7 +5,7 @@
                 v-for="(beat, index) in shortBeatList"
                 :key="index"
                 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"
                 :title="getBeatTitle(beat)"
             />
@@ -200,6 +200,10 @@ export default {
             background-color: $warning;
         }
 
+        &.maintenance {
+            background-color: $maintenance;
+        }
+
         &:not(.empty):hover {
             transition: all ease-in-out 0.15s;
             opacity: 0.8;
diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue
index ef51e89c..d943efff 100644
--- a/src/components/MonitorList.vue
+++ b/src/components/MonitorList.vue
@@ -1,7 +1,12 @@
 <template>
     <div class="shadow-box mb-3">
         <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">
                 <a v-if="searchText == ''" class="search-icon">
                     <font-awesome-icon icon="search" />
@@ -13,11 +18,25 @@
             </div>
         </div>
         <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>
             </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="col-9 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
                         <div class="info">
@@ -47,7 +66,7 @@
 import HeartbeatBar from "../components/HeartbeatBar.vue";
 import Uptime from "../components/Uptime.vue";
 import Tag from "../components/Tag.vue";
-import { getMonitorRelativeURL } from "../util.ts";
+import {getMaintenanceRelativeURL, getMonitorRelativeURL} from "../util.ts";
 
 export default {
     components: {
@@ -63,9 +82,60 @@ export default {
     data() {
         return {
             searchText: "",
+            selectedList: "monitor"
         };
     },
     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() {
             let result = Object.values(this.$root.monitorList);
 
@@ -96,7 +166,7 @@ export default {
 
             // Simple filter by search text
             // finds monitor name, tag name or tag value
-            if (this.searchText != "") {
+            if (this.searchText !== "") {
                 const loweredSearchText = this.searchText.toLowerCase();
                 result = result.filter(monitor => {
                     return monitor.name.toLowerCase().includes(loweredSearchText)
@@ -112,6 +182,9 @@ export default {
         monitorURL(id) {
             return getMonitorRelativeURL(id);
         },
+        maintenanceURL(id) {
+            return getMaintenanceRelativeURL(id);
+        },
         clearSearchText() {
             this.searchText = "";
         }
@@ -174,4 +247,12 @@ export default {
     flex-wrap: wrap;
     gap: 0;
 }
+
+.bg-maintenance {
+    background-color: $maintenance;
+}
+
+select {
+    text-align: center;
+}
 </style>
diff --git a/src/components/PingChart.vue b/src/components/PingChart.vue
index aa209fab..fb380e04 100644
--- a/src/components/PingChart.vue
+++ b/src/components/PingChart.vue
@@ -24,7 +24,7 @@ import timezone from "dayjs/plugin/timezone";
 import "chartjs-adapter-dayjs";
 import { LineChart } from "vue-chart-3";
 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(timezone);
@@ -162,7 +162,8 @@ export default {
         },
         chartData() {
             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 ||
              (this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
@@ -184,8 +185,9 @@ export default {
                     });
                     downData.push({
                         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 {
@@ -204,7 +206,7 @@ export default {
                         type: "bar",
                         data: downData,
                         borderColor: "#00000000",
-                        backgroundColor: "#DC354568",
+                        backgroundColor: colorData,
                         yAxisID: "y1",
                         barThickness: "flex",
                         barPercentage: 1,
diff --git a/src/components/PublicGroupList.vue b/src/components/PublicGroupList.vue
index f30edcef..a6539b8b 100644
--- a/src/components/PublicGroupList.vue
+++ b/src/components/PublicGroupList.vue
@@ -146,4 +146,8 @@ export default {
     }
 }
 
+.bg-maintenance {
+    background-color: $maintenance;
+}
+
 </style>
diff --git a/src/components/Status.vue b/src/components/Status.vue
index a3916adc..558ec3ee 100644
--- a/src/components/Status.vue
+++ b/src/components/Status.vue
@@ -22,6 +22,10 @@ export default {
                 return "warning";
             }
 
+            if (this.status === 3) {
+                return "maintenance";
+            }
+
             return "secondary";
         },
 
@@ -38,6 +42,10 @@ export default {
                 return this.$t("Pending");
             }
 
+            if (this.status === 3) {
+                return this.$t("Maintenance");
+            }
+
             return this.$t("Unknown");
         },
     },
diff --git a/src/components/Uptime.vue b/src/components/Uptime.vue
index 2717672c..d7aae6c6 100644
--- a/src/components/Uptime.vue
+++ b/src/components/Uptime.vue
@@ -15,6 +15,10 @@ export default {
 
     computed: {
         uptime() {
+            
+            if (this.type === "maintenance") {
+                return this.$t("Maintenance");
+            }
 
             let key = this.monitor.id + "_" + this.type;
 
@@ -26,6 +30,10 @@ export default {
         },
 
         color() {
+            if (this.type === "maintenance" || this.monitor.maintenance) {
+                return "maintenance"
+            }
+            
             if (this.lastHeartBeat.status === 0) {
                 return "danger"
             }
diff --git a/src/icon.js b/src/icon.js
index 88b8a8ec..3027d958 100644
--- a/src/icon.js
+++ b/src/icon.js
@@ -34,6 +34,7 @@ import {
     faAward,
     faLink,
     faChevronDown,
+    faWrench,
 } from "@fortawesome/free-solid-svg-icons";
 
 library.add(
@@ -67,6 +68,7 @@ library.add(
     faAward,
     faLink,
     faChevronDown,
+    faWrench,
 );
 
 export { FontAwesomeIcon };
diff --git a/src/languages/en.js b/src/languages/en.js
index 47513466..3edcc556 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -7,11 +7,13 @@ export default {
     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.",
     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.",
     notificationDescription: "Notifications must be assigned to a monitor to function.",
     keywordDescription: "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
     pauseDashboardHome: "Pause",
     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?",
     resoverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.",
     rrtypeDescription: "Select the RR type you want to monitor",
diff --git a/src/languages/zh-TW.js b/src/languages/zh-TW.js
index 0e905260..ec0f434d 100644
--- a/src/languages/zh-TW.js
+++ b/src/languages/zh-TW.js
@@ -340,7 +340,6 @@ export default {
     "No monitors available.": "沒有可用的監測器。",
     "Add one": "新增一個",
     "No Monitors": "無監測器",
-    "Add one": "新增一個",
     "Untitled Group": "未命名群組",
     Services: "服務",
     Discard: "捨棄",
diff --git a/src/layouts/Layout.vue b/src/layouts/Layout.vue
index 75173e1f..4d6ca27d 100644
--- a/src/layouts/Layout.vue
+++ b/src/layouts/Layout.vue
@@ -51,7 +51,7 @@
 
         <!-- Mobile Only -->
         <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">
                 <div><font-awesome-icon icon="tachometer-alt" /></div>
                 {{ $t("Dashboard") }}
@@ -64,7 +64,12 @@
 
             <router-link to="/add" class="nav-link">
                 <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 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>
diff --git a/src/mixins/datetime.js b/src/mixins/datetime.js
index 7cef22d2..08689520 100644
--- a/src/mixins/datetime.js
+++ b/src/mixins/datetime.js
@@ -22,6 +22,16 @@ export default {
             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) {
             return this.datetimeFormat(value, "YYYY-MM-DD");
         },
@@ -41,6 +51,13 @@ export default {
                 return dayjs.utc(value).tz(this.timezone).format(format);
             }
             return "";
+        },
+
+        datetimeMaintenanceFormat(value, format) {
+            if (value !== undefined && value !== "") {
+                return dayjs(value).format(format);
+            }
+            return "";
         }
     },
 
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
index affac4f8..3a475dc5 100644
--- a/src/mixins/socket.js
+++ b/src/mixins/socket.js
@@ -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.
             loggedIn: false,
             monitorList: { },
+            maintenanceList: { },
             heartbeatList: { },
             importantHeartbeatList: { },
             avgPingList: { },
@@ -99,6 +100,10 @@ export default {
                 this.monitorList = data;
             });
 
+            socket.on("maintenanceList", (data) => {
+                this.maintenanceList = data;
+            });
+
             socket.on("notificationList", (data) => {
                 this.notificationList = data;
             });
@@ -309,14 +314,37 @@ export default {
             socket.emit("getMonitorList", callback);
         },
 
+        getMaintenanceList(callback) {
+            if (! callback) {
+                callback = () => { };
+            }
+            socket.emit("getMaintenanceList", callback);
+        },
+
         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) {
             socket.emit("deleteMonitor", monitorID, callback);
         },
 
+        deleteMaintenance(maintenanceID, callback) {
+            socket.emit("deleteMaintenance", maintenanceID, callback);
+        },
+
         clearData() {
             console.log("reset heartbeat list");
             this.heartbeatList = {};
@@ -368,7 +396,13 @@ export default {
             for (let monitorID in this.lastHeartbeatList) {
                 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;
                 } else if (lastHeartBeat.status === 1) {
                     result[monitorID] = {
diff --git a/src/pages/Dashboard.vue b/src/pages/Dashboard.vue
index 1cf237ce..cad00963 100644
--- a/src/pages/Dashboard.vue
+++ b/src/pages/Dashboard.vue
@@ -4,6 +4,7 @@
             <div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
                 <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="/addMaintenance" class="btn btn-primary mb-3 float-end"><font-awesome-icon icon="wrench" /> {{ $t("Add New Maintenance") }}</router-link>
                 </div>
                 <MonitorList :scrollbar="true" />
             </div>
diff --git a/src/pages/DashboardHome.vue b/src/pages/DashboardHome.vue
index 16d07983..77ef90c7 100644
--- a/src/pages/DashboardHome.vue
+++ b/src/pages/DashboardHome.vue
@@ -15,6 +15,10 @@
                         <h3>{{ $t("Down") }}</h3>
                         <span class="num text-danger">{{ stats.down }}</span>
                     </div>
+                    <div class="col">
+                        <h3>{{ $t("Maintenance") }}</h3>
+                        <span class="num text-maintenance">{{ stats.maintenance }}</span>
+                    </div>
                     <div class="col">
                         <h3>{{ $t("Unknown") }}</h3>
                         <span class="num text-secondary">{{ stats.unknown }}</span>
@@ -38,7 +42,7 @@
                     </thead>
                     <tbody>
                         <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 :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
                             <td class="border-0">{{ beat.msg }}</td>
@@ -93,6 +97,7 @@ export default {
             let result = {
                 up: 0,
                 down: 0,
+                maintenance: 0,
                 unknown: 0,
                 pause: 0,
             };
@@ -100,8 +105,11 @@ export default {
             for (let monitorID in this.$root.monitorList) {
                 let beat = this.$root.lastHeartbeatList[monitorID];
                 let monitor = this.$root.monitorList[monitorID];
-
-                if (monitor && ! monitor.active) {
+                
+                if (monitor && monitor.maintenance) {
+                    result.maintenance++;
+                }
+                else if (monitor && !monitor.active) {
                     result.pause++;
                 } else if (beat) {
                     if (beat.status === 1) {
@@ -173,6 +181,14 @@ export default {
     display: block;
 }
 
+.text-maintenance {
+    color: $maintenance;
+}
+
+.bg-maintenance {
+    background-color: $maintenance;
+}
+
 .shadow-box {
     padding: 20px;
 }
diff --git a/src/pages/Details.vue b/src/pages/Details.vue
index d40561fe..096f928b 100644
--- a/src/pages/Details.vue
+++ b/src/pages/Details.vue
@@ -499,4 +499,8 @@ table {
     margin-left: 0 !important;
 }
 
+.bg-maintenance {
+    background-color: $maintenance;
+}
+
 </style>
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
new file mode 100644
index 00000000..144b398a
--- /dev/null
+++ b/src/pages/EditMaintenance.vue
@@ -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>
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index 4b6a920c..3ac05af3 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -509,7 +509,7 @@ export default {
                         toast.success(res.msg);
                         this.processing = false;
                         this.$root.getMonitorList();
-                        this.$router.push("/dashboard/" + res.monitorID);
+                        this.$router.push("/dashboard/monitor/" + res.monitorID);
                     } else {
                         toast.error(res.msg);
                         this.processing = false;
diff --git a/src/pages/MaintenanceDetails.vue b/src/pages/MaintenanceDetails.vue
new file mode 100644
index 00000000..e3e4b59b
--- /dev/null
+++ b/src/pages/MaintenanceDetails.vue
@@ -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>
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index 0dc49518..11716b45 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -144,6 +144,18 @@
             </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 -->
         <div class="shadow-box list  p-4 overall-status mb-4">
             <div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData">
@@ -167,6 +179,11 @@
                     {{ $t("Degraded Service") }}
                 </div>
 
+                <div v-else-if="isMaintenance">
+                    <font-awesome-icon icon="wrench" class="statusMaintenance" />
+                    {{ $t("Maintenance") }}
+                </div>
+
                 <div v-else>
                     <font-awesome-icon icon="question-circle" style="color: #efefef;" />
                 </div>
@@ -217,7 +234,14 @@
 import axios from "axios";
 import PublicGroupList from "../components/PublicGroupList.vue";
 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 dayjs from "dayjs";
 const toast = useToast();
@@ -259,6 +283,7 @@ export default {
             loadedTheme: false,
             loadedData: false,
             baseURL: "",
+            maintenance: [],
         };
     },
     computed: {
@@ -320,6 +345,10 @@ export default {
             return "bg-" + this.incident.style;
         },
 
+        maintenanceClass() {
+            return "bg-maintenance";
+        },
+
         overallStatus() {
 
             if (Object.keys(this.$root.publicLastHeartbeatList).length === 0) {
@@ -332,7 +361,10 @@ export default {
             for (let id in this.$root.publicLastHeartbeatList) {
                 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;
                 } else {
                     status = STATUS_PAGE_PARTIAL_DOWN;
@@ -358,6 +390,10 @@ export default {
             return this.overallStatus === STATUS_PAGE_ALL_DOWN;
         },
 
+        isMaintenance() {
+            return this.overallStatus === STATUS_PAGE_MAINTENANCE;
+        },
+
     },
     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) => {
             this.$root.publicGroupList = res.data;
         });
@@ -580,6 +620,10 @@ export default {
             return dayjs.utc(date).fromNow();
         },
 
+        dateFromNowMaintenance(date) {
+            return dayjs(date).fromNow();
+        },
+
     }
 };
 </script>
@@ -671,6 +715,22 @@ footer {
     }
 }
 
+.maintenance {
+    color: white;
+
+    .date {
+        font-size: 12px;
+    }
+}
+
+.bg-maintenance {
+    background-color: $maintenance;
+}
+
+.statusMaintenance {
+    color: $maintenance;
+}
+
 .mobile {
     h1 {
         font-size: 22px;
diff --git a/src/router.js b/src/router.js
index a2414eb6..a78007ef 100644
--- a/src/router.js
+++ b/src/router.js
@@ -5,6 +5,7 @@ import Dashboard from "./pages/Dashboard.vue";
 import DashboardHome from "./pages/DashboardHome.vue";
 import Details from "./pages/Details.vue";
 import EditMonitor from "./pages/EditMonitor.vue";
+import EditMaintenance from "./pages/EditMaintenance.vue";
 import List from "./pages/List.vue";
 const Settings = () => import("./pages/Settings.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 Backup from "./components/settings/Backup.vue";
 import About from "./components/settings/About.vue";
+import MaintenanceDetails from "./pages/MaintenanceDetails.vue";
 
 const routes = [
     {
@@ -41,7 +43,7 @@ const routes = [
                         component: DashboardHome,
                         children: [
                             {
-                                path: "/dashboard/:id",
+                                path: "/dashboard/monitor/:id",
                                 component: EmptyLayout,
                                 children: [
                                     {
@@ -54,10 +56,28 @@ const routes = [
                                     },
                                 ],
                             },
+                            {
+                                path: "/dashboard/maintenance/:id",
+                                component: EmptyLayout,
+                                children: [
+                                    {
+                                        path: "",
+                                        component: MaintenanceDetails,
+                                    },
+                                    {
+                                        path: "/editMaintenance/:id",
+                                        component: EditMaintenance,
+                                    },
+                                ],
+                            },
                             {
                                 path: "/add",
                                 component: EditMonitor,
                             },
+                            {
+                                path: "/addMaintenance",
+                                component: EditMaintenance,
+                            },
                             {
                                 path: "/list",
                                 component: List,
diff --git a/src/util.js b/src/util.js
index b2df7ac7..dc5dea58 100644
--- a/src/util.js
+++ b/src/util.js
@@ -7,7 +7,7 @@
 // Backend uses the compiled file util.js
 // Frontend uses util.ts
 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 = _dayjs;
 exports.isDev = process.env.NODE_ENV === "development";
@@ -15,9 +15,11 @@ exports.appName = "Uptime Kuma";
 exports.DOWN = 0;
 exports.UP = 1;
 exports.PENDING = 2;
+exports.MAINTENANCE = 3;
 exports.STATUS_PAGE_ALL_DOWN = 0;
 exports.STATUS_PAGE_ALL_UP = 1;
 exports.STATUS_PAGE_PARTIAL_DOWN = 2;
+exports.STATUS_PAGE_MAINTENANCE = 3;
 function flipStatus(s) {
     if (s === exports.UP) {
         return exports.DOWN;
@@ -162,6 +164,10 @@ function genSecret(length = 64) {
 }
 exports.genSecret = genSecret;
 function getMonitorRelativeURL(id) {
-    return "/dashboard/" + id;
+    return "/dashboard/monitor/" + id;
 }
 exports.getMonitorRelativeURL = getMonitorRelativeURL;
+function getMaintenanceRelativeURL(id) {
+    return "/dashboard/maintenance/" + id;
+}
+exports.getMaintenanceRelativeURL = getMaintenanceRelativeURL;
diff --git a/src/util.ts b/src/util.ts
index 633d933e..b9ce9e23 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -14,10 +14,12 @@ export const appName = "Uptime Kuma";
 export const DOWN = 0;
 export const UP = 1;
 export const PENDING = 2;
+export const MAINTENANCE = 3;
 
 export const STATUS_PAGE_ALL_DOWN = 0;
 export const STATUS_PAGE_ALL_UP = 1;
 export const STATUS_PAGE_PARTIAL_DOWN = 2;
+export const STATUS_PAGE_MAINTENANCE = 3;
 
 
 export function flipStatus(s: number) {
@@ -185,5 +187,9 @@ export function genSecret(length = 64) {
 }
 
 export function getMonitorRelativeURL(id: string) {
-    return "/dashboard/" + id;
+    return "/dashboard/monitor/" + id;
+}
+
+export function getMaintenanceRelativeURL(id: string) {
+    return "/dashboard/maintenance/" + id;
 }

From 5fda1f0f596483fe1a98d6e1731881f063e32911 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?= <karel.kryda@gmail.com>
Date: Sun, 23 Jan 2022 20:33:39 +0100
Subject: [PATCH 002/134] minor fixes (missing commas, spaces, translations)

---
 server/model/maintenance.js      |  4 ++--
 server/model/monitor.js          |  2 +-
 server/routers/api-router.js     |  2 +-
 src/components/MonitorList.vue   |  4 ++--
 src/languages/en.js              | 17 +++++++++++++++++
 src/pages/EditMaintenance.vue    | 10 ++++------
 src/pages/MaintenanceDetails.vue |  4 ++--
 7 files changed, 29 insertions(+), 14 deletions(-)

diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index 4958a203..55308895 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -17,7 +17,7 @@ class Maintenance extends BeanModel {
             title: this.title,
             description: this.description,
             start_date: this.start_date,
-            end_date: this.end_date
+            end_date: this.end_date,
         };
     }
 
@@ -30,7 +30,7 @@ class Maintenance extends BeanModel {
             title: this.title,
             description: this.description,
             start_date: this.start_date,
-            end_date: this.end_date
+            end_date: this.end_date,
         };
     }
 }
diff --git a/server/model/monitor.js b/server/model/monitor.js
index cd62ec6b..b6140c13 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -6,7 +6,7 @@ dayjs.extend(utc);
 dayjs.extend(timezone);
 const axios = require("axios");
 const { Prometheus } = require("../prometheus");
-const { debug, UP, DOWN, PENDING, MAINTENANCE, 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 { R } = require("redbean-node");
 const { BeanModel } = require("redbean-node/dist/bean-model");
diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index 19e4fcad..472a837d 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -5,7 +5,7 @@ const server = require("../server");
 const apicache = require("../modules/apicache");
 const Monitor = require("../model/monitor");
 const dayjs = require("dayjs");
-const { UP, MAINTENANCE, flipStatus, debug} = require("../../src/util");
+const { UP, MAINTENANCE, flipStatus, debug } = require("../../src/util");
 let router = express.Router();
 
 let cache = apicache.middleware;
diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue
index d943efff..4e6610ad 100644
--- a/src/components/MonitorList.vue
+++ b/src/components/MonitorList.vue
@@ -66,7 +66,7 @@
 import HeartbeatBar from "../components/HeartbeatBar.vue";
 import Uptime from "../components/Uptime.vue";
 import Tag from "../components/Tag.vue";
-import {getMaintenanceRelativeURL, getMonitorRelativeURL} from "../util.ts";
+import {getMaintenanceRelativeURL, getMonitorRelativeURL } from "../util.ts";
 
 export default {
     components: {
@@ -82,7 +82,7 @@ export default {
     data() {
         return {
             searchText: "",
-            selectedList: "monitor"
+            selectedList: "monitor",
         };
     },
     computed: {
diff --git a/src/languages/en.js b/src/languages/en.js
index 3edcc556..ce659466 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -7,7 +7,20 @@ export default {
     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.",
     acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
+    Maintenance: "Maintenance",
+    "Monitor List": "Monitor List",
+    "Maintenance List": "Maintenance List",
+    "Schedule maintenance": "Schedule maintenance",
+    "Affected Monitors": "Affected Monitors",
+    "Pick Affected Monitors...": "Pick Affected Monitors...",
+    "Start of maintenance": "Start of maintenance",
+    "Expected end of maintenance": "Expected end of maintenance",
+    Start: "Start",
+    End: "End",
     affectedMonitorsDescription: "Select monitors that are affected by current maintenance",
+    atLeastOneMonitor: "Select at least one affected monitor",
+    maintenanceTitleExample: "Network infrastructure maintenance",
+    maintenanceDescriptionExample: "Example: Network infrastructure maintenance is underway which will affect some of our services.",
     passwordNotMatchMsg: "The repeat password does not match.",
     notificationDescription: "Notifications must be assigned to a monitor to function.",
     keywordDescription: "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
@@ -40,7 +53,10 @@ export default {
     "Check Update On GitHub": "Check Update On GitHub",
     List: "List",
     Add: "Add",
+    "Add Monitor": "Add Monitor",
+    "Add Maintenance": "Add Maintenance",
     "Add New Monitor": "Add New Monitor",
+    "Add New Maintenance": "Add New Maintenance",
     "Quick Stats": "Quick Stats",
     Up: "Up",
     Down: "Down",
@@ -114,6 +130,7 @@ export default {
     "Remember me": "Remember me",
     Login: "Login",
     "No Monitors, please": "No Monitors, please",
+    "No Maintenance, please": "No Maintenance, please",
     "add one": "add one",
     "Notification Type": "Notification Type",
     Email: "Email",
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 144b398a..6b6f8057 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -114,16 +114,15 @@ export default {
         },
 
         titlePlaceholder() {
-            return this.$t("Network infrastructure maintenance");
+            return this.$t("maintenanceTitleExample");
         },
 
         descriptionPlaceholder() {
-            return this.$t("Example: Network infrastructure maintenance is underway which will affect some of our services.");
+            return this.$t("maintenanceDescriptionExample");
         }
 
     },
     watch: {
-
         "$route.fullPath"() {
             this.init();
         }
@@ -137,7 +136,7 @@ export default {
                 Object.values(this.$root.monitorList).map(monitor => {
                     this.affectedMonitorsOptions.push({
                         id: monitor.id,
-                        name: monitor.name
+                        name: monitor.name,
                     });
                 });
             }
@@ -173,14 +172,13 @@ export default {
                     }
                 });
             }
-
         },
 
         async submit() {
             this.processing = true;
 
             if (this.affectedMonitors.length === 0) {
-                toast.error(this.$t("Select at least one affected monitor"));
+                toast.error(this.$t("atLeastOneMonitor"));
                 return this.processing = false;
             }
 
diff --git a/src/pages/MaintenanceDetails.vue b/src/pages/MaintenanceDetails.vue
index e3e4b59b..77d70078 100644
--- a/src/pages/MaintenanceDetails.vue
+++ b/src/pages/MaintenanceDetails.vue
@@ -3,9 +3,9 @@
         <div v-if="maintenance">
             <h1> {{ maintenance.title }}</h1>
             <p class="url">
-                <span>Start: {{ $root.datetimeMaintenance(maintenance.start_date) }}</span>
+                <span>{{$t("Start")}}: {{ $root.datetimeMaintenance(maintenance.start_date) }}</span>
                 <br>
-                <span>End: {{ $root.datetimeMaintenance(maintenance.end_date) }}</span>
+                <span>{{$t("End")}}: {{ $root.datetimeMaintenance(maintenance.end_date) }}</span>
             </p>
 
             <div class="functions" style="margin-top: 10px">

From e7b2832967f6a40d85a78b482b6d46a8676e6303 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?= <karel.kryda@gmail.com>
Date: Mon, 24 Jan 2022 22:33:15 +0100
Subject: [PATCH 003/134] The start and end dates of the maintenance are now
 stored in UTC, which allows it to be converted between time zones

---
 server/model/monitor.js        |  6 +++---
 server/routers/api-router.js   |  6 +++---
 src/components/MonitorList.vue | 15 +++++++--------
 src/mixins/datetime.js         | 21 +++++++++++----------
 src/pages/EditMaintenance.vue  | 13 ++++++++-----
 src/pages/StatusPage.vue       |  6 +-----
 6 files changed, 33 insertions(+), 34 deletions(-)

diff --git a/server/model/monitor.js b/server/model/monitor.js
index b6140c13..4779dd0e 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -29,7 +29,7 @@ class Monitor extends BeanModel {
      * Only show necessary data to public
      */
     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]);
+        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') AND datetime(maintenance.end_date) >= datetime('now')", [this.id]);
 
         return {
             id: this.id,
@@ -54,7 +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 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]);
+        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') AND datetime(maintenance.end_date) >= datetime('now')", [this.id]);
 
         return {
             id: this.id,
@@ -142,7 +142,7 @@ class Monitor extends BeanModel {
             bean.time = R.isoDateTime(dayjs.utc());
             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]);
+            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') AND datetime(maintenance.end_date) >= datetime('now')", [this.id]);
 
             if (this.isUpsideDown()) {
                 bean.status = flipStatus(bean.status);
diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index 472a837d..408b9450 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -51,7 +51,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
             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]);
+        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') AND datetime(maintenance.end_date) >= datetime('now')", [monitor.id]);
         if (maintenance.length !== 0) {
             msg = "Monitor under maintenance";
             status = MAINTENANCE;
@@ -149,8 +149,8 @@ router.get("/api/status-page/maintenance-list", async (_request, response) => {
         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')
+            WHERE datetime(maintenance.start_date) <= datetime('now')
+              AND datetime(maintenance.end_date) >= datetime('now')
             ORDER BY maintenance.end_date
         `));
 
diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue
index 4e6610ad..696a02e7 100644
--- a/src/components/MonitorList.vue
+++ b/src/components/MonitorList.vue
@@ -25,7 +25,7 @@
                 {{ $t("No Maintenance, please") }} <router-link to="/addMaintenance">{{ $t("add one") }}</router-link>
             </div>
 
-            <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()) }">
+            <router-link v-if="selectedList === 'maintenance'" v-for="(item, index) in sortedMaintenanceList" :key="index" :to="maintenanceURL(item.id)" class="item" :class="{ 'disabled': !this.$root.isActiveMaintenance(item.end_date) }">
                 <div class="row">
                     <div class="col-9 col-md-8 small-padding">
                         <div class="info">
@@ -66,7 +66,7 @@
 import HeartbeatBar from "../components/HeartbeatBar.vue";
 import Uptime from "../components/Uptime.vue";
 import Tag from "../components/Tag.vue";
-import {getMaintenanceRelativeURL, getMonitorRelativeURL } from "../util.ts";
+import { getMaintenanceRelativeURL, getMonitorRelativeURL } from "../util.ts";
 
 export default {
     components: {
@@ -90,18 +90,17 @@ export default {
             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) {
+                if (this.$root.isActiveMaintenance(m1.end_date) !== this.$root.isActiveMaintenance(m2.end_date)) {
+                    if (!this.$root.isActiveMaintenance(m2.end_date)) {
                         return -1;
                     }
-                    if (Date.parse(m1.end_date) < now) {
+                    if (!this.$root.isActiveMaintenance(m1.end_date)) {
                         return 1;
                     }
                 }
 
-                if (Date.parse(m1.end_date) >= now && Date.parse(m2.end_date) >= now) {
+                if (this.$root.isActiveMaintenance(m1.end_date) && this.$root.isActiveMaintenance(m2.end_date)) {
                     if (Date.parse(m1.end_date) < Date.parse(m2.end_date)) {
                         return -1;
                     }
@@ -111,7 +110,7 @@ export default {
                     }
                 }
 
-                if (Date.parse(m1.end_date) < now && Date.parse(m2.end_date) < now) {
+                if (!this.$root.isActiveMaintenance(m1.end_date) && !this.$root.isActiveMaintenance(m2.end_date)) {
                     if (Date.parse(m1.end_date) < Date.parse(m2.end_date)) {
                         return 1;
                     }
diff --git a/src/mixins/datetime.js b/src/mixins/datetime.js
index 08689520..3f4749af 100644
--- a/src/mixins/datetime.js
+++ b/src/mixins/datetime.js
@@ -18,6 +18,14 @@ export default {
     },
 
     methods: {
+        isActiveMaintenance(endDate) {
+            return (dayjs.utc(endDate).unix() >= dayjs.utc().unix());
+        },
+
+        toUTC(value) {
+            return dayjs.tz(value, this.timezone).utc().format();
+        },
+
         datetime(value) {
             return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
         },
@@ -26,10 +34,10 @@ export default {
             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");
+            if (inputDate.getFullYear() === now.getUTCFullYear() && inputDate.getMonth() === now.getUTCMonth() && inputDate.getDay() === now.getUTCDay())
+                return this.datetimeFormat(value, "HH:mm");
             else
-                return this.datetimeMaintenanceFormat(value, "YYYY-MM-DD HH:mm");
+                return this.datetimeFormat(value, "YYYY-MM-DD HH:mm");
         },
 
         date(value) {
@@ -52,13 +60,6 @@ export default {
             }
             return "";
         },
-
-        datetimeMaintenanceFormat(value, format) {
-            if (value !== undefined && value !== "") {
-                return dayjs(value).format(format);
-            }
-            return "";
-        }
     },
 
     computed: {
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 6b6f8057..e50affaa 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -50,14 +50,14 @@
 
                             <!-- Start Date Time -->
                             <div class="my-3">
-                                <label for="start_date" class="form-label">{{ $t("Start of maintenance") }}</label>
+                                <label for="start_date" class="form-label">{{ $t("Start of maintenance") }} ({{this.$root.timezone}})</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>
+                                <label for="end_date" class="form-label">{{ $t("Expected end of maintenance") }} ({{this.$root.timezone}})</label>
                                 <input :type="'datetime-local'" id="end_date" v-model="maintenance.end_date"
                                        class="form-control" :class="{'darkCalendar': dark }" required>
                             </div>
@@ -156,6 +156,8 @@ export default {
             } else if (this.isEdit) {
                 this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
                     if (res.ok) {
+                        res.maintenance.start_date = this.$root.datetimeFormat(res.maintenance.start_date, "YYYY-MM-DDTHH:mm");
+                        res.maintenance.end_date = this.$root.datetimeFormat(res.maintenance.end_date, "YYYY-MM-DDTHH:mm");
                         this.maintenance = res.maintenance;
 
                         this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
@@ -182,9 +184,11 @@ export default {
                 return this.processing = false;
             }
 
+            this.maintenance.start_date = this.$root.toUTC(this.maintenance.start_date);
+            this.maintenance.end_date = this.$root.toUTC(this.maintenance.end_date);
+
             if (this.isAdd) {
                 this.$root.addMaintenance(this.maintenance, async (res) => {
-
                     if (res.ok) {
                         await this.addMonitorMaintenance(res.maintenanceID, () => {
                             toast.success(res.msg);
@@ -206,8 +210,7 @@ export default {
                             this.$root.toastRes(res);
                             this.init();
                         });
-                    }
-                    else {
+                    } else {
                         this.processing = false;
                         toast.error(res.msg);
                     }
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index 11716b45..fdddba74 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -152,7 +152,7 @@
 
             <!-- Incident Date -->
             <div class="date mt-3">
-                {{ $t("End") }}: {{ $root.datetimeMaintenance(maintenanceItem.end_date) }} ({{ dateFromNowMaintenance(maintenanceItem.start_date) }})<br />
+                {{ $t("End") }}: {{ $root.datetimeMaintenance(maintenanceItem.end_date) }} ({{ dateFromNow(maintenanceItem.start_date) }})<br />
             </div>
         </div>
 
@@ -620,10 +620,6 @@ export default {
             return dayjs.utc(date).fromNow();
         },
 
-        dateFromNowMaintenance(date) {
-            return dayjs(date).fromNow();
-        },
-
     }
 };
 </script>

From b49e5d5c39c7b60e663237fa02bbda31a47c184c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?= <karel.kryda@gmail.com>
Date: Tue, 25 Jan 2022 19:07:27 +0100
Subject: [PATCH 004/134] The SQL query to determine if the monitor is under
 maintenance is now in its own method.

---
 server/model/monitor.js      | 16 ++++++++--------
 server/routers/api-router.js |  3 +--
 2 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/server/model/monitor.js b/server/model/monitor.js
index 4779dd0e..61291d63 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -29,12 +29,10 @@ class Monitor extends BeanModel {
      * Only show necessary data to public
      */
     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') AND datetime(maintenance.end_date) >= datetime('now')", [this.id]);
-
         return {
             id: this.id,
             name: this.name,
-            maintenance: (maintenance.length !== 0),
+            maintenance: await Monitor.isUnderMaintenance(this.id),
         };
     }
 
@@ -54,7 +52,6 @@ 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 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') AND datetime(maintenance.end_date) >= datetime('now')", [this.id]);
 
         return {
             id: this.id,
@@ -84,7 +81,7 @@ class Monitor extends BeanModel {
             pushToken: this.pushToken,
             notificationIDList,
             tags: tags,
-            maintenance: (maintenance.length !== 0),
+            maintenance: await Monitor.isUnderMaintenance(this.id),
         };
     }
 
@@ -142,8 +139,6 @@ class Monitor extends BeanModel {
             bean.time = R.isoDateTime(dayjs.utc());
             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') AND datetime(maintenance.end_date) >= datetime('now')", [this.id]);
-
             if (this.isUpsideDown()) {
                 bean.status = flipStatus(bean.status);
             }
@@ -156,7 +151,7 @@ class Monitor extends BeanModel {
             }
 
             try {
-                if (maintenance.length !== 0) {
+                if (await Monitor.isUnderMaintenance(this.id)) {
                     bean.msg = "Monitor under maintenance";
                     bean.status = MAINTENANCE;
                 }
@@ -813,6 +808,11 @@ class Monitor extends BeanModel {
             monitorID
         ]);
     }
+
+    static async isUnderMaintenance(monitorID) {
+        const maintenance = await R.getRow("SELECT COUNT(*) AS count FROM monitor_maintenance mm JOIN maintenance ON mm.maintenance_id = maintenance.id WHERE mm.monitor_id = ? AND datetime(maintenance.start_date) <= datetime('now') AND datetime(maintenance.end_date) >= datetime('now') LIMIT 1", [monitorID]);
+        return maintenance.count !== 0;
+    }
 }
 
 module.exports = Monitor;
diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index 408b9450..d6ad35ce 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -51,8 +51,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
             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') AND datetime(maintenance.end_date) >= datetime('now')", [monitor.id]);
-        if (maintenance.length !== 0) {
+        if (await Monitor.isUnderMaintenance(monitor.id)) {
             msg = "Monitor under maintenance";
             status = MAINTENANCE;
         }

From 7532acc95d2347e2cc2983ca994de46fb52c9190 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?= <karel.kryda@gmail.com>
Date: Sat, 30 Apr 2022 14:33:54 +0200
Subject: [PATCH 005/134] Resolve conflicts

---
 package-lock.json            | 58 ++++++++++++++++--------------------
 server/routers/api-router.js | 18 +++++------
 src/components/PingChart.vue |  4 +--
 src/icon.js                  |  2 ++
 src/mixins/socket.js         |  6 +++-
 src/pages/Dashboard.vue      | 19 ++++++++++--
 src/pages/DashboardHome.vue  |  2 +-
 src/pages/EditMonitor.vue    |  2 +-
 src/pages/StatusPage.vue     | 26 ++++++++--------
 src/util.ts                  |  2 +-
 10 files changed, 74 insertions(+), 65 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 295cc9e6..699969c1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "uptime-kuma",
-    "version": "1.15.0",
+    "version": "1.15.1",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "uptime-kuma",
-            "version": "1.15.0",
+            "version": "1.15.1",
             "license": "MIT",
             "dependencies": {
                 "@fortawesome/fontawesome-svg-core": "~1.2.36",
@@ -26852,9 +26852,9 @@
             "dev": true
         },
         "postcss-scss": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.2.tgz",
-            "integrity": "sha512-xfdkU128CkKKKVAwkyt0M8OdnelJ3MRcIRAPPQkRpoPeuzWY3RIeg7piRCpZ79MK7Q16diLXMMAD9dN5mauPlQ=="
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.3.tgz",
+            "integrity": "sha512-j4KxzWovfdHsyxwl1BxkUal/O4uirvHgdzMKS1aWJBAV0qh2qj5qAZqpeBfVUYGWv+4iK9Az7SPyZ4fyNju1uA=="
         },
         "postcss-selector-parser": {
             "version": "6.0.10",
@@ -27072,21 +27072,15 @@
                     "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
                     "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
                     "dev": true
-                },
-                "progress": {
-                    "version": "2.0.1",
-                    "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz",
-                    "integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==",
-                    "dev": true
-                },
-                "ws": {
-                    "version": "7.4.6",
-                    "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
-                    "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
-                    "dev": true
                 }
             }
         },
+        "qlobber": {
+            "version": "5.0.3",
+            "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-5.0.3.tgz",
+            "integrity": "sha512-wW4GTZPePyh0RgOsM18oDyOUlXfurVRgoNyJfS+y7VWPyd0GYhQp5T2tycZFZjonH+hngxIfklGJhTP/ghidgQ==",
+            "dev": true
+        },
         "qrcode": {
             "version": "1.5.0",
             "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.0.tgz",
@@ -28384,9 +28378,9 @@
             }
         },
         "stylelint-config-recommended": {
-            "version": "6.0.0",
-            "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-6.0.0.tgz",
-            "integrity": "sha512-ZorSSdyMcxWpROYUvLEMm0vSZud2uB7tX1hzBZwvVY9SV/uly4AvvJPPhCcymZL3fcQhEQG5AELmrxWqtmzacw==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz",
+            "integrity": "sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==",
             "dev": true
         },
         "stylelint-config-standard": {
@@ -29101,9 +29095,9 @@
             "integrity": "sha512-CmtqT4zHQwLoJEyNVaXUjjUFPUVYlXXBHfSbRCHCUjODMqrn6L293LM1nc1ELx8epitZZvecTfIqOLlSzB3d+w=="
         },
         "vue-demi": {
-            "version": "0.10.1",
-            "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.10.1.tgz",
-            "integrity": "sha512-L6Oi+BvmMv6YXvqv5rJNCFHEKSVu7llpWWJczqmAQYOdmPPw5PNYoz1KKS//Fxhi+4QP64dsPjtmvnYGo1jemA=="
+            "version": "0.11.4",
+            "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.11.4.tgz",
+            "integrity": "sha512-/3xFwzSykLW2HiiLie43a+FFgNOcokbBJ+fzvFXd0r2T8MYohqvphUyDQ8lbAwzQ3Dlcrb1c9ykifGkhSIAk6A=="
         },
         "vue-eslint-parser": {
             "version": "8.3.0",
@@ -29189,13 +29183,6 @@
             "requires": {
                 "tslib": "^2.2.0",
                 "vue-demi": "^0.11.3"
-            },
-            "dependencies": {
-                "vue-demi": {
-                    "version": "0.11.4",
-                    "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.11.4.tgz",
-                    "integrity": "sha512-/3xFwzSykLW2HiiLie43a+FFgNOcokbBJ+fzvFXd0r2T8MYohqvphUyDQ8lbAwzQ3Dlcrb1c9ykifGkhSIAk6A=="
-                }
             }
         },
         "vue-router": {
@@ -29405,9 +29392,14 @@
             }
         },
         "ws": {
-            "version": "7.5.5",
-            "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
-            "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
+            "version": "7.5.7",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
+            "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A=="
+        },
+        "xdg-basedir": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
+            "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
             "dev": true
         },
         "xml-name-validator": {
diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index 99d9f85c..af305809 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -128,6 +128,8 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons
             incident = incident.toPublicJSON();
         }
 
+        let maintenance = await getMaintenanceList();
+
         // Public Group List
         const publicGroupList = [];
         const showTags = !!statusPage.show_tags;
@@ -145,6 +147,7 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons
         response.json({
             config: await statusPage.toPublicJSON(),
             incident,
+            maintenance,
             publicGroupList
         });
 
@@ -153,15 +156,10 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons
     }
 
 });
-
-//TODO: make OK with new multi status pages
+// TODO: make slug aware
 // Status Page - Maintenance List
-// Can fetch only if published
-router.get("/api/status-page/maintenance-list", async (_request, response) => {
-    allowDevAllOrigin(response);
-
+async function getMaintenanceList() {
     try {
-        await checkPublished();
         const publicMaintenanceList = [];
 
         let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
@@ -176,12 +174,12 @@ router.get("/api/status-page/maintenance-list", async (_request, response) => {
             publicMaintenanceList.push(await bean.toPublicJSON());
         }
 
-        response.json(publicMaintenanceList);
+        return publicMaintenanceList;
 
     } catch (error) {
-        send403(response, error.message);
+        return null;
     }
-});
+}
 
 // Status Page Polling Data
 // Can fetch only if published
diff --git a/src/components/PingChart.vue b/src/components/PingChart.vue
index 90dc838a..ede31955 100644
--- a/src/components/PingChart.vue
+++ b/src/components/PingChart.vue
@@ -24,7 +24,7 @@ import timezone from "dayjs/plugin/timezone";
 import utc from "dayjs/plugin/utc";
 import { LineChart } from "vue-chart-3";
 import { useToast } from "vue-toastification";
-import { DOWN, MAINTENANCE } from "../util.ts";
+import { DOWN, PENDING, MAINTENANCE } from "../util.ts";
 
 dayjs.extend(utc);
 dayjs.extend(timezone);
@@ -187,7 +187,7 @@ export default {
                         x,
                         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"))
+                    colorData.push((beat.status === MAINTENANCE) ? "rgba(23,71,245,0.41)" : ((beat.status === PENDING) ? "rgba(245,182,23,0.41)" : "#DC354568"));
                 });
 
             return {
diff --git a/src/icon.js b/src/icon.js
index 359fc262..c59b2a1c 100644
--- a/src/icon.js
+++ b/src/icon.js
@@ -42,6 +42,7 @@ import {
     faPlusCircle,
     faAngleDown,
     faWrench,
+    faHeartbeat,
 } from "@fortawesome/free-solid-svg-icons";
 
 library.add(
@@ -83,6 +84,7 @@ library.add(
     faPlusCircle,
     faAngleDown,
     faWrench,
+    faHeartbeat,
 );
 
 export { FontAwesomeIcon };
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
index bf4b817c..8d419706 100644
--- a/src/mixins/socket.js
+++ b/src/mixins/socket.js
@@ -494,6 +494,7 @@ export default {
             let result = {
                 up: 0,
                 down: 0,
+                maintenance: 0,
                 unknown: 0,
                 pause: 0,
             };
@@ -502,7 +503,10 @@ export default {
                 let beat = this.$root.lastHeartbeatList[monitorID];
                 let monitor = this.$root.monitorList[monitorID];
 
-                if (monitor && ! monitor.active) {
+                if (monitor && monitor.maintenance) {
+                    result.maintenance++;
+                }
+                else if (monitor && ! monitor.active) {
                     result.pause++;
                 } else if (beat) {
                     if (beat.status === 1) {
diff --git a/src/pages/Dashboard.vue b/src/pages/Dashboard.vue
index a2bd4dd2..e99422a4 100644
--- a/src/pages/Dashboard.vue
+++ b/src/pages/Dashboard.vue
@@ -2,9 +2,22 @@
     <div class="container-fluid">
         <div class="row">
             <div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
-                <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="/addMaintenance" class="btn btn-primary mb-3 float-end"><font-awesome-icon icon="wrench" /> {{ $t("Add New Maintenance") }}</router-link>
+                <div class="dropdown dropdown-create">
+                    <button class="btn btn-primary mb-3 dropdown-toggle" type="button" data-bs-toggle="dropdown">
+                        <font-awesome-icon icon="plus" /> {{ $t("Create") }}
+                    </button>
+                    <ul class="dropdown-menu dropdown-menu-end">
+                        <li>
+                            <button type="button" class="dropdown-item" @click="this.$router.push('/add')">
+                                <font-awesome-icon icon="heartbeat" /> {{ $t("Monitor") }}
+                            </button>
+                        </li>
+                        <li>
+                            <button type="button" class="dropdown-item" @click="this.$router.push('/addMaintenance')">
+                                <font-awesome-icon icon="exclamation-circle" /> {{ $t("Maintenance") }}
+                            </button>
+                        </li>
+                    </ul>
                 </div>
                 <MonitorList :scrollbar="true" />
             </div>
diff --git a/src/pages/DashboardHome.vue b/src/pages/DashboardHome.vue
index 7dc698f4..0f706912 100644
--- a/src/pages/DashboardHome.vue
+++ b/src/pages/DashboardHome.vue
@@ -17,7 +17,7 @@
                     </div>
                     <div class="col">
                         <h3>{{ $t("Maintenance") }}</h3>
-                        <span class="num text-maintenance">{{ stats.maintenance }}</span>
+                        <span class="num text-maintenance">{{ $root.stats.maintenance }}</span>
                     </div>
                     <div class="col">
                         <h3>{{ $t("Unknown") }}</h3>
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index 899a3c52..39c114ad 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -606,7 +606,7 @@ export default {
                         toast.success(res.msg);
                         this.processing = false;
                         this.$root.getMonitorList();
-                        this.$router.push("/dashboard/monitor/" + res.monitorID);
+                        this.$router.push("/dashboard/" + res.monitorID);
                     } else {
                         toast.error(res.msg);
                         this.processing = false;
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index 3cbc720b..7ae7c636 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -195,16 +195,20 @@
             </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" />
+            <template v-if="maintenance.length !== 0">
+                <div 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" />
+                    <div v-text="maintenanceItem.description" class="content"/>
 
-                <!-- Incident Date -->
-                <div class="date mt-3">
-                    {{ $t("End") }}: {{ $root.datetimeMaintenance(maintenanceItem.end_date) }} ({{ dateFromNow(maintenanceItem.start_date) }})<br />
+                    <!-- Incident Date -->
+                    <div class="date mt-3">
+                        {{ $t("End") }}: {{ $root.datetimeMaintenance(maintenanceItem.end_date) }}
+                        ({{ dateFromNow(maintenanceItem.start_date) }})<br/>
+                    </div>
                 </div>
-            </div>
+            </template>
 
             <!-- Overall Status -->
             <div class="shadow-box list  p-4 overall-status mb-4">
@@ -300,7 +304,7 @@
 import axios from "axios";
 import PublicGroupList from "../components/PublicGroupList.vue";
 import ImageCropUpload from "vue-image-crop-upload";
-import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, 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 dayjs from "dayjs";
 import Favico from "favico.js";
@@ -579,14 +583,10 @@ export default {
             }
 
             this.incident = res.data.incident;
+            this.maintenance = res.data.maintenance;
             this.$root.publicGroupList = res.data.publicGroupList;
         });
 
-        //TODO: make OK with multi status pages
-        axios.get("/api/status-page/maintenance-list").then((res) => {
-            this.maintenance = res.data;
-        });
-
         // 5mins a loop
         this.updateHeartbeatList();
         feedInterval = setInterval(() => {
diff --git a/src/util.ts b/src/util.ts
index fa67db10..b307295a 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -280,7 +280,7 @@ export function genSecret(length = 64) {
 }
 
 export function getMonitorRelativeURL(id: string) {
-    return "/dashboard/monitor/" + id;
+    return "/dashboard/" + id;
 }
 
 export function getMaintenanceRelativeURL(id: string) {

From f78d01d770e36cb80abbb0c73858b53c4cde37f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?= <karel.kryda@gmail.com>
Date: Sat, 30 Apr 2022 14:57:08 +0200
Subject: [PATCH 006/134] Resolve lint errors

---
 CNAME                            |  2 +-
 public/icon.svg                  |  2 +-
 server/model/monitor.js          |  8 ++--
 src/components/MonitorList.vue   | 71 +++++++++++++++++++-------------
 src/components/Uptime.vue        |  6 +--
 src/mixins/datetime.js           |  5 ++-
 src/mixins/socket.js             |  6 +--
 src/pages/Dashboard.vue          |  4 +-
 src/pages/EditMaintenance.vue    | 39 +++++++++++-------
 src/pages/MaintenanceDetails.vue |  8 ++--
 src/pages/StatusPage.vue         | 34 ++++++++-------
 11 files changed, 105 insertions(+), 80 deletions(-)

diff --git a/CNAME b/CNAME
index a5348b07..44250516 100644
--- a/CNAME
+++ b/CNAME
@@ -1 +1 @@
-git.kuma.pet
\ No newline at end of file
+git.kuma.pet
diff --git a/public/icon.svg b/public/icon.svg
index 825c344e..9c0bc6ca 100644
--- a/public/icon.svg
+++ b/public/icon.svg
@@ -1,3 +1,3 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 640 640" width="640" height="640"><defs><path d="M407.55 916.24C471.25 916.24 522.89 967.88 522.89 1031.57C522.89 1113.88 522.89 1245.44 522.89 1327.74C522.89 1391.44 471.25 1443.08 407.55 1443.08C325.25 1443.08 193.68 1443.08 111.38 1443.08C47.69 1443.08 -3.95 1391.44 -3.95 1327.74C-3.95 1245.44 -3.95 1113.88 -3.95 1031.57C-3.95 967.88 47.69 916.24 111.38 916.24C193.68 916.24 325.25 916.24 407.55 916.24Z" id="a1LdTs1gvU"></path><linearGradient id="gradientcoH7TNh19" gradientUnits="userSpaceOnUse" x1="256.07" y1="1132.14" x2="609.11" y2="1480.42"><stop style="stop-color: #c2efd2;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #8ff0e5;stop-opacity: 1" offset="100%"></stop></linearGradient><path d="M-467.41 394.63C-467.41 554.76 -597.42 684.76 -757.55 684.76C-917.68 684.76 -1047.69 554.76 -1047.69 394.63C-1047.69 234.5 -917.68 104.49 -757.55 104.49C-597.42 104.49 -467.41 234.5 -467.41 394.63Z" id="a1uaEBd4xM"></path><path d="M-96.99 -586.14C-57.24 -619.85 -5.79 -604.75 19.26 -580.46C31.43 -568.66 56.57 -546.36 40.97 -491.67C32.76 -462.87 10.41 -436.4 -26.05 -412.27C-15.07 -377.85 -5.6 -344.76 2.36 -313C14.29 -265.36 13.55 -189.67 -26.05 -155.4C-67.27 -119.73 -166.91 -104.09 -234.24 -103.09C-301.57 -102.1 -406.19 -113.09 -461.6 -155.4C-517.01 -197.7 -512.24 -257.07 -498.04 -313C-488.58 -350.28 -476.43 -383.38 -461.6 -412.27C-505.54 -441.3 -530.54 -467.76 -536.6 -491.67C-545.68 -527.54 -530.93 -565.61 -501.12 -586.14C-471.31 -606.67 -435.18 -606.9 -400.45 -586.14C-377.3 -572.3 -354.79 -542.13 -332.92 -495.62C-287.85 -505.25 -254.96 -509.57 -234.24 -508.6C-214.74 -507.68 -186.57 -503.36 -149.72 -495.62C-135.81 -537.95 -118.23 -568.12 -96.99 -586.14Z" id="f8p7QlEjN3"></path><linearGradient id="gradienta4Tg99ZOOp" gradientUnits="userSpaceOnUse" x1="-440.25" y1="-388.59" x2="-100.49" y2="-147.33"><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #7ae6a1;stop-opacity: 1" offset="100%"></stop></linearGradient><path d="M-86.03 -10.69C-61.35 -10.69 -41.34 9.32 -41.34 34.01C-41.34 119.07 -41.34 329.58 -41.34 414.65C-41.34 439.33 -61.35 459.34 -86.03 459.34C-136.01 459.34 -241.25 459.34 -291.23 459.34C-315.92 459.34 -335.93 439.33 -335.93 414.65C-335.93 329.58 -335.93 119.07 -335.93 34.01C-335.93 9.32 -315.92 -10.69 -291.23 -10.69C-241.25 -10.69 -136.01 -10.69 -86.03 -10.69Z" id="d32ZZRxd1S"></path><linearGradient id="gradientb1JxIe4xUm" gradientUnits="userSpaceOnUse" x1="-791.65" y1="-33.27" x2="892.1" y2="418.94"><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #5ae98f;stop-opacity: 1" offset="100%"></stop></linearGradient><path d="M-257.95 458.12C-247.92 449.62 -234.93 453.43 -228.61 459.56C-225.54 462.54 -219.19 468.17 -223.13 481.97C-225.2 489.24 -230.84 495.92 -240.05 502.01C-237.27 510.7 -234.88 519.06 -232.88 527.07C-229.86 539.1 -230.05 558.21 -240.05 566.86C-250.45 575.86 -275.6 579.81 -292.6 580.06C-309.6 580.31 -336.01 577.54 -349.99 566.86C-363.98 556.18 -362.77 541.19 -359.19 527.07C-356.8 517.66 -353.73 509.31 -349.99 502.01C-361.08 494.69 -367.39 488.01 -368.92 481.97C-371.22 472.92 -367.49 463.31 -359.97 458.12C-352.44 452.94 -343.32 452.88 -334.56 458.12C-328.71 461.62 -323.03 469.23 -317.51 480.97C-306.13 478.54 -297.83 477.45 -292.6 477.7C-287.68 477.93 -280.56 479.02 -271.26 480.97C-267.75 470.29 -263.32 462.67 -257.95 458.12Z" id="b19LRRbPrG"></path><path d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.26 407.74 99.26 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z" id="bN5StdyPU"></path><linearGradient id="gradientb1HT15TsY0" gradientUnits="userSpaceOnUse" x1="259.78" y1="261.15" x2="463.85" y2="456.49"><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #86e6a9;stop-opacity: 1" offset="100%"></stop></linearGradient><path d="M393.81 -775.89C428.26 -748.09 439.99 -725.54 429 -708.22C412.51 -682.24 353.16 -646.07 324.5 -657.93C305.39 -665.83 294.22 -687.32 290.97 -722.41C292.69 -748.43 304.61 -767.19 326.73 -778.69C348.85 -790.19 371.21 -789.26 393.81 -775.89Z" id="arh6miPP2"></path><linearGradient id="gradientc2g6rBSAiq" gradientUnits="userSpaceOnUse" x1="330.1" y1="-733.26" x2="419.69" y2="-707.1"><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #86e6a9;stop-opacity: 1" offset="100%"></stop></linearGradient><path d="M675.36 -369.24C669.97 -325.31 657.02 -303.43 636.51 -303.61C605.74 -303.87 543.67 -335.15 538.59 -365.74C535.2 -386.14 547.54 -406.99 575.61 -428.29C598.61 -440.58 620.83 -440.37 642.29 -427.67C663.74 -414.97 674.77 -395.49 675.36 -369.24Z" id="a2VENFzCvL"></path><linearGradient id="gradientc18GuJy4sZ" gradientUnits="userSpaceOnUse" x1="605.5" y1="-400.8" x2="630.64" y2="-310.92"><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #86e6a9;stop-opacity: 1" offset="100%"></stop></linearGradient></defs><g><g><g><use xlink:href="#a1LdTs1gvU" opacity="1" fill="url(#gradientcoH7TNh19)"></use></g><g><use xlink:href="#a1uaEBd4xM" opacity="1" fill="#ebf0ed" fill-opacity="1"></use></g><g><use xlink:href="#f8p7QlEjN3" opacity="1" fill="url(#gradienta4Tg99ZOOp)"></use><g><use xlink:href="#f8p7QlEjN3" opacity="1" fill-opacity="0" stroke="#ffffff" stroke-width="98" stroke-opacity="0.57"></use></g></g><g><use xlink:href="#d32ZZRxd1S" opacity="1" fill="url(#gradientb1JxIe4xUm)"></use><g><use xlink:href="#d32ZZRxd1S" opacity="1" fill-opacity="0" stroke="#f2f2f2" stroke-width="60" stroke-opacity="0.51"></use></g></g><g><use xlink:href="#b19LRRbPrG" opacity="1" fill="#d8ad9a" fill-opacity="1"></use><g><use xlink:href="#b19LRRbPrG" opacity="1" fill-opacity="0" stroke="#ffffff" stroke-width="17" stroke-opacity="1"></use></g></g><g><use xlink:href="#bN5StdyPU" opacity="1" fill="url(#gradientb1HT15TsY0)"></use><g><use xlink:href="#bN5StdyPU" opacity="1" fill-opacity="0" stroke="#f2f2f2" stroke-width="200" stroke-opacity="0.51"></use></g></g><g><use xlink:href="#arh6miPP2" opacity="1" fill="url(#gradientc2g6rBSAiq)"></use></g><g><use xlink:href="#a2VENFzCvL" opacity="1" fill="url(#gradientc18GuJy4sZ)"></use></g></g></g></svg>
\ No newline at end of file
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 640 640" width="640" height="640"><defs><path d="M407.55 916.24C471.25 916.24 522.89 967.88 522.89 1031.57C522.89 1113.88 522.89 1245.44 522.89 1327.74C522.89 1391.44 471.25 1443.08 407.55 1443.08C325.25 1443.08 193.68 1443.08 111.38 1443.08C47.69 1443.08 -3.95 1391.44 -3.95 1327.74C-3.95 1245.44 -3.95 1113.88 -3.95 1031.57C-3.95 967.88 47.69 916.24 111.38 916.24C193.68 916.24 325.25 916.24 407.55 916.24Z" id="a1LdTs1gvU"></path><linearGradient id="gradientcoH7TNh19" gradientUnits="userSpaceOnUse" x1="256.07" y1="1132.14" x2="609.11" y2="1480.42"><stop style="stop-color: #c2efd2;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #8ff0e5;stop-opacity: 1" offset="100%"></stop></linearGradient><path d="M-467.41 394.63C-467.41 554.76 -597.42 684.76 -757.55 684.76C-917.68 684.76 -1047.69 554.76 -1047.69 394.63C-1047.69 234.5 -917.68 104.49 -757.55 104.49C-597.42 104.49 -467.41 234.5 -467.41 394.63Z" id="a1uaEBd4xM"></path><path d="M-96.99 -586.14C-57.24 -619.85 -5.79 -604.75 19.26 -580.46C31.43 -568.66 56.57 -546.36 40.97 -491.67C32.76 -462.87 10.41 -436.4 -26.05 -412.27C-15.07 -377.85 -5.6 -344.76 2.36 -313C14.29 -265.36 13.55 -189.67 -26.05 -155.4C-67.27 -119.73 -166.91 -104.09 -234.24 -103.09C-301.57 -102.1 -406.19 -113.09 -461.6 -155.4C-517.01 -197.7 -512.24 -257.07 -498.04 -313C-488.58 -350.28 -476.43 -383.38 -461.6 -412.27C-505.54 -441.3 -530.54 -467.76 -536.6 -491.67C-545.68 -527.54 -530.93 -565.61 -501.12 -586.14C-471.31 -606.67 -435.18 -606.9 -400.45 -586.14C-377.3 -572.3 -354.79 -542.13 -332.92 -495.62C-287.85 -505.25 -254.96 -509.57 -234.24 -508.6C-214.74 -507.68 -186.57 -503.36 -149.72 -495.62C-135.81 -537.95 -118.23 -568.12 -96.99 -586.14Z" id="f8p7QlEjN3"></path><linearGradient id="gradienta4Tg99ZOOp" gradientUnits="userSpaceOnUse" x1="-440.25" y1="-388.59" x2="-100.49" y2="-147.33"><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #7ae6a1;stop-opacity: 1" offset="100%"></stop></linearGradient><path d="M-86.03 -10.69C-61.35 -10.69 -41.34 9.32 -41.34 34.01C-41.34 119.07 -41.34 329.58 -41.34 414.65C-41.34 439.33 -61.35 459.34 -86.03 459.34C-136.01 459.34 -241.25 459.34 -291.23 459.34C-315.92 459.34 -335.93 439.33 -335.93 414.65C-335.93 329.58 -335.93 119.07 -335.93 34.01C-335.93 9.32 -315.92 -10.69 -291.23 -10.69C-241.25 -10.69 -136.01 -10.69 -86.03 -10.69Z" id="d32ZZRxd1S"></path><linearGradient id="gradientb1JxIe4xUm" gradientUnits="userSpaceOnUse" x1="-791.65" y1="-33.27" x2="892.1" y2="418.94"><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #5ae98f;stop-opacity: 1" offset="100%"></stop></linearGradient><path d="M-257.95 458.12C-247.92 449.62 -234.93 453.43 -228.61 459.56C-225.54 462.54 -219.19 468.17 -223.13 481.97C-225.2 489.24 -230.84 495.92 -240.05 502.01C-237.27 510.7 -234.88 519.06 -232.88 527.07C-229.86 539.1 -230.05 558.21 -240.05 566.86C-250.45 575.86 -275.6 579.81 -292.6 580.06C-309.6 580.31 -336.01 577.54 -349.99 566.86C-363.98 556.18 -362.77 541.19 -359.19 527.07C-356.8 517.66 -353.73 509.31 -349.99 502.01C-361.08 494.69 -367.39 488.01 -368.92 481.97C-371.22 472.92 -367.49 463.31 -359.97 458.12C-352.44 452.94 -343.32 452.88 -334.56 458.12C-328.71 461.62 -323.03 469.23 -317.51 480.97C-306.13 478.54 -297.83 477.45 -292.6 477.7C-287.68 477.93 -280.56 479.02 -271.26 480.97C-267.75 470.29 -263.32 462.67 -257.95 458.12Z" id="b19LRRbPrG"></path><path d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.26 407.74 99.26 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z" id="bN5StdyPU"></path><linearGradient id="gradientb1HT15TsY0" gradientUnits="userSpaceOnUse" x1="259.78" y1="261.15" x2="463.85" y2="456.49"><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #86e6a9;stop-opacity: 1" offset="100%"></stop></linearGradient><path d="M393.81 -775.89C428.26 -748.09 439.99 -725.54 429 -708.22C412.51 -682.24 353.16 -646.07 324.5 -657.93C305.39 -665.83 294.22 -687.32 290.97 -722.41C292.69 -748.43 304.61 -767.19 326.73 -778.69C348.85 -790.19 371.21 -789.26 393.81 -775.89Z" id="arh6miPP2"></path><linearGradient id="gradientc2g6rBSAiq" gradientUnits="userSpaceOnUse" x1="330.1" y1="-733.26" x2="419.69" y2="-707.1"><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #86e6a9;stop-opacity: 1" offset="100%"></stop></linearGradient><path d="M675.36 -369.24C669.97 -325.31 657.02 -303.43 636.51 -303.61C605.74 -303.87 543.67 -335.15 538.59 -365.74C535.2 -386.14 547.54 -406.99 575.61 -428.29C598.61 -440.58 620.83 -440.37 642.29 -427.67C663.74 -414.97 674.77 -395.49 675.36 -369.24Z" id="a2VENFzCvL"></path><linearGradient id="gradientc18GuJy4sZ" gradientUnits="userSpaceOnUse" x1="605.5" y1="-400.8" x2="630.64" y2="-310.92"><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #86e6a9;stop-opacity: 1" offset="100%"></stop></linearGradient></defs><g><g><g><use xlink:href="#a1LdTs1gvU" opacity="1" fill="url(#gradientcoH7TNh19)"></use></g><g><use xlink:href="#a1uaEBd4xM" opacity="1" fill="#ebf0ed" fill-opacity="1"></use></g><g><use xlink:href="#f8p7QlEjN3" opacity="1" fill="url(#gradienta4Tg99ZOOp)"></use><g><use xlink:href="#f8p7QlEjN3" opacity="1" fill-opacity="0" stroke="#ffffff" stroke-width="98" stroke-opacity="0.57"></use></g></g><g><use xlink:href="#d32ZZRxd1S" opacity="1" fill="url(#gradientb1JxIe4xUm)"></use><g><use xlink:href="#d32ZZRxd1S" opacity="1" fill-opacity="0" stroke="#f2f2f2" stroke-width="60" stroke-opacity="0.51"></use></g></g><g><use xlink:href="#b19LRRbPrG" opacity="1" fill="#d8ad9a" fill-opacity="1"></use><g><use xlink:href="#b19LRRbPrG" opacity="1" fill-opacity="0" stroke="#ffffff" stroke-width="17" stroke-opacity="1"></use></g></g><g><use xlink:href="#bN5StdyPU" opacity="1" fill="url(#gradientb1HT15TsY0)"></use><g><use xlink:href="#bN5StdyPU" opacity="1" fill-opacity="0" stroke="#f2f2f2" stroke-width="200" stroke-opacity="0.51"></use></g></g><g><use xlink:href="#arh6miPP2" opacity="1" fill="url(#gradientc2g6rBSAiq)"></use></g><g><use xlink:href="#a2VENFzCvL" opacity="1" fill="url(#gradientc18GuJy4sZ)"></use></g></g></g></svg>
diff --git a/server/model/monitor.js b/server/model/monitor.js
index 115f86da..23876ac7 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -212,8 +212,7 @@ class Monitor extends BeanModel {
                 if (await Monitor.isUnderMaintenance(this.id)) {
                     bean.msg = "Monitor under maintenance";
                     bean.status = MAINTENANCE;
-                }
-                else if (this.type === "http" || this.type === "keyword") {
+                } else if (this.type === "http" || this.type === "keyword") {
                     // Do not do any queries/high loading things before the "bean.ping"
                     let startTime = dayjs().valueOf();
 
@@ -482,8 +481,7 @@ class Monitor extends BeanModel {
                 if (Monitor.isImportantForNotification(isFirstBeat, previousBeat?.status, bean.status)) {
                     log.debug("monitor", `[${this.name}] sendNotification`);
                     await Monitor.sendNotification(isFirstBeat, this, bean);
-                }
-                else {
+                } else {
                     log.debug("monitor", `[${this.name}] will not sendNotification because it is (or was) under maintenance`);
                 }
 
@@ -950,7 +948,7 @@ class Monitor extends BeanModel {
     }
 
     static async isUnderMaintenance(monitorID) {
-        const maintenance = await R.getRow("SELECT COUNT(*) AS count FROM monitor_maintenance mm JOIN maintenance ON mm.maintenance_id = maintenance.id WHERE mm.monitor_id = ? AND datetime(maintenance.start_date) <= datetime('now') AND datetime(maintenance.end_date) >= datetime('now') LIMIT 1", [monitorID]);
+        const maintenance = await R.getRow("SELECT COUNT(*) AS count FROM monitor_maintenance mm JOIN maintenance ON mm.maintenance_id = maintenance.id WHERE mm.monitor_id = ? AND datetime(maintenance.start_date) <= datetime('now') AND datetime(maintenance.end_date) >= datetime('now') LIMIT 1", [ monitorID ]);
         return maintenance.count !== 0;
     }
 }
diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue
index 4e761e77..8662dfee 100644
--- a/src/components/MonitorList.vue
+++ b/src/components/MonitorList.vue
@@ -3,8 +3,8 @@
         <div class="list-header">
             <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>
+                    <option value="monitor" selected>{{ $t('Monitor List') }}</option>
+                    <option value="maintenance">{{ $t('Maintenance List') }}</option>
                 </select>
             </div>
             <div class="search-wrapper">
@@ -27,39 +27,55 @@
                 {{ $t("No Maintenance, please") }} <router-link to="/addMaintenance">{{ $t("add one") }}</router-link>
             </div>
 
-            <router-link v-if="selectedList === 'maintenance'" v-for="(item, index) in sortedMaintenanceList" :key="index" :to="maintenanceURL(item.id)" class="item" :class="{ 'disabled': !this.$root.isActiveMaintenance(item.end_date) }">
-                <div class="row">
-                    <div class="col-9 col-md-8 small-padding">
-                        <div class="info">
-                            <Uptime :monitor="null" type="maintenance" :pill="true" />
-                            {{ item.title }}
+            <template v-if="selectedList === 'maintenance'">
+                <router-link
+                    v-for="(item, index) in sortedMaintenanceList" :key="index" :to="maintenanceURL(item.id)"
+                    class="item" :class="{ 'disabled': !$root.isActiveMaintenance(item.end_date) }"
+                >
+                    <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>
-                </div>
-            </router-link>
+                </router-link>
+            </template>
 
-            <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="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
-                        <div class="info">
-                            <Uptime :monitor="item" type="24" :pill="true" />
-                            {{ item.name }}
+            <template v-if="selectedList === 'monitor'">
+                <router-link
+                    v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)"
+                    class="item" :class="{ 'disabled': ! item.active }"
+                >
+                    <div class="row">
+                        <div
+                            class="col-9 col-md-8 small-padding"
+                            :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }"
+                        >
+                            <div class="info">
+                                <Uptime :monitor="item" type="24" :pill="true" />
+                                {{ item.name }}
+                            </div>
+                            <div class="tags">
+                                <Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
+                            </div>
                         </div>
-                        <div class="tags">
-                            <Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
+                        <div
+                            v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar"
+                            class="col-3 col-md-4"
+                        >
+                            <HeartbeatBar size="small" :monitor-id="item.id" />
                         </div>
                     </div>
-                    <div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
-                        <HeartbeatBar size="small" :monitor-id="item.id" />
-                    </div>
-                </div>
 
-                <div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
-                    <div class="col-12 bottom-style">
-                        <HeartbeatBar size="small" :monitor-id="item.id" />
+                    <div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
+                        <div class="col-12 bottom-style">
+                            <HeartbeatBar size="small" :monitor-id="item.id" />
+                        </div>
                     </div>
-                </div>
-            </router-link>
+                </router-link>
+            </template>
         </div>
     </div>
 </template>
@@ -282,7 +298,6 @@ export default {
     margin-top: 5px;
 }
 
-
 .bg-maintenance {
     background-color: $maintenance;
 }
diff --git a/src/components/Uptime.vue b/src/components/Uptime.vue
index 226dfae3..d663db88 100644
--- a/src/components/Uptime.vue
+++ b/src/components/Uptime.vue
@@ -15,7 +15,7 @@ export default {
 
     computed: {
         uptime() {
-            
+
             if (this.type === "maintenance") {
                 return this.$t("Maintenance");
             }
@@ -31,9 +31,9 @@ export default {
 
         color() {
             if (this.type === "maintenance" || this.monitor.maintenance) {
-                return "maintenance"
+                return "maintenance";
             }
-            
+
             if (this.lastHeartBeat.status === 0) {
                 return "danger";
             }
diff --git a/src/mixins/datetime.js b/src/mixins/datetime.js
index 3f4749af..c6461562 100644
--- a/src/mixins/datetime.js
+++ b/src/mixins/datetime.js
@@ -34,10 +34,11 @@ export default {
             const inputDate = new Date(value);
             const now = new Date(Date.now());
 
-            if (inputDate.getFullYear() === now.getUTCFullYear() && inputDate.getMonth() === now.getUTCMonth() && inputDate.getDay() === now.getUTCDay())
+            if (inputDate.getFullYear() === now.getUTCFullYear() && inputDate.getMonth() === now.getUTCMonth() && inputDate.getDay() === now.getUTCDay()) {
                 return this.datetimeFormat(value, "HH:mm");
-            else
+            } else {
                 return this.datetimeFormat(value, "YYYY-MM-DD HH:mm");
+            }
         },
 
         date(value) {
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
index 8d419706..6d4311b1 100644
--- a/src/mixins/socket.js
+++ b/src/mixins/socket.js
@@ -464,8 +464,7 @@ export default {
                         text: this.$t("Maintenance"),
                         color: "maintenance",
                     };
-                }
-                else if (! lastHeartBeat) {
+                } else if (! lastHeartBeat) {
                     result[monitorID] = unknown;
                 } else if (lastHeartBeat.status === 1) {
                     result[monitorID] = {
@@ -505,8 +504,7 @@ export default {
 
                 if (monitor && monitor.maintenance) {
                     result.maintenance++;
-                }
-                else if (monitor && ! monitor.active) {
+                } else if (monitor && ! monitor.active) {
                     result.pause++;
                 } else if (beat) {
                     if (beat.status === 1) {
diff --git a/src/pages/Dashboard.vue b/src/pages/Dashboard.vue
index e99422a4..207e5b3a 100644
--- a/src/pages/Dashboard.vue
+++ b/src/pages/Dashboard.vue
@@ -8,12 +8,12 @@
                     </button>
                     <ul class="dropdown-menu dropdown-menu-end">
                         <li>
-                            <button type="button" class="dropdown-item" @click="this.$router.push('/add')">
+                            <button type="button" class="dropdown-item" @click="$router.push('/add')">
                                 <font-awesome-icon icon="heartbeat" /> {{ $t("Monitor") }}
                             </button>
                         </li>
                         <li>
-                            <button type="button" class="dropdown-item" @click="this.$router.push('/addMaintenance')">
+                            <button type="button" class="dropdown-item" @click="$router.push('/addMaintenance')">
                                 <font-awesome-icon icon="exclamation-circle" /> {{ $t("Maintenance") }}
                             </button>
                         </li>
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index e50affaa..84675c0c 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -11,15 +11,19 @@
                             <!-- 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>
+                                <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>
+                                <textarea
+                                    id="description" v-model="maintenance.description" class="form-control"
+                                    :placeholder="descriptionPlaceholder"
+                                ></textarea>
                             </div>
 
                             <!-- Affected Monitors -->
@@ -50,21 +54,28 @@
 
                             <!-- Start Date Time -->
                             <div class="my-3">
-                                <label for="start_date" class="form-label">{{ $t("Start of maintenance") }} ({{this.$root.timezone}})</label>
-                                <input :type="'datetime-local'" id="start_date" v-model="maintenance.start_date"
-                                       class="form-control" :class="{'darkCalendar': dark }" required>
+                                <label for="start_date" class="form-label">{{ $t("Start of maintenance") }} ({{ $root.timezone }})</label>
+                                <input
+                                    id="start_date" v-model="maintenance.start_date" :type="'datetime-local'"
+                                    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") }} ({{this.$root.timezone}})</label>
-                                <input :type="'datetime-local'" id="end_date" v-model="maintenance.end_date"
-                                       class="form-control" :class="{'darkCalendar': dark }" required>
+                                <label for="end_date" class="form-label">{{ $t("Expected end of maintenance") }} ({{ $root.timezone }})</label>
+                                <input
+                                    id="end_date" v-model="maintenance.end_date" :type="'datetime-local'"
+                                    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
+                                    id="monitor-submit-btn" class="btn btn-primary" type="submit"
+                                    :disabled="processing"
+                                >
+                                    {{ $t("Save") }}
                                 </button>
                             </div>
                         </div>
@@ -78,7 +89,7 @@
 <script>
 import CopyableInput from "../components/CopyableInput.vue";
 
-import {useToast} from "vue-toastification";
+import { useToast } from "vue-toastification";
 import VueMultiselect from "vue-multiselect";
 
 const toast = useToast();
@@ -145,7 +156,7 @@ export default {
     methods: {
         init() {
             this.affectedMonitors = [];
-            
+
             if (this.isAdd) {
                 this.maintenance = {
                     title: "",
diff --git a/src/pages/MaintenanceDetails.vue b/src/pages/MaintenanceDetails.vue
index 77d70078..c3b9abee 100644
--- a/src/pages/MaintenanceDetails.vue
+++ b/src/pages/MaintenanceDetails.vue
@@ -3,9 +3,9 @@
         <div v-if="maintenance">
             <h1> {{ maintenance.title }}</h1>
             <p class="url">
-                <span>{{$t("Start")}}: {{ $root.datetimeMaintenance(maintenance.start_date) }}</span>
+                <span>{{ $t("Start") }}: {{ $root.datetimeMaintenance(maintenance.start_date) }}</span>
                 <br>
-                <span>{{$t("End")}}: {{ $root.datetimeMaintenance(maintenance.end_date) }}</span>
+                <span>{{ $t("End") }}: {{ $root.datetimeMaintenance(maintenance.end_date) }}</span>
             </p>
 
             <div class="functions" style="margin-top: 10px">
@@ -22,7 +22,7 @@
 
             <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">
+            <button v-for="monitor in affectedMonitors" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: bold">
                 {{ monitor }}
             </button>
 
@@ -66,7 +66,7 @@ export default {
                 }
             });
         },
-        
+
         deleteDialog() {
             this.$refs.confirmDelete.show();
         },
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index 7ae7c636..46a2c79f 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -104,15 +104,16 @@
 
                 <!-- Uploader -->
                 <!--    url="/api/status-page/upload-logo" -->
-                <ImageCropUpload v-model="showImageCropUpload"
-                                 field="img"
-                                 :width="128"
-                                 :height="128"
-                                 :langType="$i18n.locale"
-                                 img-format="png"
-                                 :noCircle="true"
-                                 :noSquare="false"
-                                 @crop-success="cropSuccess"
+                <ImageCropUpload
+                    v-model="showImageCropUpload"
+                    field="img"
+                    :width="128"
+                    :height="128"
+                    :langType="$i18n.locale"
+                    img-format="png"
+                    :noCircle="true"
+                    :noSquare="false"
+                    @crop-success="cropSuccess"
                 />
 
                 <!-- Title -->
@@ -196,16 +197,18 @@
 
             <!-- Maintenance -->
             <template v-if="maintenance.length !== 0">
-                <div 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-for="maintenanceItem in maintenance" class="shadow-box alert mb-4 p-4 maintenance" role="alert"
+                    :class="maintenanceClass"
+                >
+                    <h4 class="alert-heading" v-text="maintenanceItem.title" />
 
-                    <div v-text="maintenanceItem.description" class="content"/>
+                    <div class="content" v-text="maintenanceItem.description" />
 
                     <!-- Incident Date -->
                     <div class="date mt-3">
                         {{ $t("End") }}: {{ $root.datetimeMaintenance(maintenanceItem.end_date) }}
-                        ({{ dateFromNow(maintenanceItem.start_date) }})<br/>
+                        ({{ dateFromNow(maintenanceItem.start_date) }})<br />
                     </div>
                 </div>
             </template>
@@ -448,8 +451,7 @@ export default {
 
                 if (beat.status === MAINTENANCE) {
                     return STATUS_PAGE_MAINTENANCE;
-                }
-                else if (beat.status === UP) {
+                } else if (beat.status === UP) {
                     hasUp = true;
                 } else {
                     status = STATUS_PAGE_PARTIAL_DOWN;

From 11ef22edec3502ec5a9b90c609b7f2f4f1588505 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?= <karel.kryda@gmail.com>
Date: Sat, 30 Apr 2022 15:13:13 +0200
Subject: [PATCH 007/134] Fixed remaining lint errors

---
 src/languages/en.js              | 1 -
 src/pages/EditMaintenance.vue    | 2 --
 src/pages/MaintenanceDetails.vue | 6 +++---
 src/pages/StatusPage.vue         | 2 +-
 4 files changed, 4 insertions(+), 7 deletions(-)

diff --git a/src/languages/en.js b/src/languages/en.js
index df4e583e..8bb1a73c 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -15,7 +15,6 @@ export default {
     "Pick Affected Monitors...": "Pick Affected Monitors...",
     "Start of maintenance": "Start of maintenance",
     "Expected end of maintenance": "Expected end of maintenance",
-    Start: "Start",
     End: "End",
     affectedMonitorsDescription: "Select monitors that are affected by current maintenance",
     atLeastOneMonitor: "Select at least one affected monitor",
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 84675c0c..e8cfeb4f 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -87,7 +87,6 @@
 </template>
 
 <script>
-import CopyableInput from "../components/CopyableInput.vue";
 
 import { useToast } from "vue-toastification";
 import VueMultiselect from "vue-multiselect";
@@ -96,7 +95,6 @@ const toast = useToast();
 
 export default {
     components: {
-        CopyableInput,
         VueMultiselect,
     },
 
diff --git a/src/pages/MaintenanceDetails.vue b/src/pages/MaintenanceDetails.vue
index c3b9abee..4f9779f6 100644
--- a/src/pages/MaintenanceDetails.vue
+++ b/src/pages/MaintenanceDetails.vue
@@ -1,7 +1,7 @@
 <template>
     <transition name="slide-fade" appear>
         <div v-if="maintenance">
-            <h1> {{ maintenance.title }}</h1>
+            <h1>{{ maintenance.title }}</h1>
             <p class="url">
                 <span>{{ $t("Start") }}: {{ $root.datetimeMaintenance(maintenance.start_date) }}</span>
                 <br>
@@ -18,11 +18,11 @@
             </div>
 
             <label for="description" class="form-label" style="margin-top: 20px">{{ $t("Description") }}</label>
-            <textarea id="description" class="form-control" disabled>{{ maintenance.description }}</textarea>
+            <textarea id="description" v-model="maintenance.description" class="form-control" disabled></textarea>
 
             <label for="affected_monitors" class="form-label" style="margin-top: 20px">{{ $t("Affected Monitors") }}</label>
             <br>
-            <button v-for="monitor in affectedMonitors" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: bold">
+            <button v-for="monitor in affectedMonitors" :key="monitor.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: bold">
                 {{ monitor }}
             </button>
 
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index 46a2c79f..8914d5ca 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -198,7 +198,7 @@
             <!-- Maintenance -->
             <template v-if="maintenance.length !== 0">
                 <div
-                    v-for="maintenanceItem in maintenance" class="shadow-box alert mb-4 p-4 maintenance" role="alert"
+                    v-for="maintenanceItem in maintenance" :key="maintenanceItem.id" class="shadow-box alert mb-4 p-4 maintenance" role="alert"
                     :class="maintenanceClass"
                 >
                     <h4 class="alert-heading" v-text="maintenanceItem.title" />

From 57368c8c6caca877c1a076969327dd50bbaa4533 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?= <karel.kryda@gmail.com>
Date: Sat, 30 Apr 2022 15:32:56 +0200
Subject: [PATCH 008/134] More modern look of maintenance information on status
 page (same design as for the new incident system)

---
 src/pages/StatusPage.vue | 48 +++++++++++++++++++++++++---------------
 1 file changed, 30 insertions(+), 18 deletions(-)

diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index 8914d5ca..015a57be 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -196,19 +196,29 @@
             </div>
 
             <!-- Maintenance -->
-            <template v-if="maintenance.length !== 0">
+            <template v-if="maintenance.length">
                 <div
-                    v-for="maintenanceItem in maintenance" :key="maintenanceItem.id" class="shadow-box alert mb-4 p-4 maintenance" role="alert"
-                    :class="maintenanceClass"
+                    v-for="maintenanceItem in maintenance" :key="maintenanceItem.id"
+                    class="shadow-box alert mb-4 p-4 maintenance mt-4 position-relative" role="alert"
                 >
-                    <h4 class="alert-heading" v-text="maintenanceItem.title" />
+                    <div class="item">
+                        <div class="row">
+                            <div class="col-1 col-md-1 d-flex justify-content-center align-items-center">
+                                <font-awesome-icon
+                                    icon="wrench"
+                                    class="maintenance-icon maintenance-bg-info"
+                                />
+                            </div>
+                            <div class="col-11 col-md-11">
+                                <h4 class="alert-heading">{{ maintenanceItem.title }}</h4>
+                                <div class="content">{{ maintenanceItem.description }}</div>
 
-                    <div class="content" v-text="maintenanceItem.description" />
-
-                    <!-- Incident Date -->
-                    <div class="date mt-3">
-                        {{ $t("End") }}: {{ $root.datetimeMaintenance(maintenanceItem.end_date) }}
-                        ({{ dateFromNow(maintenanceItem.start_date) }})<br />
+                                <div class="date mt-3">
+                                    {{ $t("End") }}: {{ $root.datetimeMaintenance(maintenanceItem.end_date) }}
+                                    ({{ dateFromNow(maintenanceItem.start_date) }})<br />
+                                </div>
+                            </div>
+                        </div>
                     </div>
                 </div>
             </template>
@@ -933,20 +943,22 @@ footer {
     }
 }
 
-.maintenance {
-    color: white;
-
-    .date {
-        font-size: 12px;
-    }
+.maintenance-bg-info {
+    color: $maintenance;
 }
 
-.bg-maintenance {
-    background-color: $maintenance;
+.maintenance-icon {
+    font-size: 30px;
+    vertical-align: middle;
+}
+
+.dark .shadow-box {
+    background-color: #0d1117;
 }
 
 .statusMaintenance {
     color: $maintenance;
+    margin-right: 5px;
 }
 
 .mobile {

From b4ffcc5555bad78ea9b6828e08934517c3761825 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?= <karel.kryda@gmail.com>
Date: Sat, 30 Apr 2022 15:50:05 +0200
Subject: [PATCH 009/134] Added JSDoc

---
 server/model/maintenance.js  | 6 ++++--
 server/model/monitor.js      | 5 +++++
 server/routers/api-router.js | 2 +-
 server/server.js             | 5 +++++
 4 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index 55308895..1b0b9ee0 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -8,8 +8,9 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
 class Maintenance extends BeanModel {
 
     /**
-     * Return a object that ready to parse to JSON for public
+     * Return an object that ready to parse to JSON for public
      * Only show necessary data to public
+     * @returns {Object}
      */
     async toPublicJSON() {
         return {
@@ -22,7 +23,8 @@ class Maintenance extends BeanModel {
     }
 
     /**
-     * Return a object that ready to parse to JSON
+     * Return an object that ready to parse to JSON
+     * @returns {Object}
      */
     async toJSON() {
         return {
diff --git a/server/model/monitor.js b/server/model/monitor.js
index 23876ac7..6587d2cd 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -947,6 +947,11 @@ class Monitor extends BeanModel {
         ]);
     }
 
+    /**
+     * Check if monitor is under maintenance
+     * @param {number} monitorID ID of monitor to check
+     * @returns {Promise<boolean>}
+     */
     static async isUnderMaintenance(monitorID) {
         const maintenance = await R.getRow("SELECT COUNT(*) AS count FROM monitor_maintenance mm JOIN maintenance ON mm.maintenance_id = maintenance.id WHERE mm.monitor_id = ? AND datetime(maintenance.start_date) <= datetime('now') AND datetime(maintenance.end_date) >= datetime('now') LIMIT 1", [ monitorID ]);
         return maintenance.count !== 0;
diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index af305809..a5a87b8c 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -156,7 +156,7 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons
     }
 
 });
-// TODO: make slug aware
+
 // Status Page - Maintenance List
 async function getMaintenanceList() {
     try {
diff --git a/server/server.js b/server/server.js
index 38b0babf..b4060351 100644
--- a/server/server.js
+++ b/server/server.js
@@ -1740,6 +1740,11 @@ async function afterLogin(socket, user) {
     }
 }
 
+/**
+ * Get a list of maintenances for the given user.
+ * @param {string} userID - The ID of the user to get maintenances for.
+ * @returns {Promise<Object>} A promise that resolves to an object with maintenance IDs as keys and maintenances objects as values.
+ */
 async function getMaintenanceJSONList(userID) {
     let result = {};
 

From 31b90d12a4ef07c0bdd45187baf31d8931e25c4b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?= <karel.kryda@gmail.com>
Date: Sat, 30 Apr 2022 17:17:22 +0200
Subject: [PATCH 010/134] Added the ability to choose on which status pages
 maintenance information should be displayed

---
 db/patch-maintenance-table.sql   |   9 +++
 server/routers/api-router.js     |  19 ++---
 server/server.js                 |  58 ++++++++++++++++
 src/languages/en.js              |   5 ++
 src/mixins/socket.js             |   8 +++
 src/pages/EditMaintenance.vue    | 116 +++++++++++++++++++++++++++----
 src/pages/MaintenanceDetails.vue |  24 +++++--
 src/pages/StatusPage.vue         |   6 +-
 8 files changed, 216 insertions(+), 29 deletions(-)

diff --git a/db/patch-maintenance-table.sql b/db/patch-maintenance-table.sql
index ee4a7f88..ce14f766 100644
--- a/db/patch-maintenance-table.sql
+++ b/db/patch-maintenance-table.sql
@@ -20,6 +20,15 @@ CREATE TABLE monitor_maintenance
     CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor (id) ON DELETE CASCADE ON UPDATE CASCADE
 );
 
+CREATE TABLE maintenance_status_page
+(
+    id             INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+    status_page_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_status_page FOREIGN KEY (status_page_id) REFERENCES status_page (id) ON DELETE CASCADE ON UPDATE CASCADE
+);
+
 create index maintenance_user_id on maintenance (user_id);
 
 COMMIT;
diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index a5a87b8c..da036ee2 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -128,7 +128,7 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons
             incident = incident.toPublicJSON();
         }
 
-        let maintenance = await getMaintenanceList();
+        let maintenance = await getMaintenanceList(statusPage.id);
 
         // Public Group List
         const publicGroupList = [];
@@ -158,17 +158,20 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons
 });
 
 // Status Page - Maintenance List
-async function getMaintenanceList() {
+async function getMaintenanceList(statusPageId) {
     try {
         const publicMaintenanceList = [];
 
         let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
-            SELECT maintenance.*
-            FROM maintenance
-            WHERE datetime(maintenance.start_date) <= datetime('now')
-              AND datetime(maintenance.end_date) >= datetime('now')
-            ORDER BY maintenance.end_date
-        `));
+            SELECT m.*
+            FROM maintenance m
+            JOIN maintenance_status_page msp
+            ON msp.maintenance_id = m.id
+            WHERE datetime(m.start_date) <= datetime('now')
+              AND datetime(m.end_date) >= datetime('now')
+              AND msp.status_page_id = ?
+            ORDER BY m.end_date
+        `, [ statusPageId ]));
 
         for (const bean of maintenanceBeanList) {
             publicMaintenanceList.push(await bean.toPublicJSON());
diff --git a/server/server.js b/server/server.js
index b4060351..d45b97a4 100644
--- a/server/server.js
+++ b/server/server.js
@@ -802,6 +802,40 @@ try {
             }
         });
 
+        // Add a new monitor_maintenance
+        socket.on("addMaintenanceStatusPage", async (maintenanceID, statusPages, callback) => {
+            try {
+                checkLogin(socket);
+
+                await R.exec("DELETE FROM maintenance_status_page WHERE maintenance_id = ?", [
+                    maintenanceID
+                ]);
+
+                for await (const statusPage of statusPages) {
+                    let bean = R.dispense("maintenance_status_page");
+
+                    bean.import({
+                        status_page_id: statusPage.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) => {
             try {
                 checkLogin(socket);
@@ -906,6 +940,30 @@ try {
             }
         });
 
+        socket.on("getMaintenanceStatusPage", async (maintenanceID, callback) => {
+            try {
+                checkLogin(socket);
+
+                console.log(`Get Status Pages for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+
+                let statusPages = await R.getAll("SELECT status_page.id, status_page.title FROM maintenance_status_page msp JOIN status_page ON msp.status_page_id = status_page.id WHERE msp.maintenance_id = ? ", [
+                    maintenanceID,
+                ]);
+
+                callback({
+                    ok: true,
+                    statusPages,
+                });
+
+            } catch (e) {
+                console.error(e);
+                callback({
+                    ok: false,
+                    msg: e.message,
+                });
+            }
+        });
+
         socket.on("getMonitorBeats", async (monitorID, period, callback) => {
             try {
                 checkLogin(socket);
diff --git a/src/languages/en.js b/src/languages/en.js
index 8bb1a73c..37f7cb43 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -15,9 +15,14 @@ export default {
     "Pick Affected Monitors...": "Pick Affected Monitors...",
     "Start of maintenance": "Start of maintenance",
     "Expected end of maintenance": "Expected end of maintenance",
+    "Show on all pages": "Show on all status pages",
+    "Selected status pages": "Selected status pages",
+    "Select status pages...": "Select status pages...",
     End: "End",
     affectedMonitorsDescription: "Select monitors that are affected by current maintenance",
     atLeastOneMonitor: "Select at least one affected monitor",
+    selectedStatusPagesDescription: "Select status pages to display maintenance info on",
+    atLeastOneStatusPage: "Select at least one status page",
     maintenanceTitleExample: "Network infrastructure maintenance",
     maintenanceDescriptionExample: "Example: Network infrastructure maintenance is underway which will affect some of our services.",
     passwordNotMatchMsg: "The repeat password does not match.",
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
index 6d4311b1..4fa779bf 100644
--- a/src/mixins/socket.js
+++ b/src/mixins/socket.js
@@ -388,10 +388,18 @@ export default {
             socket.emit("addMonitorMaintenance", maintenanceID, monitors, callback);
         },
 
+        addMaintenanceStatusPage(maintenanceID, statusPages, callback) {
+            socket.emit("addMaintenanceStatusPage", maintenanceID, statusPages, callback);
+        },
+
         getMonitorMaintenance(maintenanceID, callback) {
             socket.emit("getMonitorMaintenance", maintenanceID, callback);
         },
 
+        getMaintenanceStatusPage(maintenanceID, callback) {
+            socket.emit("getMaintenanceStatusPage", maintenanceID, callback);
+        },
+
         deleteMonitor(monitorID, callback) {
             socket.emit("deleteMonitor", monitorID, callback);
         },
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index e8cfeb4f..26aa857f 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -54,22 +54,63 @@
 
                             <!-- Start Date Time -->
                             <div class="my-3">
-                                <label for="start_date" class="form-label">{{ $t("Start of maintenance") }} ({{ $root.timezone }})</label>
+                                <label for="start_date" class="form-label">{{ $t("Start of maintenance") }}
+                                    ({{ $root.timezone }})</label>
                                 <input
                                     id="start_date" v-model="maintenance.start_date" :type="'datetime-local'"
-                                    class="form-control" :class="{'darkCalendar': dark }" required
+                                    class="form-control" :class="{'dark-calendar': dark }" required
                                 >
                             </div>
 
                             <!-- End Date Time -->
                             <div class="my-3">
-                                <label for="end_date" class="form-label">{{ $t("Expected end of maintenance") }} ({{ $root.timezone }})</label>
+                                <label for="end_date" class="form-label">{{ $t("Expected end of maintenance") }}
+                                    ({{ $root.timezone }})</label>
                                 <input
                                     id="end_date" v-model="maintenance.end_date" :type="'datetime-local'"
-                                    class="form-control" :class="{'darkCalendar': dark }" required
+                                    class="form-control" :class="{'dark-calendar': dark }" required
                                 >
                             </div>
 
+                            <!-- Show on all pages -->
+                            <div class="my-3 form-check">
+                                <input
+                                    id="show-on-all-pages" v-model="showOnAllPages" class="form-check-input"
+                                    type="checkbox"
+                                >
+                                <label class="form-check-label" for="show-powered-by">{{
+                                    $t("Show on all pages")
+                                }}</label>
+                            </div>
+
+                            <!-- Status pages to display maintenance info on -->
+                            <div v-if="!showOnAllPages" class="my-3">
+                                <label for="selected_status_pages" class="form-label">{{
+                                    $t("Selected status pages")
+                                }}</label>
+
+                                <VueMultiselect
+                                    id="selected_status_pages"
+                                    v-model="selectedStatusPages"
+                                    :options="selectedStatusPagesOptions"
+                                    track-by="id"
+                                    label="name"
+                                    :multiple="true"
+                                    :allow-empty="false"
+                                    :close-on-select="false"
+                                    :clear-on-select="false"
+                                    :preserve-search="true"
+                                    :placeholder="$t('Select status pages...')"
+                                    :preselect-first="false"
+                                    :max-height="600"
+                                    :taggable="false"
+                                ></VueMultiselect>
+
+                                <div class="form-text">
+                                    {{ $t("selectedStatusPagesDescription") }}
+                                </div>
+                            </div>
+
                             <div class="mt-5 mb-1">
                                 <button
                                     id="monitor-submit-btn" class="btn btn-primary" type="submit"
@@ -104,6 +145,9 @@ export default {
             maintenance: {},
             affectedMonitors: [],
             affectedMonitorsOptions: [],
+            showOnAllPages: true,
+            selectedStatusPages: [],
+            selectedStatusPagesOptions: [],
             dark: (this.$root.theme === "dark"),
         };
     },
@@ -150,10 +194,18 @@ export default {
                 });
             }
         });
+
+        Object.values(this.$root.statusPageList).map(statusPage => {
+            this.selectedStatusPagesOptions.push({
+                id: statusPage.id,
+                name: statusPage.title
+            });
+        });
     },
     methods: {
         init() {
             this.affectedMonitors = [];
+            this.selectedStatusPages = [];
 
             if (this.isAdd) {
                 this.maintenance = {
@@ -178,6 +230,21 @@ export default {
                                 toast.error(res.msg);
                             }
                         });
+
+                        this.$root.getSocket().emit("getMaintenanceStatusPage", this.$route.params.id, (res) => {
+                            if (res.ok) {
+                                Object.values(res.statusPages).map(statusPage => {
+                                    this.selectedStatusPages.push({
+                                        id: statusPage.id,
+                                        name: statusPage.title
+                                    });
+                                });
+
+                                this.showOnAllPages = Object.values(res.statusPages).length === this.selectedStatusPagesOptions.length;
+                            } else {
+                                toast.error(res.msg);
+                            }
+                        });
                     } else {
                         toast.error(res.msg);
                     }
@@ -193,17 +260,24 @@ export default {
                 return this.processing = false;
             }
 
+            if (!this.showOnAllPages && this.selectedStatusPages.length === 0) {
+                toast.error(this.$t("atLeastOneStatusPage"));
+                return this.processing = false;
+            }
+
             this.maintenance.start_date = this.$root.toUTC(this.maintenance.start_date);
             this.maintenance.end_date = this.$root.toUTC(this.maintenance.end_date);
 
             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);
+                        await this.addMonitorMaintenance(res.maintenanceID, async () => {
+                            await this.addMaintenanceStatusPage(res.maintenanceID, () => {
+                                toast.success(res.msg);
+                                this.processing = false;
+                                this.$root.getMaintenanceList();
+                                this.$router.push("/dashboard/maintenance/" + res.maintenanceID);
+                            });
                         });
                     } else {
                         toast.error(res.msg);
@@ -214,10 +288,12 @@ export default {
             } 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();
+                        await this.addMonitorMaintenance(res.maintenanceID, async () => {
+                            await this.addMaintenanceStatusPage(res.maintenanceID, () => {
+                                this.processing = false;
+                                this.$root.toastRes(res);
+                                this.init();
+                            });
                         });
                     } else {
                         this.processing = false;
@@ -238,6 +314,18 @@ export default {
                 callback();
             });
         },
+
+        async addMaintenanceStatusPage(maintenanceID, callback) {
+            await this.$root.addMaintenanceStatusPage(maintenanceID, (this.showOnAllPages) ? this.selectedStatusPagesOptions : this.selectedStatusPages, async (res) => {
+                if (!res.ok) {
+                    toast.error(res.msg);
+                } else {
+                    this.$root.getMaintenanceList();
+                }
+
+                callback();
+            });
+        },
     },
 };
 </script>
@@ -251,7 +339,7 @@ textarea {
     min-height: 200px;
 }
 
-.darkCalendar::-webkit-calendar-picker-indicator {
+.dark-calendar::-webkit-calendar-picker-indicator {
     filter: invert(1);
 }
 </style>
diff --git a/src/pages/MaintenanceDetails.vue b/src/pages/MaintenanceDetails.vue
index 4f9779f6..ae4b5a7b 100644
--- a/src/pages/MaintenanceDetails.vue
+++ b/src/pages/MaintenanceDetails.vue
@@ -8,7 +8,7 @@
                 <span>{{ $t("End") }}: {{ $root.datetimeMaintenance(maintenance.end_date) }}</span>
             </p>
 
-            <div class="functions" style="margin-top: 10px">
+            <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>
@@ -17,14 +17,21 @@
                 </button>
             </div>
 
-            <label for="description" class="form-label" style="margin-top: 20px">{{ $t("Description") }}</label>
+            <label for="description" class="form-label" style="margin-top: 20px;">{{ $t("Description") }}</label>
             <textarea id="description" v-model="maintenance.description" class="form-control" disabled></textarea>
 
-            <label for="affected_monitors" class="form-label" style="margin-top: 20px">{{ $t("Affected Monitors") }}</label>
+            <label for="affected_monitors" class="form-label" style="margin-top: 20px;">{{ $t("Affected Monitors") }}</label>
             <br>
-            <button v-for="monitor in affectedMonitors" :key="monitor.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: bold">
+            <button v-for="monitor in affectedMonitors" :key="monitor.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: bold;">
                 {{ monitor }}
             </button>
+            <br />
+
+            <label for="selected_status_pages" class="form-label" style="margin-top: 20px;">{{ $t("Selected status pages") }}</label>
+            <br>
+            <button v-for="statusPage in selectedStatusPages" :key="statusPage.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: bold;">
+                {{ statusPage }}
+            </button>
 
             <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMaintenance">
                 {{ $t("deleteMaintenanceMsg") }}
@@ -45,6 +52,7 @@ export default {
     data() {
         return {
             affectedMonitors: [],
+            selectedStatusPages: [],
         };
     },
     computed: {
@@ -65,6 +73,14 @@ export default {
                     toast.error(res.msg);
                 }
             });
+
+            this.$root.getSocket().emit("getMaintenanceStatusPage", this.$route.params.id, (res) => {
+                if (res.ok) {
+                    this.selectedStatusPages = Object.values(res.statusPages).map(statusPage => statusPage.title);
+                } else {
+                    toast.error(res.msg);
+                }
+            });
         },
 
         deleteDialog() {
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index 015a57be..4d021dc4 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -247,7 +247,7 @@
                     </div>
 
                     <div v-else-if="isMaintenance">
-                        <font-awesome-icon icon="wrench" class="statusMaintenance" />
+                        <font-awesome-icon icon="wrench" class="status-maintenance" />
                         {{ $t("Maintenance") }}
                     </div>
 
@@ -595,7 +595,7 @@ export default {
             }
 
             this.incident = res.data.incident;
-            this.maintenance = res.data.maintenance;
+            this.maintenance = res.data.maintenance || [];
             this.$root.publicGroupList = res.data.publicGroupList;
         });
 
@@ -956,7 +956,7 @@ footer {
     background-color: #0d1117;
 }
 
-.statusMaintenance {
+.status-maintenance {
     color: $maintenance;
     margin-right: 5px;
 }

From ed218e73bb17d5f3895bf56aa4d994498ebeedb6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?= <karel.kryda@gmail.com>
Date: Sun, 8 May 2022 20:03:24 +0200
Subject: [PATCH 011/134] UI improvements

---
 src/components/MonitorList.vue   |  9 +++++----
 src/icon.js                      |  2 ++
 src/languages/en.js              |  3 +--
 src/pages/Dashboard.vue          | 29 +++++++++++++++++++++++++++--
 src/pages/MaintenanceDetails.vue |  8 ++++++--
 5 files changed, 41 insertions(+), 10 deletions(-)

diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue
index 8662dfee..01f1c0fa 100644
--- a/src/components/MonitorList.vue
+++ b/src/components/MonitorList.vue
@@ -1,10 +1,11 @@
 <template>
     <div class="shadow-box mb-3" :style="boxStyle">
         <div class="list-header">
-            <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>
+            <div class="search-wrapper float-start" style="margin-left: 5px;">
+                <font-awesome-icon icon="filter" />
+                <select v-model="selectedList" class="form-control" style="margin-left: 5px;">
+                    <option value="monitor" selected>{{ $t('Monitors') }}</option>
+                    <option value="maintenance">{{ $t('Maintenance') }}</option>
                 </select>
             </div>
             <div class="search-wrapper">
diff --git a/src/icon.js b/src/icon.js
index c59b2a1c..b2b670c8 100644
--- a/src/icon.js
+++ b/src/icon.js
@@ -43,6 +43,7 @@ import {
     faAngleDown,
     faWrench,
     faHeartbeat,
+    faFilter,
 } from "@fortawesome/free-solid-svg-icons";
 
 library.add(
@@ -85,6 +86,7 @@ library.add(
     faAngleDown,
     faWrench,
     faHeartbeat,
+    faFilter,
 );
 
 export { FontAwesomeIcon };
diff --git a/src/languages/en.js b/src/languages/en.js
index 37f7cb43..b235904e 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -8,8 +8,7 @@ export default {
     maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
     acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
     Maintenance: "Maintenance",
-    "Monitor List": "Monitor List",
-    "Maintenance List": "Maintenance List",
+    "Monitors": "Monitors",
     "Schedule maintenance": "Schedule maintenance",
     "Affected Monitors": "Affected Monitors",
     "Pick Affected Monitors...": "Pick Affected Monitors...",
diff --git a/src/pages/Dashboard.vue b/src/pages/Dashboard.vue
index 207e5b3a..e2a5275f 100644
--- a/src/pages/Dashboard.vue
+++ b/src/pages/Dashboard.vue
@@ -1,7 +1,7 @@
 <template>
     <div class="container-fluid">
         <div class="row">
-            <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 class="dropdown dropdown-create">
                     <button class="btn btn-primary mb-3 dropdown-toggle" type="button" data-bs-toggle="dropdown">
                         <font-awesome-icon icon="plus" /> {{ $t("Create") }}
@@ -14,7 +14,7 @@
                         </li>
                         <li>
                             <button type="button" class="dropdown-item" @click="$router.push('/addMaintenance')">
-                                <font-awesome-icon icon="exclamation-circle" /> {{ $t("Maintenance") }}
+                                <font-awesome-icon icon="wrench" /> {{ $t("Maintenance") }}
                             </button>
                         </li>
                     </ul>
@@ -45,7 +45,32 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "../assets/vars.scss";
+
 .container-fluid {
     width: 98%;
 }
+
+.dropdown-create {
+    display: flex;
+    justify-content: end;
+}
+
+.dark {
+    .dropdown-create {
+        ul {
+            background-color: $dark-bg;
+            border-color: $dark-bg2;
+            border-width: 2px;
+
+            li button {
+                color: $dark-font-color;
+            }
+
+            li button:hover {
+                background-color: $dark-bg2;
+            }
+        }
+    }
+}
 </style>
diff --git a/src/pages/MaintenanceDetails.vue b/src/pages/MaintenanceDetails.vue
index ae4b5a7b..432e7c3f 100644
--- a/src/pages/MaintenanceDetails.vue
+++ b/src/pages/MaintenanceDetails.vue
@@ -22,14 +22,14 @@
 
             <label for="affected_monitors" class="form-label" style="margin-top: 20px;">{{ $t("Affected Monitors") }}</label>
             <br>
-            <button v-for="monitor in affectedMonitors" :key="monitor.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: bold;">
+            <button v-for="monitor in affectedMonitors" :key="monitor.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: 500;">
                 {{ monitor }}
             </button>
             <br />
 
             <label for="selected_status_pages" class="form-label" style="margin-top: 20px;">{{ $t("Selected status pages") }}</label>
             <br>
-            <button v-for="statusPage in selectedStatusPages" :key="statusPage.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: bold;">
+            <button v-for="statusPage in selectedStatusPages" :key="statusPage.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: 500;">
                 {{ statusPage }}
             </button>
 
@@ -154,4 +154,8 @@ textarea {
     background-color: #5cdd8b;
 }
 
+.dark .btn-monitor {
+    color: #020b05 !important;
+}
+
 </style>

From 2fe5c090aa9c3f0ad67f7880dcaef59bf0335aa7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?= <karel.kryda@gmail.com>
Date: Sun, 8 May 2022 20:50:08 +0200
Subject: [PATCH 012/134] small fixes

---
 server/routers/api-router.js  | 2 +-
 src/languages/en.js           | 1 +
 src/pages/EditMaintenance.vue | 5 +++++
 src/pages/StatusPage.vue      | 2 +-
 4 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index da036ee2..84fc8c55 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -180,7 +180,7 @@ async function getMaintenanceList(statusPageId) {
         return publicMaintenanceList;
 
     } catch (error) {
-        return null;
+        return [];
     }
 }
 
diff --git a/src/languages/en.js b/src/languages/en.js
index b235904e..2653b029 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -20,6 +20,7 @@ export default {
     End: "End",
     affectedMonitorsDescription: "Select monitors that are affected by current maintenance",
     atLeastOneMonitor: "Select at least one affected monitor",
+    maintenanceInvalidDate: "Invalid maintenance end date entered",
     selectedStatusPagesDescription: "Select status pages to display maintenance info on",
     atLeastOneStatusPage: "Select at least one status page",
     maintenanceTitleExample: "Network infrastructure maintenance",
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 26aa857f..281f1241 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -260,6 +260,11 @@ export default {
                 return this.processing = false;
             }
 
+            if (this.maintenance.start_date >= this.maintenance.end_date) {
+                toast.error(this.$t("maintenanceInvalidDate"));
+                return this.processing = false;
+            }
+
             if (!this.showOnAllPages && this.selectedStatusPages.length === 0) {
                 toast.error(this.$t("atLeastOneStatusPage"));
                 return this.processing = false;
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index 4d021dc4..85f3803b 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -595,7 +595,7 @@ export default {
             }
 
             this.incident = res.data.incident;
-            this.maintenance = res.data.maintenance || [];
+            this.maintenance = res.data.maintenance;
             this.$root.publicGroupList = res.data.publicGroupList;
         });
 

From 25262cfb91d46b1ff398e05bdd9ec613f07881c9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?=
 <59953812+karelkryda@users.noreply.github.com>
Date: Mon, 30 May 2022 15:31:45 +0200
Subject: [PATCH 013/134] Update server/model/monitor.js

Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 server/model/monitor.js | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/server/model/monitor.js b/server/model/monitor.js
index cce9060d..4f483415 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -796,6 +796,13 @@ class Monitor extends BeanModel {
             (previousBeatStatus === PENDING && currentBeatStatus === DOWN);
     }
 
+    /**
+     * Is this beat important for notifications?
+     * @param {boolean} isFirstBeat Is this the first beat of this monitor?
+     * @param {const} previousBeatStatus Status of the previous beat
+     * @param {const} currentBeatStatus Status of the current beat
+     * @returns {boolean} True if is an important beat else false
+     */
     static isImportantForNotification(isFirstBeat, previousBeatStatus, currentBeatStatus) {
         // * ? -> ANY STATUS = important [isFirstBeat]
         // UP -> PENDING = not important

From 6d0683b055c8cc114347459aa5f15bab0fbc2448 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?=
 <59953812+karelkryda@users.noreply.github.com>
Date: Mon, 30 May 2022 15:32:19 +0200
Subject: [PATCH 014/134] Update server/routers/api-router.js

Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 server/routers/api-router.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index 7304d128..f298a84a 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -160,7 +160,11 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons
 
 });
 
-// Status Page - Maintenance List
+/**
+ * Get list of maintenances
+ * @param {number} statusPageId ID of status page to get maintenance for
+ * @returns {Object} Object representing maintenances sanitized for public
+ */
 async function getMaintenanceList(statusPageId) {
     try {
         const publicMaintenanceList = [];

From fa777c5bc0d1aa3ae9aa8e7abe5bee0fb184e06e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Karel=20Kr=C3=BDda?=
 <59953812+karelkryda@users.noreply.github.com>
Date: Mon, 30 May 2022 15:32:42 +0200
Subject: [PATCH 015/134] Update server/server.js

Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 server/server.js | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/server/server.js b/server/server.js
index d18ef12f..5c9b6503 100644
--- a/server/server.js
+++ b/server/server.js
@@ -1759,6 +1759,11 @@ async function checkOwner(userID, monitorID) {
     }
 }
 
+/**
+ * Send maintenance list to client
+ * @param {Socket} socket Socket.io instance to send to
+ * @returns {Object}
+ */
 async function sendMaintenanceList(socket) {
     let list = await getMaintenanceJSONList(socket.userID);
     io.to(socket.userID).emit("maintenanceList", list);

From 42e30de2099f95f3076d65f78c5e517ff56da952 Mon Sep 17 00:00:00 2001
From: Thomas Christlieb <thomaschristlieb@hotmail.com>
Date: Mon, 4 Jul 2022 10:16:33 +0200
Subject: [PATCH 016/134] change page title to " - Login" when on Login Form

---
 src/components/Login.vue | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/components/Login.vue b/src/components/Login.vue
index 3a821881..0a0266e7 100644
--- a/src/components/Login.vue
+++ b/src/components/Login.vue
@@ -54,6 +54,15 @@ export default {
             tokenRequired: false,
         };
     },
+
+    mounted() {
+        document.title += " - Login";
+    },
+
+    unmounted() {
+        document.title = document.title.replace(" - Login", "");
+    },
+
     methods: {
         /** Submit the user details and attempt to log in */
         submit() {

From c4a2ce4e78cf90fc371a62c394f9523b5d62c102 Mon Sep 17 00:00:00 2001
From: Rolf Bachmann <1196109+rolfbachmann@users.noreply.github.com>
Date: Tue, 19 Jul 2022 10:57:52 +0200
Subject: [PATCH 017/134] Add authentication support for ntfy

---
 server/notification-providers/ntfy.js | 11 +++++++++--
 src/components/notifications/Ntfy.vue | 18 +++++++++++++++++-
 2 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/server/notification-providers/ntfy.js b/server/notification-providers/ntfy.js
index 21f358f6..17d6d812 100644
--- a/server/notification-providers/ntfy.js
+++ b/server/notification-providers/ntfy.js
@@ -8,12 +8,19 @@ class Ntfy extends NotificationProvider {
     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
         let okMsg = "Sent Successfully.";
         try {
-            await axios.post(`${notification.ntfyserverurl}`, {
+            let headers = {};
+            if (notification.ntfyusername.length > 0) {
+                headers = {
+                    "Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"),
+                };
+            }
+            let data = {
                 "topic": notification.ntfytopic,
                 "message": msg,
                 "priority": notification.ntfyPriority || 4,
                 "title": "Uptime-Kuma",
-            });
+            };
+            await axios.post(`${notification.ntfyserverurl}`, data, { headers: headers });
 
             return okMsg;
 
diff --git a/src/components/notifications/Ntfy.vue b/src/components/notifications/Ntfy.vue
index d9a83b49..a42dca30 100644
--- a/src/components/notifications/Ntfy.vue
+++ b/src/components/notifications/Ntfy.vue
@@ -11,7 +11,18 @@
             <input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required>
         </div>
     </div>
-
+    <div class="mb-3">
+        <label for="ntfy-username" class="form-label">{{ $t("Username") }}</label>
+        <div class="input-group mb-3">
+            <input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control" required>
+        </div>
+    </div>
+    <div class="mb-3">
+        <label for="ntfy-password" class="form-label">{{ $t("Password") }}</label>
+        <div class="input-group mb-3">
+            <HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword"></HiddenInput>
+        </div>
+    </div>
     <div class="mb-3">
         <label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
         <input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
@@ -19,7 +30,12 @@
 </template>
 
 <script>
+import HiddenInput from "../HiddenInput.vue";
+
 export default {
+    components: {
+        HiddenInput,
+    },
     mounted() {
         if (typeof this.$parent.notification.ntfyPriority === "undefined") {
             this.$parent.notification.ntfyserverurl = "https://ntfy.sh";

From 3ea57600ba1b80aefb9b05594d38f5cbf22eaa58 Mon Sep 17 00:00:00 2001
From: Mambo <kdevkr@gmail.com>
Date: Sat, 10 Sep 2022 23:16:05 +0900
Subject: [PATCH 018/134] chore: fix typo

Modifying Korean Spelling
---
 src/languages/ko-KR.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/languages/ko-KR.js b/src/languages/ko-KR.js
index f181389b..f614f518 100644
--- a/src/languages/ko-KR.js
+++ b/src/languages/ko-KR.js
@@ -69,7 +69,7 @@ export default {
     Port: "포트",
     "Heartbeat Interval": "하트비트 주기",
     Retries: "재시도",
-    "Heartbeat Retry Interval": "하트비드 재시도 주기",
+    "Heartbeat Retry Interval": "하트비트 재시도 주기",
     Advanced: "고급",
     "Upside Down Mode": "상태 반전 모드",
     "Max. Redirects": "최대 리다이렉트",
@@ -110,7 +110,7 @@ export default {
     "Remember me": "비밀번호 기억하기",
     Login: "로그인",
     "No Monitors, please": "모니터링이 현재 없어요,",
-    "add one": "한번 추가해보실레요?",
+    "add one": "한번 추가해보실래요?",
     "Notification Type": "알림 종류",
     Email: "이메일",
     Test: "테스트",

From 7577477ae815df0cb928d739481db32c495ead26 Mon Sep 17 00:00:00 2001
From: d3vyce <nicolas.sudres@hotmail.fr>
Date: Sat, 10 Sep 2022 21:35:22 +0200
Subject: [PATCH 019/134] Add rel="noopener noreferrer" to html link

---
 src/components/PublicGroupList.vue | 1 +
 src/pages/Details.vue              | 2 +-
 src/pages/StatusPage.vue           | 2 +-
 3 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/components/PublicGroupList.vue b/src/components/PublicGroupList.vue
index e8ed57d8..1eb433ee 100644
--- a/src/components/PublicGroupList.vue
+++ b/src/components/PublicGroupList.vue
@@ -44,6 +44,7 @@
                                                 :href="monitor.element.url"
                                                 class="item-name"
                                                 target="_blank"
+                                                rel="noopener noreferrer"
                                             >
                                                 {{ monitor.element.name }}
                                             </a>
diff --git a/src/pages/Details.vue b/src/pages/Details.vue
index 0ff0fe77..7cf25892 100644
--- a/src/pages/Details.vue
+++ b/src/pages/Details.vue
@@ -6,7 +6,7 @@
                 <Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" />
             </div>
             <p class="url">
-                <a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
+                <a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ monitor.url }}</a>
                 <span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
                 <span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
                 <span v-if="monitor.type === 'keyword'">
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index 485b0942..45d5e0c2 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -265,7 +265,7 @@
                 <Editable v-model="config.footerText" tag="div" :contenteditable="enableEditMode" :noNL="false" class="alert-heading p-2" />
 
                 <p v-if="config.showPoweredBy">
-                    {{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
+                    {{ $t("Powered by") }} <a target="_blank" rel="noopener noreferrer" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
                 </p>
             </footer>
         </div>

From f503488618a47d8179da144534cc65241569776f Mon Sep 17 00:00:00 2001
From: rezzorix <72935517+rezzorix@users.noreply.github.com>
Date: Sun, 11 Sep 2022 14:54:33 +0800
Subject: [PATCH 020/134] Create stale-bot.yml

---
 .github/workflows/stale-bot.yml | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)
 create mode 100644 .github/workflows/stale-bot.yml

diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml
new file mode 100644
index 00000000..34a42d60
--- /dev/null
+++ b/.github/workflows/stale-bot.yml
@@ -0,0 +1,22 @@
+name: 'Automatically close stale issues and PRs'
+on:
+  schedule:
+    - cron: '0 0 * * *'
+#Run once a day at midnight 
+
+jobs:
+  stale:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/stale@v5
+        with:
+          stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
+          stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
+          close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'
+          close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.'
+          days-before-stale: 90
+          days-before-close: 7
+          exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request'
+          exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,feature-request'
+          exempt-issue-assignees: 'louislam'
+          exempt-pr-assignees: 'louislam'

From 104d5216330749c8a9ea892309423f1930a61331 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Mon, 12 Sep 2022 18:33:46 +0800
Subject: [PATCH 021/134] Update vite from 2.9.9 to 3.1.0

---
 config/vite.config.js |    3 +
 package-lock.json     | 2262 ++++++++++++++++++++++++++++++++++++-----
 package.json          |    6 +-
 3 files changed, 2000 insertions(+), 271 deletions(-)

diff --git a/config/vite.config.js b/config/vite.config.js
index 84de961d..6e9ebbde 100644
--- a/config/vite.config.js
+++ b/config/vite.config.js
@@ -11,6 +11,9 @@ const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i;
 
 // https://vitejs.dev/config/
 export default defineConfig({
+    server: {
+        port: 3000,
+    },
     define: {
         "FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
     },
diff --git a/package-lock.json b/package-lock.json
index b4310950..7c32fd43 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "uptime-kuma",
-    "version": "1.18.0-beta.0",
+    "version": "1.18.0",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "uptime-kuma",
-            "version": "1.18.0-beta.0",
+            "version": "1.18.0",
             "license": "MIT",
             "dependencies": {
                 "@louislam/sqlite3": "~15.0.6",
@@ -65,8 +65,8 @@
                 "@fortawesome/vue-fontawesome": "~3.0.0-5",
                 "@popperjs/core": "~2.10.2",
                 "@types/bootstrap": "~5.1.9",
-                "@vitejs/plugin-legacy": "~1.8.2",
-                "@vitejs/plugin-vue": "~2.3.3",
+                "@vitejs/plugin-legacy": "~2.1.0",
+                "@vitejs/plugin-vue": "~3.1.0",
                 "@vue/compiler-sfc": "~3.2.36",
                 "aedes": "^0.46.3",
                 "babel-plugin-rewire": "~1.2.0",
@@ -76,6 +76,8 @@
                 "concurrently": "^7.1.0",
                 "core-js": "~3.18.3",
                 "cross-env": "~7.0.3",
+                "cypress": "^10.1.0",
+                "delay": "^5.0.0",
                 "dns2": "~2.0.1",
                 "eslint": "~8.14.0",
                 "eslint-plugin-vue": "~8.7.1",
@@ -95,7 +97,7 @@
                 "timezones-list": "~3.0.1",
                 "typescript": "~4.4.4",
                 "v-pagination-3": "~0.1.7",
-                "vite": "~2.9.9",
+                "vite": "~3.1.0",
                 "vite-plugin-compression": "^0.5.1",
                 "vue": "next",
                 "vue-chart-3": "3.0.9",
@@ -2040,10 +2042,133 @@
                 "node": ">= 10"
             }
         },
+        "node_modules/@colors/colors": {
+            "version": "1.5.0",
+            "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
+            "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
+            "dev": true,
+            "optional": true,
+            "engines": {
+                "node": ">=0.1.90"
+            }
+        },
+        "node_modules/@cypress/request": {
+            "version": "2.88.10",
+            "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz",
+            "integrity": "sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==",
+            "dev": true,
+            "dependencies": {
+                "aws-sign2": "~0.7.0",
+                "aws4": "^1.8.0",
+                "caseless": "~0.12.0",
+                "combined-stream": "~1.0.6",
+                "extend": "~3.0.2",
+                "forever-agent": "~0.6.1",
+                "form-data": "~2.3.2",
+                "http-signature": "~1.3.6",
+                "is-typedarray": "~1.0.0",
+                "isstream": "~0.1.2",
+                "json-stringify-safe": "~5.0.1",
+                "mime-types": "~2.1.19",
+                "performance-now": "^2.1.0",
+                "qs": "~6.5.2",
+                "safe-buffer": "^5.1.2",
+                "tough-cookie": "~2.5.0",
+                "tunnel-agent": "^0.6.0",
+                "uuid": "^8.3.2"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/@cypress/request/node_modules/form-data": {
+            "version": "2.3.3",
+            "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+            "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+            "dev": true,
+            "dependencies": {
+                "asynckit": "^0.4.0",
+                "combined-stream": "^1.0.6",
+                "mime-types": "^2.1.12"
+            },
+            "engines": {
+                "node": ">= 0.12"
+            }
+        },
+        "node_modules/@cypress/request/node_modules/http-signature": {
+            "version": "1.3.6",
+            "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
+            "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==",
+            "dev": true,
+            "dependencies": {
+                "assert-plus": "^1.0.0",
+                "jsprim": "^2.0.2",
+                "sshpk": "^1.14.1"
+            },
+            "engines": {
+                "node": ">=0.10"
+            }
+        },
+        "node_modules/@cypress/request/node_modules/jsprim": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
+            "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==",
+            "dev": true,
+            "engines": [
+                "node >=0.6.0"
+            ],
+            "dependencies": {
+                "assert-plus": "1.0.0",
+                "extsprintf": "1.3.0",
+                "json-schema": "0.4.0",
+                "verror": "1.10.0"
+            }
+        },
+        "node_modules/@cypress/request/node_modules/qs": {
+            "version": "6.5.3",
+            "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
+            "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.6"
+            }
+        },
+        "node_modules/@cypress/request/node_modules/tough-cookie": {
+            "version": "2.5.0",
+            "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+            "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+            "dev": true,
+            "dependencies": {
+                "psl": "^1.1.28",
+                "punycode": "^2.1.1"
+            },
+            "engines": {
+                "node": ">=0.8"
+            }
+        },
+        "node_modules/@cypress/xvfb": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
+            "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==",
+            "dev": true,
+            "dependencies": {
+                "debug": "^3.1.0",
+                "lodash.once": "^4.1.1"
+            }
+        },
+        "node_modules/@cypress/xvfb/node_modules/debug": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+            "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+            "dev": true,
+            "dependencies": {
+                "ms": "^2.1.1"
+            }
+        },
         "node_modules/@esbuild/linux-loong64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz",
-            "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.7.tgz",
+            "integrity": "sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==",
             "cpu": [
                 "loong64"
             ],
@@ -3526,6 +3651,18 @@
                 "@types/node": "*"
             }
         },
+        "node_modules/@types/sinonjs__fake-timers": {
+            "version": "8.1.1",
+            "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz",
+            "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==",
+            "dev": true
+        },
+        "node_modules/@types/sizzle": {
+            "version": "2.3.3",
+            "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
+            "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
+            "dev": true
+        },
         "node_modules/@types/stack-utils": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
@@ -3558,22 +3695,23 @@
             }
         },
         "node_modules/@vitejs/plugin-legacy": {
-            "version": "1.8.2",
-            "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-1.8.2.tgz",
-            "integrity": "sha512-NCOKU+pU+cxLMR9P9RTolEuOK+h+zYBXlknj+zGcKSj/NXBZYgA1GAH1FnO4zijoWRiTaiOm2ha9LQrELE7XHg==",
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-2.1.0.tgz",
+            "integrity": "sha512-en3h0L7okBonSYKJx81bU8AVFPjSCiUSz8xUDAW8J0CxskfxSt/VJKbZO6G9yCVgZLywGoO8PNAfOQWVLUWZ2A==",
             "dev": true,
             "dependencies": {
-                "@babel/standalone": "^7.17.11",
-                "core-js": "^3.22.3",
-                "magic-string": "^0.26.1",
+                "@babel/standalone": "^7.18.13",
+                "core-js": "^3.25.0",
+                "magic-string": "^0.26.2",
                 "regenerator-runtime": "^0.13.9",
-                "systemjs": "^6.12.1"
+                "systemjs": "^6.12.4"
             },
             "engines": {
-                "node": ">=12.0.0"
+                "node": "^14.18.0 || >=16.0.0"
             },
             "peerDependencies": {
-                "vite": "^2.8.0"
+                "terser": "^5.4.0",
+                "vite": "^3.0.0"
             }
         },
         "node_modules/@vitejs/plugin-legacy/node_modules/core-js": {
@@ -3588,15 +3726,15 @@
             }
         },
         "node_modules/@vitejs/plugin-vue": {
-            "version": "2.3.4",
-            "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.3.4.tgz",
-            "integrity": "sha512-IfFNbtkbIm36O9KB8QodlwwYvTEsJb4Lll4c2IwB3VHc2gie2mSPtSzL0eYay7X2jd/2WX02FjSGTWR6OPr/zg==",
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-3.1.0.tgz",
+            "integrity": "sha512-fmxtHPjSOEIRg6vHYDaem+97iwCUg/uSIaTzp98lhELt2ISOQuDo2hbkBdXod0g15IhfPMQmAxh4heUks2zvDA==",
             "dev": true,
             "engines": {
-                "node": ">=12.0.0"
+                "node": "^14.18.0 || >=16.0.0"
             },
             "peerDependencies": {
-                "vite": "^2.5.10",
+                "vite": "^3.0.0",
                 "vue": "^3.2.25"
             }
         },
@@ -3940,6 +4078,19 @@
                 "node": ">= 6.0.0"
             }
         },
+        "node_modules/aggregate-error": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+            "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+            "dev": true,
+            "dependencies": {
+                "clean-stack": "^2.0.0",
+                "indent-string": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/ajv": {
             "version": "6.12.6",
             "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -3964,6 +4115,15 @@
                 "char-width-table-consumer": "^1.0.0"
             }
         },
+        "node_modules/ansi-colors": {
+            "version": "4.1.3",
+            "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+            "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+            "dev": true,
+            "engines": {
+                "node": ">=6"
+            }
+        },
         "node_modules/ansi-escapes": {
             "version": "4.3.2",
             "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -4017,6 +4177,26 @@
             "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
             "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
         },
+        "node_modules/arch": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
+            "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ]
+        },
         "node_modules/are-we-there-yet": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
@@ -4076,7 +4256,7 @@
             "version": "0.2.6",
             "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
             "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
-            "optional": true,
+            "devOptional": true,
             "dependencies": {
                 "safer-buffer": "~2.1.0"
             }
@@ -4085,7 +4265,7 @@
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
             "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
-            "optional": true,
+            "devOptional": true,
             "engines": {
                 "node": ">=0.8"
             }
@@ -4099,11 +4279,26 @@
                 "node": ">=8"
             }
         },
+        "node_modules/async": {
+            "version": "3.2.4",
+            "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+            "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
+            "dev": true
+        },
         "node_modules/asynckit": {
             "version": "0.4.0",
             "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
             "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
         },
+        "node_modules/at-least-node": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+            "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+            "dev": true,
+            "engines": {
+                "node": ">= 4.0.0"
+            }
+        },
         "node_modules/await-lock": {
             "version": "2.2.2",
             "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz",
@@ -4113,7 +4308,7 @@
             "version": "0.7.0",
             "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
             "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
-            "optional": true,
+            "devOptional": true,
             "engines": {
                 "node": "*"
             }
@@ -4122,7 +4317,7 @@
             "version": "1.11.0",
             "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
             "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
-            "optional": true
+            "devOptional": true
         },
         "node_modules/axios": {
             "version": "0.27.2",
@@ -4464,7 +4659,7 @@
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
             "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
-            "optional": true,
+            "devOptional": true,
             "dependencies": {
                 "tweetnacl": "^0.14.3"
             }
@@ -4509,6 +4704,18 @@
                 "readable-stream": "^3.4.0"
             }
         },
+        "node_modules/blob-util": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz",
+            "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==",
+            "dev": true
+        },
+        "node_modules/bluebird": {
+            "version": "3.7.2",
+            "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+            "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+            "dev": true
+        },
         "node_modules/body-parser": {
             "version": "1.19.2",
             "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz",
@@ -4745,6 +4952,15 @@
                 "node": ">=10.6.0"
             }
         },
+        "node_modules/cachedir": {
+            "version": "2.3.0",
+            "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
+            "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==",
+            "dev": true,
+            "engines": {
+                "node": ">=6"
+            }
+        },
         "node_modules/call-bind": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -4812,7 +5028,7 @@
             "version": "0.12.0",
             "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
             "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
-            "optional": true
+            "devOptional": true
         },
         "node_modules/chalk": {
             "version": "2.4.2",
@@ -4866,6 +5082,15 @@
                 "dayjs": "^1.8.15"
             }
         },
+        "node_modules/check-more-types": {
+            "version": "2.24.0",
+            "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
+            "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==",
+            "dev": true,
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
         "node_modules/check-password-strength": {
             "version": "2.0.7",
             "resolved": "https://registry.npmjs.org/check-password-strength/-/check-password-strength-2.0.7.tgz",
@@ -4971,6 +5196,105 @@
             "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==",
             "dev": true
         },
+        "node_modules/clean-stack": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+            "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+            "dev": true,
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/cli-cursor": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+            "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+            "dev": true,
+            "dependencies": {
+                "restore-cursor": "^3.1.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/cli-table3": {
+            "version": "0.6.2",
+            "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz",
+            "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==",
+            "dev": true,
+            "dependencies": {
+                "string-width": "^4.2.0"
+            },
+            "engines": {
+                "node": "10.* || >= 12.*"
+            },
+            "optionalDependencies": {
+                "@colors/colors": "1.5.0"
+            }
+        },
+        "node_modules/cli-truncate": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+            "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+            "dev": true,
+            "dependencies": {
+                "slice-ansi": "^3.0.0",
+                "string-width": "^4.2.0"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/cli-truncate/node_modules/ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "dev": true,
+            "dependencies": {
+                "color-convert": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/cli-truncate/node_modules/color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "dev": true,
+            "dependencies": {
+                "color-name": "~1.1.4"
+            },
+            "engines": {
+                "node": ">=7.0.0"
+            }
+        },
+        "node_modules/cli-truncate/node_modules/color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+            "dev": true
+        },
+        "node_modules/cli-truncate/node_modules/slice-ansi": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+            "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+            "dev": true,
+            "dependencies": {
+                "ansi-styles": "^4.0.0",
+                "astral-regex": "^2.0.0",
+                "is-fullwidth-code-point": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/cliui": {
             "version": "7.0.4",
             "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@@ -5132,6 +5456,15 @@
                 "node": ">=0.10.0"
             }
         },
+        "node_modules/common-tags": {
+            "version": "1.8.2",
+            "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
+            "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
+            "dev": true,
+            "engines": {
+                "node": ">=4.0.0"
+            }
+        },
         "node_modules/compare-versions": {
             "version": "3.6.0",
             "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
@@ -5604,11 +5937,251 @@
                 "node": ">=0.8"
             }
         },
+        "node_modules/cypress": {
+            "version": "10.7.0",
+            "resolved": "https://registry.npmjs.org/cypress/-/cypress-10.7.0.tgz",
+            "integrity": "sha512-gTFvjrUoBnqPPOu9Vl5SBHuFlzx/Wxg/ZXIz2H4lzoOLFelKeF7mbwYUOzgzgF0oieU2WhJAestQdkgwJMMTvQ==",
+            "dev": true,
+            "hasInstallScript": true,
+            "dependencies": {
+                "@cypress/request": "^2.88.10",
+                "@cypress/xvfb": "^1.2.4",
+                "@types/node": "^14.14.31",
+                "@types/sinonjs__fake-timers": "8.1.1",
+                "@types/sizzle": "^2.3.2",
+                "arch": "^2.2.0",
+                "blob-util": "^2.0.2",
+                "bluebird": "^3.7.2",
+                "buffer": "^5.6.0",
+                "cachedir": "^2.3.0",
+                "chalk": "^4.1.0",
+                "check-more-types": "^2.24.0",
+                "cli-cursor": "^3.1.0",
+                "cli-table3": "~0.6.1",
+                "commander": "^5.1.0",
+                "common-tags": "^1.8.0",
+                "dayjs": "^1.10.4",
+                "debug": "^4.3.2",
+                "enquirer": "^2.3.6",
+                "eventemitter2": "^6.4.3",
+                "execa": "4.1.0",
+                "executable": "^4.1.1",
+                "extract-zip": "2.0.1",
+                "figures": "^3.2.0",
+                "fs-extra": "^9.1.0",
+                "getos": "^3.2.1",
+                "is-ci": "^3.0.0",
+                "is-installed-globally": "~0.4.0",
+                "lazy-ass": "^1.6.0",
+                "listr2": "^3.8.3",
+                "lodash": "^4.17.21",
+                "log-symbols": "^4.0.0",
+                "minimist": "^1.2.6",
+                "ospath": "^1.2.2",
+                "pretty-bytes": "^5.6.0",
+                "proxy-from-env": "1.0.0",
+                "request-progress": "^3.0.0",
+                "semver": "^7.3.2",
+                "supports-color": "^8.1.1",
+                "tmp": "~0.2.1",
+                "untildify": "^4.0.0",
+                "yauzl": "^2.10.0"
+            },
+            "bin": {
+                "cypress": "bin/cypress"
+            },
+            "engines": {
+                "node": ">=12.0.0"
+            }
+        },
+        "node_modules/cypress/node_modules/@types/node": {
+            "version": "14.18.28",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.28.tgz",
+            "integrity": "sha512-CK2fnrQlIgKlCV3N2kM+Gznb5USlwA1KFX3rJVHmgVk6NJxFPuQ86pAcvKnu37IA4BGlSRz7sEE1lHL1aLZ/eQ==",
+            "dev": true
+        },
+        "node_modules/cypress/node_modules/ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "dev": true,
+            "dependencies": {
+                "color-convert": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/cypress/node_modules/chalk": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+            "dev": true,
+            "dependencies": {
+                "ansi-styles": "^4.1.0",
+                "supports-color": "^7.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
+            }
+        },
+        "node_modules/cypress/node_modules/chalk/node_modules/supports-color": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+            "dev": true,
+            "dependencies": {
+                "has-flag": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/cypress/node_modules/color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "dev": true,
+            "dependencies": {
+                "color-name": "~1.1.4"
+            },
+            "engines": {
+                "node": ">=7.0.0"
+            }
+        },
+        "node_modules/cypress/node_modules/color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+            "dev": true
+        },
+        "node_modules/cypress/node_modules/execa": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
+            "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
+            "dev": true,
+            "dependencies": {
+                "cross-spawn": "^7.0.0",
+                "get-stream": "^5.0.0",
+                "human-signals": "^1.1.1",
+                "is-stream": "^2.0.0",
+                "merge-stream": "^2.0.0",
+                "npm-run-path": "^4.0.0",
+                "onetime": "^5.1.0",
+                "signal-exit": "^3.0.2",
+                "strip-final-newline": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sindresorhus/execa?sponsor=1"
+            }
+        },
+        "node_modules/cypress/node_modules/fs-extra": {
+            "version": "9.1.0",
+            "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+            "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+            "dev": true,
+            "dependencies": {
+                "at-least-node": "^1.0.0",
+                "graceful-fs": "^4.2.0",
+                "jsonfile": "^6.0.1",
+                "universalify": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/cypress/node_modules/get-stream": {
+            "version": "5.2.0",
+            "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+            "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+            "dev": true,
+            "dependencies": {
+                "pump": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/cypress/node_modules/has-flag": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/cypress/node_modules/human-signals": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
+            "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
+            "dev": true,
+            "engines": {
+                "node": ">=8.12.0"
+            }
+        },
+        "node_modules/cypress/node_modules/proxy-from-env": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
+            "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==",
+            "dev": true
+        },
+        "node_modules/cypress/node_modules/semver": {
+            "version": "7.3.7",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+            "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+            "dev": true,
+            "dependencies": {
+                "lru-cache": "^6.0.0"
+            },
+            "bin": {
+                "semver": "bin/semver.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/cypress/node_modules/supports-color": {
+            "version": "8.1.1",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+            "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+            "dev": true,
+            "dependencies": {
+                "has-flag": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/supports-color?sponsor=1"
+            }
+        },
+        "node_modules/cypress/node_modules/universalify": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+            "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+            "dev": true,
+            "engines": {
+                "node": ">= 10.0.0"
+            }
+        },
         "node_modules/dashdash": {
             "version": "1.14.1",
             "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
             "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
-            "optional": true,
+            "devOptional": true,
             "dependencies": {
                 "assert-plus": "^1.0.0"
             },
@@ -5750,6 +6323,18 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/delay": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
+            "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
         "node_modules/delayed-stream": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -5942,7 +6527,7 @@
             "version": "0.1.2",
             "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
             "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
-            "optional": true,
+            "devOptional": true,
             "dependencies": {
                 "jsbn": "~0.1.0",
                 "safer-buffer": "^2.1.0"
@@ -6090,6 +6675,18 @@
                 }
             }
         },
+        "node_modules/enquirer": {
+            "version": "2.3.6",
+            "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+            "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+            "dev": true,
+            "dependencies": {
+                "ansi-colors": "^4.1.1"
+            },
+            "engines": {
+                "node": ">=8.6"
+            }
+        },
         "node_modules/entities": {
             "version": "4.4.0",
             "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
@@ -6192,9 +6789,9 @@
             }
         },
         "node_modules/esbuild": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz",
-            "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.7.tgz",
+            "integrity": "sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw==",
             "dev": true,
             "hasInstallScript": true,
             "bin": {
@@ -6204,33 +6801,33 @@
                 "node": ">=12"
             },
             "optionalDependencies": {
-                "@esbuild/linux-loong64": "0.14.54",
-                "esbuild-android-64": "0.14.54",
-                "esbuild-android-arm64": "0.14.54",
-                "esbuild-darwin-64": "0.14.54",
-                "esbuild-darwin-arm64": "0.14.54",
-                "esbuild-freebsd-64": "0.14.54",
-                "esbuild-freebsd-arm64": "0.14.54",
-                "esbuild-linux-32": "0.14.54",
-                "esbuild-linux-64": "0.14.54",
-                "esbuild-linux-arm": "0.14.54",
-                "esbuild-linux-arm64": "0.14.54",
-                "esbuild-linux-mips64le": "0.14.54",
-                "esbuild-linux-ppc64le": "0.14.54",
-                "esbuild-linux-riscv64": "0.14.54",
-                "esbuild-linux-s390x": "0.14.54",
-                "esbuild-netbsd-64": "0.14.54",
-                "esbuild-openbsd-64": "0.14.54",
-                "esbuild-sunos-64": "0.14.54",
-                "esbuild-windows-32": "0.14.54",
-                "esbuild-windows-64": "0.14.54",
-                "esbuild-windows-arm64": "0.14.54"
+                "@esbuild/linux-loong64": "0.15.7",
+                "esbuild-android-64": "0.15.7",
+                "esbuild-android-arm64": "0.15.7",
+                "esbuild-darwin-64": "0.15.7",
+                "esbuild-darwin-arm64": "0.15.7",
+                "esbuild-freebsd-64": "0.15.7",
+                "esbuild-freebsd-arm64": "0.15.7",
+                "esbuild-linux-32": "0.15.7",
+                "esbuild-linux-64": "0.15.7",
+                "esbuild-linux-arm": "0.15.7",
+                "esbuild-linux-arm64": "0.15.7",
+                "esbuild-linux-mips64le": "0.15.7",
+                "esbuild-linux-ppc64le": "0.15.7",
+                "esbuild-linux-riscv64": "0.15.7",
+                "esbuild-linux-s390x": "0.15.7",
+                "esbuild-netbsd-64": "0.15.7",
+                "esbuild-openbsd-64": "0.15.7",
+                "esbuild-sunos-64": "0.15.7",
+                "esbuild-windows-32": "0.15.7",
+                "esbuild-windows-64": "0.15.7",
+                "esbuild-windows-arm64": "0.15.7"
             }
         },
         "node_modules/esbuild-android-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz",
-            "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.7.tgz",
+            "integrity": "sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w==",
             "cpu": [
                 "x64"
             ],
@@ -6244,9 +6841,9 @@
             }
         },
         "node_modules/esbuild-android-arm64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz",
-            "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.7.tgz",
+            "integrity": "sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ==",
             "cpu": [
                 "arm64"
             ],
@@ -6260,9 +6857,9 @@
             }
         },
         "node_modules/esbuild-darwin-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz",
-            "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.7.tgz",
+            "integrity": "sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg==",
             "cpu": [
                 "x64"
             ],
@@ -6276,9 +6873,9 @@
             }
         },
         "node_modules/esbuild-darwin-arm64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz",
-            "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.7.tgz",
+            "integrity": "sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ==",
             "cpu": [
                 "arm64"
             ],
@@ -6292,9 +6889,9 @@
             }
         },
         "node_modules/esbuild-freebsd-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz",
-            "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.7.tgz",
+            "integrity": "sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ==",
             "cpu": [
                 "x64"
             ],
@@ -6308,9 +6905,9 @@
             }
         },
         "node_modules/esbuild-freebsd-arm64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz",
-            "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.7.tgz",
+            "integrity": "sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q==",
             "cpu": [
                 "arm64"
             ],
@@ -6324,9 +6921,9 @@
             }
         },
         "node_modules/esbuild-linux-32": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz",
-            "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.7.tgz",
+            "integrity": "sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg==",
             "cpu": [
                 "ia32"
             ],
@@ -6340,9 +6937,9 @@
             }
         },
         "node_modules/esbuild-linux-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz",
-            "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.7.tgz",
+            "integrity": "sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ==",
             "cpu": [
                 "x64"
             ],
@@ -6356,9 +6953,9 @@
             }
         },
         "node_modules/esbuild-linux-arm": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz",
-            "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.7.tgz",
+            "integrity": "sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ==",
             "cpu": [
                 "arm"
             ],
@@ -6372,9 +6969,9 @@
             }
         },
         "node_modules/esbuild-linux-arm64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz",
-            "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.7.tgz",
+            "integrity": "sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw==",
             "cpu": [
                 "arm64"
             ],
@@ -6388,9 +6985,9 @@
             }
         },
         "node_modules/esbuild-linux-mips64le": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz",
-            "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.7.tgz",
+            "integrity": "sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw==",
             "cpu": [
                 "mips64el"
             ],
@@ -6404,9 +7001,9 @@
             }
         },
         "node_modules/esbuild-linux-ppc64le": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz",
-            "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.7.tgz",
+            "integrity": "sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw==",
             "cpu": [
                 "ppc64"
             ],
@@ -6420,9 +7017,9 @@
             }
         },
         "node_modules/esbuild-linux-riscv64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz",
-            "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.7.tgz",
+            "integrity": "sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g==",
             "cpu": [
                 "riscv64"
             ],
@@ -6436,9 +7033,9 @@
             }
         },
         "node_modules/esbuild-linux-s390x": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz",
-            "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.7.tgz",
+            "integrity": "sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ==",
             "cpu": [
                 "s390x"
             ],
@@ -6452,9 +7049,9 @@
             }
         },
         "node_modules/esbuild-netbsd-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz",
-            "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.7.tgz",
+            "integrity": "sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ==",
             "cpu": [
                 "x64"
             ],
@@ -6468,9 +7065,9 @@
             }
         },
         "node_modules/esbuild-openbsd-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz",
-            "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.7.tgz",
+            "integrity": "sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ==",
             "cpu": [
                 "x64"
             ],
@@ -6484,9 +7081,9 @@
             }
         },
         "node_modules/esbuild-sunos-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz",
-            "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.7.tgz",
+            "integrity": "sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag==",
             "cpu": [
                 "x64"
             ],
@@ -6500,9 +7097,9 @@
             }
         },
         "node_modules/esbuild-windows-32": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz",
-            "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.7.tgz",
+            "integrity": "sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA==",
             "cpu": [
                 "ia32"
             ],
@@ -6516,9 +7113,9 @@
             }
         },
         "node_modules/esbuild-windows-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz",
-            "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.7.tgz",
+            "integrity": "sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q==",
             "cpu": [
                 "x64"
             ],
@@ -6532,9 +7129,9 @@
             }
         },
         "node_modules/esbuild-windows-arm64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz",
-            "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.7.tgz",
+            "integrity": "sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw==",
             "cpu": [
                 "arm64"
             ],
@@ -7039,6 +7636,12 @@
                 "node": ">= 0.6"
             }
         },
+        "node_modules/eventemitter2": {
+            "version": "6.4.8",
+            "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.8.tgz",
+            "integrity": "sha512-pAJurPyD+Nj/pfz8m0usKF1RW0E9gfY4Dfdem2l6jZbqcZlK8SP93qUMCv9V9FgOn+GSZEW6qeaglpf/vQ9D5A==",
+            "dev": true
+        },
         "node_modules/events": {
             "version": "3.3.0",
             "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -7082,6 +7685,18 @@
                 "node": ">=8"
             }
         },
+        "node_modules/executable": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
+            "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==",
+            "dev": true,
+            "dependencies": {
+                "pify": "^2.2.0"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
         "node_modules/exit": {
             "version": "0.1.2",
             "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
@@ -7216,7 +7831,7 @@
             "version": "3.0.2",
             "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
             "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
-            "optional": true
+            "devOptional": true
         },
         "node_modules/extract-zip": {
             "version": "2.0.1",
@@ -7257,10 +7872,10 @@
             "version": "1.3.0",
             "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
             "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
+            "devOptional": true,
             "engines": [
                 "node >=0.6.0"
-            ],
-            "optional": true
+            ]
         },
         "node_modules/fast-deep-equal": {
             "version": "3.1.3",
@@ -7378,6 +7993,21 @@
                 "pend": "~1.2.0"
             }
         },
+        "node_modules/figures": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+            "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+            "dev": true,
+            "dependencies": {
+                "escape-string-regexp": "^1.0.5"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
         "node_modules/file-entry-cache": {
             "version": "6.0.1",
             "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -7617,7 +8247,7 @@
             "version": "0.6.1",
             "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
             "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
-            "optional": true,
+            "devOptional": true,
             "engines": {
                 "node": "*"
             }
@@ -7892,11 +8522,20 @@
             "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.2.5.tgz",
             "integrity": "sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA=="
         },
+        "node_modules/getos": {
+            "version": "3.2.1",
+            "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz",
+            "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==",
+            "dev": true,
+            "dependencies": {
+                "async": "^3.2.0"
+            }
+        },
         "node_modules/getpass": {
             "version": "0.1.7",
             "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
             "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
-            "optional": true,
+            "devOptional": true,
             "dependencies": {
                 "assert-plus": "^1.0.0"
             }
@@ -7932,6 +8571,30 @@
                 "node": ">=10.13.0"
             }
         },
+        "node_modules/global-dirs": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz",
+            "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==",
+            "dev": true,
+            "dependencies": {
+                "ini": "2.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/global-dirs/node_modules/ini": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
+            "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            }
+        },
         "node_modules/global-modules": {
             "version": "0.2.3",
             "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz",
@@ -8529,6 +9192,18 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/is-ci": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
+            "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
+            "dev": true,
+            "dependencies": {
+                "ci-info": "^3.2.0"
+            },
+            "bin": {
+                "is-ci": "bin.js"
+            }
+        },
         "node_modules/is-core-module": {
             "version": "2.10.0",
             "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz",
@@ -8615,6 +9290,22 @@
                 "node": ">=0.10.0"
             }
         },
+        "node_modules/is-installed-globally": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
+            "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
+            "dev": true,
+            "dependencies": {
+                "global-dirs": "^3.0.0",
+                "is-path-inside": "^3.0.2"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
         "node_modules/is-invalid-path": {
             "version": "0.1.0",
             "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz",
@@ -8679,6 +9370,15 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/is-path-inside": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+            "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/is-plain-obj": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
@@ -8800,6 +9500,18 @@
             "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
             "devOptional": true
         },
+        "node_modules/is-unicode-supported": {
+            "version": "0.1.0",
+            "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+            "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
         "node_modules/is-valid-path": {
             "version": "0.1.1",
             "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz",
@@ -8878,7 +9590,7 @@
             "version": "0.1.2",
             "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
             "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
-            "optional": true
+            "devOptional": true
         },
         "node_modules/istanbul-lib-coverage": {
             "version": "3.2.0",
@@ -10901,7 +11613,7 @@
             "version": "0.1.1",
             "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
             "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
-            "optional": true
+            "devOptional": true
         },
         "node_modules/jsdom": {
             "version": "16.7.0",
@@ -11014,7 +11726,7 @@
             "version": "0.4.0",
             "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
             "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
-            "optional": true
+            "devOptional": true
         },
         "node_modules/json-schema-traverse": {
             "version": "0.4.1",
@@ -11032,7 +11744,7 @@
             "version": "5.0.1",
             "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
             "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
-            "optional": true
+            "devOptional": true
         },
         "node_modules/json5": {
             "version": "2.2.1",
@@ -11250,6 +11962,15 @@
             "integrity": "sha512-RTSoaUAfLvpR357vWzAz/50Q/BmHfmE6ETSWfutT0AJiw10e6CmcdYRQJlLRd95B53D0Y2aD1jSxD3V3ySF+PA==",
             "dev": true
         },
+        "node_modules/lazy-ass": {
+            "version": "1.6.0",
+            "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
+            "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==",
+            "dev": true,
+            "engines": {
+                "node": "> 0.8"
+            }
+        },
         "node_modules/lazy-cache": {
             "version": "1.0.4",
             "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
@@ -11295,6 +12016,33 @@
             "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
             "dev": true
         },
+        "node_modules/listr2": {
+            "version": "3.14.0",
+            "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
+            "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==",
+            "dev": true,
+            "dependencies": {
+                "cli-truncate": "^2.1.0",
+                "colorette": "^2.0.16",
+                "log-update": "^4.0.0",
+                "p-map": "^4.0.0",
+                "rfdc": "^1.3.0",
+                "rxjs": "^7.5.1",
+                "through": "^2.3.8",
+                "wrap-ansi": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "peerDependencies": {
+                "enquirer": ">= 2.3.0 < 3"
+            },
+            "peerDependenciesMeta": {
+                "enquirer": {
+                    "optional": true
+                }
+            }
+        },
         "node_modules/locate-path": {
             "version": "5.0.0",
             "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -11424,6 +12172,157 @@
                 "lodash._baseuniq": "~4.6.0"
             }
         },
+        "node_modules/log-symbols": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+            "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+            "dev": true,
+            "dependencies": {
+                "chalk": "^4.1.0",
+                "is-unicode-supported": "^0.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/log-symbols/node_modules/ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "dev": true,
+            "dependencies": {
+                "color-convert": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/log-symbols/node_modules/chalk": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+            "dev": true,
+            "dependencies": {
+                "ansi-styles": "^4.1.0",
+                "supports-color": "^7.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
+            }
+        },
+        "node_modules/log-symbols/node_modules/color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "dev": true,
+            "dependencies": {
+                "color-name": "~1.1.4"
+            },
+            "engines": {
+                "node": ">=7.0.0"
+            }
+        },
+        "node_modules/log-symbols/node_modules/color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+            "dev": true
+        },
+        "node_modules/log-symbols/node_modules/has-flag": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/log-symbols/node_modules/supports-color": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+            "dev": true,
+            "dependencies": {
+                "has-flag": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/log-update": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
+            "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
+            "dev": true,
+            "dependencies": {
+                "ansi-escapes": "^4.3.0",
+                "cli-cursor": "^3.1.0",
+                "slice-ansi": "^4.0.0",
+                "wrap-ansi": "^6.2.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/log-update/node_modules/ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "dev": true,
+            "dependencies": {
+                "color-convert": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/log-update/node_modules/color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "dev": true,
+            "dependencies": {
+                "color-name": "~1.1.4"
+            },
+            "engines": {
+                "node": ">=7.0.0"
+            }
+        },
+        "node_modules/log-update/node_modules/color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+            "dev": true
+        },
+        "node_modules/log-update/node_modules/wrap-ansi": {
+            "version": "6.2.0",
+            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+            "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+            "dev": true,
+            "dependencies": {
+                "ansi-styles": "^4.0.0",
+                "string-width": "^4.1.0",
+                "strip-ansi": "^6.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/lru-cache": {
             "version": "6.0.0",
             "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -12391,6 +13290,12 @@
                 "node": ">=0.10.0"
             }
         },
+        "node_modules/ospath": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
+            "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==",
+            "dev": true
+        },
         "node_modules/p-finally": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
@@ -12426,6 +13331,21 @@
                 "node": ">=8"
             }
         },
+        "node_modules/p-map": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+            "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+            "dev": true,
+            "dependencies": {
+                "aggregate-error": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
         "node_modules/p-timeout": {
             "version": "3.2.0",
             "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
@@ -12611,7 +13531,7 @@
             "version": "2.1.0",
             "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
             "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
-            "optional": true
+            "devOptional": true
         },
         "node_modules/pg": {
             "version": "8.8.0",
@@ -12713,6 +13633,15 @@
                 "url": "https://github.com/sponsors/jonschlinkert"
             }
         },
+        "node_modules/pify": {
+            "version": "2.3.0",
+            "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+            "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
         "node_modules/pirates": {
             "version": "4.0.5",
             "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
@@ -12918,6 +13847,18 @@
                 "node": ">= 0.8.0"
             }
         },
+        "node_modules/pretty-bytes": {
+            "version": "5.6.0",
+            "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+            "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
+            "dev": true,
+            "engines": {
+                "node": ">=6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
         "node_modules/pretty-format": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
@@ -13654,6 +14595,15 @@
                 "node": ">= 6"
             }
         },
+        "node_modules/request-progress": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
+            "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==",
+            "dev": true,
+            "dependencies": {
+                "throttleit": "^1.0.0"
+            }
+        },
         "node_modules/request/node_modules/form-data": {
             "version": "2.3.3",
             "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
@@ -13798,6 +14748,19 @@
                 "node": ">=10"
             }
         },
+        "node_modules/restore-cursor": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+            "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+            "dev": true,
+            "dependencies": {
+                "onetime": "^5.1.0",
+                "signal-exit": "^3.0.2"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/retimer": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/retimer/-/retimer-3.0.0.tgz",
@@ -13834,9 +14797,9 @@
             }
         },
         "node_modules/rollup": {
-            "version": "2.77.3",
-            "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz",
-            "integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==",
+            "version": "2.78.1",
+            "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.78.1.tgz",
+            "integrity": "sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==",
             "dev": true,
             "bin": {
                 "rollup": "dist/bin/rollup"
@@ -14454,7 +15417,7 @@
             "version": "1.17.0",
             "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
             "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==",
-            "optional": true,
+            "devOptional": true,
             "dependencies": {
                 "asn1": "~0.2.3",
                 "assert-plus": "^1.0.0",
@@ -15101,6 +16064,12 @@
             "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==",
             "dev": true
         },
+        "node_modules/throttleit": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
+            "integrity": "sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==",
+            "dev": true
+        },
         "node_modules/through": {
             "version": "2.3.8",
             "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -15121,6 +16090,18 @@
             "integrity": "sha512-yfOzyuVwzgD0LkldD3Epkr+JUdUIxEUL147Fa6ZgG/23KU28iOv3e3M7vQOCFMPyopAhDX7dqOLWttIP7tkTKg==",
             "dev": true
         },
+        "node_modules/tmp": {
+            "version": "0.2.1",
+            "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
+            "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
+            "dev": true,
+            "dependencies": {
+                "rimraf": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=8.17.0"
+            }
+        },
         "node_modules/tmpl": {
             "version": "1.0.5",
             "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -15233,7 +16214,7 @@
             "version": "0.6.0",
             "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
             "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
-            "optional": true,
+            "devOptional": true,
             "dependencies": {
                 "safe-buffer": "^5.0.1"
             },
@@ -15245,7 +16226,7 @@
             "version": "0.14.5",
             "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
             "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
-            "optional": true
+            "devOptional": true
         },
         "node_modules/type-check": {
             "version": "0.4.0",
@@ -15406,6 +16387,15 @@
                 "node": ">= 0.8"
             }
         },
+        "node_modules/untildify": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+            "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/update-browserslist-db": {
             "version": "1.0.5",
             "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz",
@@ -15540,10 +16530,10 @@
             "version": "1.10.0",
             "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
             "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
+            "devOptional": true,
             "engines": [
                 "node >=0.6.0"
             ],
-            "optional": true,
             "dependencies": {
                 "assert-plus": "^1.0.0",
                 "core-util-is": "1.0.2",
@@ -15551,21 +16541,21 @@
             }
         },
         "node_modules/vite": {
-            "version": "2.9.15",
-            "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.15.tgz",
-            "integrity": "sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==",
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/vite/-/vite-3.1.0.tgz",
+            "integrity": "sha512-YBg3dUicDpDWFCGttmvMbVyS9ydjntwEjwXRj2KBFwSB8SxmGcudo1yb8FW5+M/G86aS8x828ujnzUVdsLjs9g==",
             "dev": true,
             "dependencies": {
-                "esbuild": "^0.14.27",
-                "postcss": "^8.4.13",
-                "resolve": "^1.22.0",
-                "rollup": ">=2.59.0 <2.78.0"
+                "esbuild": "^0.15.6",
+                "postcss": "^8.4.16",
+                "resolve": "^1.22.1",
+                "rollup": "~2.78.0"
             },
             "bin": {
                 "vite": "bin/vite.js"
             },
             "engines": {
-                "node": ">=12.2.0"
+                "node": "^14.18.0 || >=16.0.0"
             },
             "optionalDependencies": {
                 "fsevents": "~2.3.2"
@@ -15573,7 +16563,8 @@
             "peerDependencies": {
                 "less": "*",
                 "sass": "*",
-                "stylus": "*"
+                "stylus": "*",
+                "terser": "^5.4.0"
             },
             "peerDependenciesMeta": {
                 "less": {
@@ -15584,6 +16575,9 @@
                 },
                 "stylus": {
                     "optional": true
+                },
+                "terser": {
+                    "optional": true
                 }
             }
         },
@@ -17779,10 +18773,116 @@
             "resolved": "https://registry.npmjs.org/@breejs/later/-/later-4.1.0.tgz",
             "integrity": "sha512-QgGnZ9b7o4k0Ai1ZbTJWwZpZcFK9d+Gb+DyNt4UT9x6IEIs5HVu0iIlmgzGqN+t9MoJSpSPo9S/Mm51UtHr3JA=="
         },
+        "@colors/colors": {
+            "version": "1.5.0",
+            "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
+            "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
+            "dev": true,
+            "optional": true
+        },
+        "@cypress/request": {
+            "version": "2.88.10",
+            "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz",
+            "integrity": "sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==",
+            "dev": true,
+            "requires": {
+                "aws-sign2": "~0.7.0",
+                "aws4": "^1.8.0",
+                "caseless": "~0.12.0",
+                "combined-stream": "~1.0.6",
+                "extend": "~3.0.2",
+                "forever-agent": "~0.6.1",
+                "form-data": "~2.3.2",
+                "http-signature": "~1.3.6",
+                "is-typedarray": "~1.0.0",
+                "isstream": "~0.1.2",
+                "json-stringify-safe": "~5.0.1",
+                "mime-types": "~2.1.19",
+                "performance-now": "^2.1.0",
+                "qs": "~6.5.2",
+                "safe-buffer": "^5.1.2",
+                "tough-cookie": "~2.5.0",
+                "tunnel-agent": "^0.6.0",
+                "uuid": "^8.3.2"
+            },
+            "dependencies": {
+                "form-data": {
+                    "version": "2.3.3",
+                    "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+                    "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+                    "dev": true,
+                    "requires": {
+                        "asynckit": "^0.4.0",
+                        "combined-stream": "^1.0.6",
+                        "mime-types": "^2.1.12"
+                    }
+                },
+                "http-signature": {
+                    "version": "1.3.6",
+                    "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
+                    "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==",
+                    "dev": true,
+                    "requires": {
+                        "assert-plus": "^1.0.0",
+                        "jsprim": "^2.0.2",
+                        "sshpk": "^1.14.1"
+                    }
+                },
+                "jsprim": {
+                    "version": "2.0.2",
+                    "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
+                    "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==",
+                    "dev": true,
+                    "requires": {
+                        "assert-plus": "1.0.0",
+                        "extsprintf": "1.3.0",
+                        "json-schema": "0.4.0",
+                        "verror": "1.10.0"
+                    }
+                },
+                "qs": {
+                    "version": "6.5.3",
+                    "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
+                    "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
+                    "dev": true
+                },
+                "tough-cookie": {
+                    "version": "2.5.0",
+                    "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+                    "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+                    "dev": true,
+                    "requires": {
+                        "psl": "^1.1.28",
+                        "punycode": "^2.1.1"
+                    }
+                }
+            }
+        },
+        "@cypress/xvfb": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
+            "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==",
+            "dev": true,
+            "requires": {
+                "debug": "^3.1.0",
+                "lodash.once": "^4.1.1"
+            },
+            "dependencies": {
+                "debug": {
+                    "version": "3.2.7",
+                    "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+                    "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+                    "dev": true,
+                    "requires": {
+                        "ms": "^2.1.1"
+                    }
+                }
+            }
+        },
         "@esbuild/linux-loong64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz",
-            "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.7.tgz",
+            "integrity": "sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==",
             "dev": true,
             "optional": true
         },
@@ -18993,6 +20093,18 @@
                 "@types/node": "*"
             }
         },
+        "@types/sinonjs__fake-timers": {
+            "version": "8.1.1",
+            "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz",
+            "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==",
+            "dev": true
+        },
+        "@types/sizzle": {
+            "version": "2.3.3",
+            "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
+            "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
+            "dev": true
+        },
         "@types/stack-utils": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
@@ -19025,16 +20137,16 @@
             }
         },
         "@vitejs/plugin-legacy": {
-            "version": "1.8.2",
-            "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-1.8.2.tgz",
-            "integrity": "sha512-NCOKU+pU+cxLMR9P9RTolEuOK+h+zYBXlknj+zGcKSj/NXBZYgA1GAH1FnO4zijoWRiTaiOm2ha9LQrELE7XHg==",
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-2.1.0.tgz",
+            "integrity": "sha512-en3h0L7okBonSYKJx81bU8AVFPjSCiUSz8xUDAW8J0CxskfxSt/VJKbZO6G9yCVgZLywGoO8PNAfOQWVLUWZ2A==",
             "dev": true,
             "requires": {
-                "@babel/standalone": "^7.17.11",
-                "core-js": "^3.22.3",
-                "magic-string": "^0.26.1",
+                "@babel/standalone": "^7.18.13",
+                "core-js": "^3.25.0",
+                "magic-string": "^0.26.2",
                 "regenerator-runtime": "^0.13.9",
-                "systemjs": "^6.12.1"
+                "systemjs": "^6.12.4"
             },
             "dependencies": {
                 "core-js": {
@@ -19046,9 +20158,9 @@
             }
         },
         "@vitejs/plugin-vue": {
-            "version": "2.3.4",
-            "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.3.4.tgz",
-            "integrity": "sha512-IfFNbtkbIm36O9KB8QodlwwYvTEsJb4Lll4c2IwB3VHc2gie2mSPtSzL0eYay7X2jd/2WX02FjSGTWR6OPr/zg==",
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-3.1.0.tgz",
+            "integrity": "sha512-fmxtHPjSOEIRg6vHYDaem+97iwCUg/uSIaTzp98lhELt2ISOQuDo2hbkBdXod0g15IhfPMQmAxh4heUks2zvDA==",
             "dev": true
         },
         "@vue/compiler-core": {
@@ -19371,6 +20483,16 @@
                 "debug": "4"
             }
         },
+        "aggregate-error": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+            "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+            "dev": true,
+            "requires": {
+                "clean-stack": "^2.0.0",
+                "indent-string": "^4.0.0"
+            }
+        },
         "ajv": {
             "version": "6.12.6",
             "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -19391,6 +20513,12 @@
                 "char-width-table-consumer": "^1.0.0"
             }
         },
+        "ansi-colors": {
+            "version": "4.1.3",
+            "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+            "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+            "dev": true
+        },
         "ansi-escapes": {
             "version": "4.3.2",
             "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -19429,6 +20557,12 @@
             "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
             "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
         },
+        "arch": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
+            "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
+            "dev": true
+        },
         "are-we-there-yet": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
@@ -19476,7 +20610,7 @@
             "version": "0.2.6",
             "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
             "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
-            "optional": true,
+            "devOptional": true,
             "requires": {
                 "safer-buffer": "~2.1.0"
             }
@@ -19485,7 +20619,7 @@
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
             "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
-            "optional": true
+            "devOptional": true
         },
         "astral-regex": {
             "version": "2.0.0",
@@ -19493,11 +20627,23 @@
             "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
             "dev": true
         },
+        "async": {
+            "version": "3.2.4",
+            "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+            "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
+            "dev": true
+        },
         "asynckit": {
             "version": "0.4.0",
             "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
             "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
         },
+        "at-least-node": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+            "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+            "dev": true
+        },
         "await-lock": {
             "version": "2.2.2",
             "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz",
@@ -19507,13 +20653,13 @@
             "version": "0.7.0",
             "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
             "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
-            "optional": true
+            "devOptional": true
         },
         "aws4": {
             "version": "1.11.0",
             "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
             "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
-            "optional": true
+            "devOptional": true
         },
         "axios": {
             "version": "0.27.2",
@@ -19781,7 +20927,7 @@
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
             "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
-            "optional": true,
+            "devOptional": true,
             "requires": {
                 "tweetnacl": "^0.14.3"
             }
@@ -19823,6 +20969,18 @@
                 "readable-stream": "^3.4.0"
             }
         },
+        "blob-util": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz",
+            "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==",
+            "dev": true
+        },
+        "bluebird": {
+            "version": "3.7.2",
+            "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+            "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+            "dev": true
+        },
         "body-parser": {
             "version": "1.19.2",
             "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz",
@@ -19997,6 +21155,12 @@
             "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz",
             "integrity": "sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A=="
         },
+        "cachedir": {
+            "version": "2.3.0",
+            "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
+            "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==",
+            "dev": true
+        },
         "call-bind": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -20039,7 +21203,7 @@
             "version": "0.12.0",
             "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
             "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
-            "optional": true
+            "devOptional": true
         },
         "chalk": {
             "version": "2.4.2",
@@ -20083,6 +21247,12 @@
             "integrity": "sha512-EnbVqTJGFKLpg1TROLdCEufrzbmIa2oeLGx8O2Wdjw2EoMudoOo9+YFu+6CM0Z0hQ/v3yq/e/Y6efQMu22n8Jg==",
             "dev": true
         },
+        "check-more-types": {
+            "version": "2.24.0",
+            "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
+            "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==",
+            "dev": true
+        },
         "check-password-strength": {
             "version": "2.0.7",
             "resolved": "https://registry.npmjs.org/check-password-strength/-/check-password-strength-2.0.7.tgz",
@@ -20164,6 +21334,78 @@
             "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==",
             "dev": true
         },
+        "clean-stack": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+            "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+            "dev": true
+        },
+        "cli-cursor": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+            "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+            "dev": true,
+            "requires": {
+                "restore-cursor": "^3.1.0"
+            }
+        },
+        "cli-table3": {
+            "version": "0.6.2",
+            "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz",
+            "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==",
+            "dev": true,
+            "requires": {
+                "@colors/colors": "1.5.0",
+                "string-width": "^4.2.0"
+            }
+        },
+        "cli-truncate": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+            "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+            "dev": true,
+            "requires": {
+                "slice-ansi": "^3.0.0",
+                "string-width": "^4.2.0"
+            },
+            "dependencies": {
+                "ansi-styles": {
+                    "version": "4.3.0",
+                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+                    "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+                    "dev": true,
+                    "requires": {
+                        "color-convert": "^2.0.1"
+                    }
+                },
+                "color-convert": {
+                    "version": "2.0.1",
+                    "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+                    "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+                    "dev": true,
+                    "requires": {
+                        "color-name": "~1.1.4"
+                    }
+                },
+                "color-name": {
+                    "version": "1.1.4",
+                    "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+                    "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+                    "dev": true
+                },
+                "slice-ansi": {
+                    "version": "3.0.0",
+                    "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+                    "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+                    "dev": true,
+                    "requires": {
+                        "ansi-styles": "^4.0.0",
+                        "astral-regex": "^2.0.0",
+                        "is-fullwidth-code-point": "^3.0.0"
+                    }
+                }
+            }
+        },
         "cliui": {
             "version": "7.0.4",
             "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@@ -20301,6 +21543,12 @@
                 }
             }
         },
+        "common-tags": {
+            "version": "1.8.2",
+            "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
+            "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
+            "dev": true
+        },
         "compare-versions": {
             "version": "3.6.0",
             "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
@@ -20667,11 +21915,194 @@
                 "fs-exists-sync": "^0.1.0"
             }
         },
+        "cypress": {
+            "version": "10.7.0",
+            "resolved": "https://registry.npmjs.org/cypress/-/cypress-10.7.0.tgz",
+            "integrity": "sha512-gTFvjrUoBnqPPOu9Vl5SBHuFlzx/Wxg/ZXIz2H4lzoOLFelKeF7mbwYUOzgzgF0oieU2WhJAestQdkgwJMMTvQ==",
+            "dev": true,
+            "requires": {
+                "@cypress/request": "^2.88.10",
+                "@cypress/xvfb": "^1.2.4",
+                "@types/node": "^14.14.31",
+                "@types/sinonjs__fake-timers": "8.1.1",
+                "@types/sizzle": "^2.3.2",
+                "arch": "^2.2.0",
+                "blob-util": "^2.0.2",
+                "bluebird": "^3.7.2",
+                "buffer": "^5.6.0",
+                "cachedir": "^2.3.0",
+                "chalk": "^4.1.0",
+                "check-more-types": "^2.24.0",
+                "cli-cursor": "^3.1.0",
+                "cli-table3": "~0.6.1",
+                "commander": "^5.1.0",
+                "common-tags": "^1.8.0",
+                "dayjs": "^1.10.4",
+                "debug": "^4.3.2",
+                "enquirer": "^2.3.6",
+                "eventemitter2": "^6.4.3",
+                "execa": "4.1.0",
+                "executable": "^4.1.1",
+                "extract-zip": "2.0.1",
+                "figures": "^3.2.0",
+                "fs-extra": "^9.1.0",
+                "getos": "^3.2.1",
+                "is-ci": "^3.0.0",
+                "is-installed-globally": "~0.4.0",
+                "lazy-ass": "^1.6.0",
+                "listr2": "^3.8.3",
+                "lodash": "^4.17.21",
+                "log-symbols": "^4.0.0",
+                "minimist": "^1.2.6",
+                "ospath": "^1.2.2",
+                "pretty-bytes": "^5.6.0",
+                "proxy-from-env": "1.0.0",
+                "request-progress": "^3.0.0",
+                "semver": "^7.3.2",
+                "supports-color": "^8.1.1",
+                "tmp": "~0.2.1",
+                "untildify": "^4.0.0",
+                "yauzl": "^2.10.0"
+            },
+            "dependencies": {
+                "@types/node": {
+                    "version": "14.18.28",
+                    "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.28.tgz",
+                    "integrity": "sha512-CK2fnrQlIgKlCV3N2kM+Gznb5USlwA1KFX3rJVHmgVk6NJxFPuQ86pAcvKnu37IA4BGlSRz7sEE1lHL1aLZ/eQ==",
+                    "dev": true
+                },
+                "ansi-styles": {
+                    "version": "4.3.0",
+                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+                    "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+                    "dev": true,
+                    "requires": {
+                        "color-convert": "^2.0.1"
+                    }
+                },
+                "chalk": {
+                    "version": "4.1.2",
+                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+                    "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+                    "dev": true,
+                    "requires": {
+                        "ansi-styles": "^4.1.0",
+                        "supports-color": "^7.1.0"
+                    },
+                    "dependencies": {
+                        "supports-color": {
+                            "version": "7.2.0",
+                            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+                            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+                            "dev": true,
+                            "requires": {
+                                "has-flag": "^4.0.0"
+                            }
+                        }
+                    }
+                },
+                "color-convert": {
+                    "version": "2.0.1",
+                    "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+                    "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+                    "dev": true,
+                    "requires": {
+                        "color-name": "~1.1.4"
+                    }
+                },
+                "color-name": {
+                    "version": "1.1.4",
+                    "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+                    "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+                    "dev": true
+                },
+                "execa": {
+                    "version": "4.1.0",
+                    "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
+                    "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
+                    "dev": true,
+                    "requires": {
+                        "cross-spawn": "^7.0.0",
+                        "get-stream": "^5.0.0",
+                        "human-signals": "^1.1.1",
+                        "is-stream": "^2.0.0",
+                        "merge-stream": "^2.0.0",
+                        "npm-run-path": "^4.0.0",
+                        "onetime": "^5.1.0",
+                        "signal-exit": "^3.0.2",
+                        "strip-final-newline": "^2.0.0"
+                    }
+                },
+                "fs-extra": {
+                    "version": "9.1.0",
+                    "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+                    "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+                    "dev": true,
+                    "requires": {
+                        "at-least-node": "^1.0.0",
+                        "graceful-fs": "^4.2.0",
+                        "jsonfile": "^6.0.1",
+                        "universalify": "^2.0.0"
+                    }
+                },
+                "get-stream": {
+                    "version": "5.2.0",
+                    "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+                    "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+                    "dev": true,
+                    "requires": {
+                        "pump": "^3.0.0"
+                    }
+                },
+                "has-flag": {
+                    "version": "4.0.0",
+                    "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+                    "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+                    "dev": true
+                },
+                "human-signals": {
+                    "version": "1.1.1",
+                    "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
+                    "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
+                    "dev": true
+                },
+                "proxy-from-env": {
+                    "version": "1.0.0",
+                    "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
+                    "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==",
+                    "dev": true
+                },
+                "semver": {
+                    "version": "7.3.7",
+                    "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+                    "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+                    "dev": true,
+                    "requires": {
+                        "lru-cache": "^6.0.0"
+                    }
+                },
+                "supports-color": {
+                    "version": "8.1.1",
+                    "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+                    "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+                    "dev": true,
+                    "requires": {
+                        "has-flag": "^4.0.0"
+                    }
+                },
+                "universalify": {
+                    "version": "2.0.0",
+                    "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+                    "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+                    "dev": true
+                }
+            }
+        },
         "dashdash": {
             "version": "1.14.1",
             "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
             "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
-            "optional": true,
+            "devOptional": true,
             "requires": {
                 "assert-plus": "^1.0.0"
             }
@@ -20775,6 +22206,12 @@
                 "object-keys": "^1.1.1"
             }
         },
+        "delay": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
+            "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==",
+            "dev": true
+        },
         "delayed-stream": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -20924,7 +22361,7 @@
             "version": "0.1.2",
             "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
             "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
-            "optional": true,
+            "devOptional": true,
             "requires": {
                 "jsbn": "~0.1.0",
                 "safer-buffer": "^2.1.0"
@@ -21031,6 +22468,15 @@
             "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
             "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg=="
         },
+        "enquirer": {
+            "version": "2.3.6",
+            "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+            "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+            "dev": true,
+            "requires": {
+                "ansi-colors": "^4.1.1"
+            }
+        },
         "entities": {
             "version": "4.4.0",
             "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
@@ -21106,171 +22552,171 @@
             }
         },
         "esbuild": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz",
-            "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.7.tgz",
+            "integrity": "sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw==",
             "dev": true,
             "requires": {
-                "@esbuild/linux-loong64": "0.14.54",
-                "esbuild-android-64": "0.14.54",
-                "esbuild-android-arm64": "0.14.54",
-                "esbuild-darwin-64": "0.14.54",
-                "esbuild-darwin-arm64": "0.14.54",
-                "esbuild-freebsd-64": "0.14.54",
-                "esbuild-freebsd-arm64": "0.14.54",
-                "esbuild-linux-32": "0.14.54",
-                "esbuild-linux-64": "0.14.54",
-                "esbuild-linux-arm": "0.14.54",
-                "esbuild-linux-arm64": "0.14.54",
-                "esbuild-linux-mips64le": "0.14.54",
-                "esbuild-linux-ppc64le": "0.14.54",
-                "esbuild-linux-riscv64": "0.14.54",
-                "esbuild-linux-s390x": "0.14.54",
-                "esbuild-netbsd-64": "0.14.54",
-                "esbuild-openbsd-64": "0.14.54",
-                "esbuild-sunos-64": "0.14.54",
-                "esbuild-windows-32": "0.14.54",
-                "esbuild-windows-64": "0.14.54",
-                "esbuild-windows-arm64": "0.14.54"
+                "@esbuild/linux-loong64": "0.15.7",
+                "esbuild-android-64": "0.15.7",
+                "esbuild-android-arm64": "0.15.7",
+                "esbuild-darwin-64": "0.15.7",
+                "esbuild-darwin-arm64": "0.15.7",
+                "esbuild-freebsd-64": "0.15.7",
+                "esbuild-freebsd-arm64": "0.15.7",
+                "esbuild-linux-32": "0.15.7",
+                "esbuild-linux-64": "0.15.7",
+                "esbuild-linux-arm": "0.15.7",
+                "esbuild-linux-arm64": "0.15.7",
+                "esbuild-linux-mips64le": "0.15.7",
+                "esbuild-linux-ppc64le": "0.15.7",
+                "esbuild-linux-riscv64": "0.15.7",
+                "esbuild-linux-s390x": "0.15.7",
+                "esbuild-netbsd-64": "0.15.7",
+                "esbuild-openbsd-64": "0.15.7",
+                "esbuild-sunos-64": "0.15.7",
+                "esbuild-windows-32": "0.15.7",
+                "esbuild-windows-64": "0.15.7",
+                "esbuild-windows-arm64": "0.15.7"
             }
         },
         "esbuild-android-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz",
-            "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.7.tgz",
+            "integrity": "sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w==",
             "dev": true,
             "optional": true
         },
         "esbuild-android-arm64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz",
-            "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.7.tgz",
+            "integrity": "sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ==",
             "dev": true,
             "optional": true
         },
         "esbuild-darwin-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz",
-            "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.7.tgz",
+            "integrity": "sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg==",
             "dev": true,
             "optional": true
         },
         "esbuild-darwin-arm64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz",
-            "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.7.tgz",
+            "integrity": "sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ==",
             "dev": true,
             "optional": true
         },
         "esbuild-freebsd-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz",
-            "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.7.tgz",
+            "integrity": "sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ==",
             "dev": true,
             "optional": true
         },
         "esbuild-freebsd-arm64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz",
-            "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.7.tgz",
+            "integrity": "sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q==",
             "dev": true,
             "optional": true
         },
         "esbuild-linux-32": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz",
-            "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.7.tgz",
+            "integrity": "sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg==",
             "dev": true,
             "optional": true
         },
         "esbuild-linux-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz",
-            "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.7.tgz",
+            "integrity": "sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ==",
             "dev": true,
             "optional": true
         },
         "esbuild-linux-arm": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz",
-            "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.7.tgz",
+            "integrity": "sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ==",
             "dev": true,
             "optional": true
         },
         "esbuild-linux-arm64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz",
-            "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.7.tgz",
+            "integrity": "sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw==",
             "dev": true,
             "optional": true
         },
         "esbuild-linux-mips64le": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz",
-            "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.7.tgz",
+            "integrity": "sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw==",
             "dev": true,
             "optional": true
         },
         "esbuild-linux-ppc64le": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz",
-            "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.7.tgz",
+            "integrity": "sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw==",
             "dev": true,
             "optional": true
         },
         "esbuild-linux-riscv64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz",
-            "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.7.tgz",
+            "integrity": "sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g==",
             "dev": true,
             "optional": true
         },
         "esbuild-linux-s390x": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz",
-            "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.7.tgz",
+            "integrity": "sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ==",
             "dev": true,
             "optional": true
         },
         "esbuild-netbsd-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz",
-            "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.7.tgz",
+            "integrity": "sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ==",
             "dev": true,
             "optional": true
         },
         "esbuild-openbsd-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz",
-            "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.7.tgz",
+            "integrity": "sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ==",
             "dev": true,
             "optional": true
         },
         "esbuild-sunos-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz",
-            "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.7.tgz",
+            "integrity": "sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag==",
             "dev": true,
             "optional": true
         },
         "esbuild-windows-32": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz",
-            "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.7.tgz",
+            "integrity": "sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA==",
             "dev": true,
             "optional": true
         },
         "esbuild-windows-64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz",
-            "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.7.tgz",
+            "integrity": "sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q==",
             "dev": true,
             "optional": true
         },
         "esbuild-windows-arm64": {
-            "version": "0.14.54",
-            "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz",
-            "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==",
+            "version": "0.15.7",
+            "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.7.tgz",
+            "integrity": "sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw==",
             "dev": true,
             "optional": true
         },
@@ -21624,6 +23070,12 @@
             "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
             "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
         },
+        "eventemitter2": {
+            "version": "6.4.8",
+            "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.8.tgz",
+            "integrity": "sha512-pAJurPyD+Nj/pfz8m0usKF1RW0E9gfY4Dfdem2l6jZbqcZlK8SP93qUMCv9V9FgOn+GSZEW6qeaglpf/vQ9D5A==",
+            "dev": true
+        },
         "events": {
             "version": "3.3.0",
             "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -21655,6 +23107,15 @@
                 "clone-regexp": "^2.1.0"
             }
         },
+        "executable": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
+            "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==",
+            "dev": true,
+            "requires": {
+                "pify": "^2.2.0"
+            }
+        },
         "exit": {
             "version": "0.1.2",
             "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
@@ -21765,7 +23226,7 @@
             "version": "3.0.2",
             "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
             "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
-            "optional": true
+            "devOptional": true
         },
         "extract-zip": {
             "version": "2.0.1",
@@ -21794,7 +23255,7 @@
             "version": "1.3.0",
             "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
             "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
-            "optional": true
+            "devOptional": true
         },
         "fast-deep-equal": {
             "version": "3.1.3",
@@ -21902,6 +23363,15 @@
                 "pend": "~1.2.0"
             }
         },
+        "figures": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+            "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+            "dev": true,
+            "requires": {
+                "escape-string-regexp": "^1.0.5"
+            }
+        },
         "file-entry-cache": {
             "version": "6.0.1",
             "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -22080,7 +23550,7 @@
             "version": "0.6.1",
             "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
             "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
-            "optional": true
+            "devOptional": true
         },
         "form-data": {
             "version": "4.0.0",
@@ -22286,11 +23756,20 @@
             "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.2.5.tgz",
             "integrity": "sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA=="
         },
+        "getos": {
+            "version": "3.2.1",
+            "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz",
+            "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==",
+            "dev": true,
+            "requires": {
+                "async": "^3.2.0"
+            }
+        },
         "getpass": {
             "version": "0.1.7",
             "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
             "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
-            "optional": true,
+            "devOptional": true,
             "requires": {
                 "assert-plus": "^1.0.0"
             }
@@ -22317,6 +23796,23 @@
                 "is-glob": "^4.0.3"
             }
         },
+        "global-dirs": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz",
+            "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==",
+            "dev": true,
+            "requires": {
+                "ini": "2.0.0"
+            },
+            "dependencies": {
+                "ini": {
+                    "version": "2.0.0",
+                    "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
+                    "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
+                    "dev": true
+                }
+            }
+        },
         "global-modules": {
             "version": "0.2.3",
             "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz",
@@ -22745,6 +24241,15 @@
             "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
             "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w=="
         },
+        "is-ci": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
+            "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
+            "dev": true,
+            "requires": {
+                "ci-info": "^3.2.0"
+            }
+        },
         "is-core-module": {
             "version": "2.10.0",
             "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz",
@@ -22798,6 +24303,16 @@
                 "is-extglob": "^2.1.1"
             }
         },
+        "is-installed-globally": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
+            "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
+            "dev": true,
+            "requires": {
+                "global-dirs": "^3.0.0",
+                "is-path-inside": "^3.0.2"
+            }
+        },
         "is-invalid-path": {
             "version": "0.1.0",
             "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz",
@@ -22840,6 +24355,12 @@
                 "has-tostringtag": "^1.0.0"
             }
         },
+        "is-path-inside": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+            "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+            "dev": true
+        },
         "is-plain-obj": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
@@ -22922,6 +24443,12 @@
             "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
             "devOptional": true
         },
+        "is-unicode-supported": {
+            "version": "0.1.0",
+            "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+            "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+            "dev": true
+        },
         "is-valid-path": {
             "version": "0.1.1",
             "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz",
@@ -22982,7 +24509,7 @@
             "version": "0.1.2",
             "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
             "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
-            "optional": true
+            "devOptional": true
         },
         "istanbul-lib-coverage": {
             "version": "3.2.0",
@@ -24507,7 +26034,7 @@
             "version": "0.1.1",
             "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
             "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
-            "optional": true
+            "devOptional": true
         },
         "jsdom": {
             "version": "16.7.0",
@@ -24596,7 +26123,7 @@
             "version": "0.4.0",
             "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
             "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
-            "optional": true
+            "devOptional": true
         },
         "json-schema-traverse": {
             "version": "0.4.1",
@@ -24614,7 +26141,7 @@
             "version": "5.0.1",
             "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
             "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
-            "optional": true
+            "devOptional": true
         },
         "json5": {
             "version": "2.2.1",
@@ -24771,6 +26298,12 @@
             "integrity": "sha512-RTSoaUAfLvpR357vWzAz/50Q/BmHfmE6ETSWfutT0AJiw10e6CmcdYRQJlLRd95B53D0Y2aD1jSxD3V3ySF+PA==",
             "dev": true
         },
+        "lazy-ass": {
+            "version": "1.6.0",
+            "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
+            "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==",
+            "dev": true
+        },
         "lazy-cache": {
             "version": "1.0.4",
             "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
@@ -24807,6 +26340,22 @@
             "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
             "dev": true
         },
+        "listr2": {
+            "version": "3.14.0",
+            "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
+            "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==",
+            "dev": true,
+            "requires": {
+                "cli-truncate": "^2.1.0",
+                "colorette": "^2.0.16",
+                "log-update": "^4.0.0",
+                "p-map": "^4.0.0",
+                "rfdc": "^1.3.0",
+                "rxjs": "^7.5.1",
+                "through": "^2.3.8",
+                "wrap-ansi": "^7.0.0"
+            }
+        },
         "locate-path": {
             "version": "5.0.0",
             "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -24933,6 +26482,116 @@
                 "lodash._baseuniq": "~4.6.0"
             }
         },
+        "log-symbols": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+            "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+            "dev": true,
+            "requires": {
+                "chalk": "^4.1.0",
+                "is-unicode-supported": "^0.1.0"
+            },
+            "dependencies": {
+                "ansi-styles": {
+                    "version": "4.3.0",
+                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+                    "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+                    "dev": true,
+                    "requires": {
+                        "color-convert": "^2.0.1"
+                    }
+                },
+                "chalk": {
+                    "version": "4.1.2",
+                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+                    "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+                    "dev": true,
+                    "requires": {
+                        "ansi-styles": "^4.1.0",
+                        "supports-color": "^7.1.0"
+                    }
+                },
+                "color-convert": {
+                    "version": "2.0.1",
+                    "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+                    "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+                    "dev": true,
+                    "requires": {
+                        "color-name": "~1.1.4"
+                    }
+                },
+                "color-name": {
+                    "version": "1.1.4",
+                    "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+                    "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+                    "dev": true
+                },
+                "has-flag": {
+                    "version": "4.0.0",
+                    "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+                    "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+                    "dev": true
+                },
+                "supports-color": {
+                    "version": "7.2.0",
+                    "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+                    "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+                    "dev": true,
+                    "requires": {
+                        "has-flag": "^4.0.0"
+                    }
+                }
+            }
+        },
+        "log-update": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
+            "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
+            "dev": true,
+            "requires": {
+                "ansi-escapes": "^4.3.0",
+                "cli-cursor": "^3.1.0",
+                "slice-ansi": "^4.0.0",
+                "wrap-ansi": "^6.2.0"
+            },
+            "dependencies": {
+                "ansi-styles": {
+                    "version": "4.3.0",
+                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+                    "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+                    "dev": true,
+                    "requires": {
+                        "color-convert": "^2.0.1"
+                    }
+                },
+                "color-convert": {
+                    "version": "2.0.1",
+                    "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+                    "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+                    "dev": true,
+                    "requires": {
+                        "color-name": "~1.1.4"
+                    }
+                },
+                "color-name": {
+                    "version": "1.1.4",
+                    "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+                    "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+                    "dev": true
+                },
+                "wrap-ansi": {
+                    "version": "6.2.0",
+                    "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+                    "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+                    "dev": true,
+                    "requires": {
+                        "ansi-styles": "^4.0.0",
+                        "string-width": "^4.1.0",
+                        "strip-ansi": "^6.0.0"
+                    }
+                }
+            }
+        },
         "lru-cache": {
             "version": "6.0.0",
             "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -25683,6 +27342,12 @@
             "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
             "dev": true
         },
+        "ospath": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
+            "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==",
+            "dev": true
+        },
         "p-finally": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
@@ -25706,6 +27371,15 @@
                 "p-limit": "^2.2.0"
             }
         },
+        "p-map": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+            "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+            "dev": true,
+            "requires": {
+                "aggregate-error": "^3.0.0"
+            }
+        },
         "p-timeout": {
             "version": "3.2.0",
             "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
@@ -25840,7 +27514,7 @@
             "version": "2.1.0",
             "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
             "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
-            "optional": true
+            "devOptional": true
         },
         "pg": {
             "version": "8.8.0",
@@ -25915,6 +27589,12 @@
             "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
             "dev": true
         },
+        "pify": {
+            "version": "2.3.0",
+            "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+            "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+            "dev": true
+        },
         "pirates": {
             "version": "4.0.5",
             "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
@@ -26050,6 +27730,12 @@
             "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
             "dev": true
         },
+        "pretty-bytes": {
+            "version": "5.6.0",
+            "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+            "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
+            "dev": true
+        },
         "pretty-format": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
@@ -26656,6 +28342,15 @@
                 }
             }
         },
+        "request-progress": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
+            "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==",
+            "dev": true,
+            "requires": {
+                "throttleit": "^1.0.0"
+            }
+        },
         "require-directory": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -26729,6 +28424,16 @@
             "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==",
             "dev": true
         },
+        "restore-cursor": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+            "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+            "dev": true,
+            "requires": {
+                "onetime": "^5.1.0",
+                "signal-exit": "^3.0.2"
+            }
+        },
         "retimer": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/retimer/-/retimer-3.0.0.tgz",
@@ -26755,9 +28460,9 @@
             }
         },
         "rollup": {
-            "version": "2.77.3",
-            "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz",
-            "integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==",
+            "version": "2.78.1",
+            "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.78.1.tgz",
+            "integrity": "sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==",
             "dev": true,
             "requires": {
                 "fsevents": "~2.3.2"
@@ -27244,7 +28949,7 @@
             "version": "1.17.0",
             "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
             "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==",
-            "optional": true,
+            "devOptional": true,
             "requires": {
                 "asn1": "~0.2.3",
                 "assert-plus": "^1.0.0",
@@ -27743,6 +29448,12 @@
             "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==",
             "dev": true
         },
+        "throttleit": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
+            "integrity": "sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==",
+            "dev": true
+        },
         "through": {
             "version": "2.3.8",
             "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -27760,6 +29471,15 @@
             "integrity": "sha512-yfOzyuVwzgD0LkldD3Epkr+JUdUIxEUL147Fa6ZgG/23KU28iOv3e3M7vQOCFMPyopAhDX7dqOLWttIP7tkTKg==",
             "dev": true
         },
+        "tmp": {
+            "version": "0.2.1",
+            "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
+            "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
+            "dev": true,
+            "requires": {
+                "rimraf": "^3.0.0"
+            }
+        },
         "tmpl": {
             "version": "1.0.5",
             "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -27847,7 +29567,7 @@
             "version": "0.6.0",
             "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
             "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
-            "optional": true,
+            "devOptional": true,
             "requires": {
                 "safe-buffer": "^5.0.1"
             }
@@ -27856,7 +29576,7 @@
             "version": "0.14.5",
             "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
             "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
-            "optional": true
+            "devOptional": true
         },
         "type-check": {
             "version": "0.4.0",
@@ -27974,6 +29694,12 @@
             "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
             "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
         },
+        "untildify": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+            "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+            "dev": true
+        },
         "update-browserslist-db": {
             "version": "1.0.5",
             "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz",
@@ -28079,7 +29805,7 @@
             "version": "1.10.0",
             "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
             "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
-            "optional": true,
+            "devOptional": true,
             "requires": {
                 "assert-plus": "^1.0.0",
                 "core-util-is": "1.0.2",
@@ -28087,16 +29813,16 @@
             }
         },
         "vite": {
-            "version": "2.9.15",
-            "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.15.tgz",
-            "integrity": "sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==",
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/vite/-/vite-3.1.0.tgz",
+            "integrity": "sha512-YBg3dUicDpDWFCGttmvMbVyS9ydjntwEjwXRj2KBFwSB8SxmGcudo1yb8FW5+M/G86aS8x828ujnzUVdsLjs9g==",
             "dev": true,
             "requires": {
-                "esbuild": "^0.14.27",
+                "esbuild": "^0.15.6",
                 "fsevents": "~2.3.2",
-                "postcss": "^8.4.13",
-                "resolve": "^1.22.0",
-                "rollup": ">=2.59.0 <2.78.0"
+                "postcss": "^8.4.16",
+                "resolve": "^1.22.1",
+                "rollup": "~2.78.0"
             }
         },
         "vite-plugin-compression": {
diff --git a/package.json b/package.json
index be335ae0..34936fdf 100644
--- a/package.json
+++ b/package.json
@@ -121,8 +121,8 @@
         "@fortawesome/vue-fontawesome": "~3.0.0-5",
         "@popperjs/core": "~2.10.2",
         "@types/bootstrap": "~5.1.9",
-        "@vitejs/plugin-legacy": "~1.8.2",
-        "@vitejs/plugin-vue": "~2.3.3",
+        "@vitejs/plugin-legacy": "~2.1.0",
+        "@vitejs/plugin-vue": "~3.1.0",
         "@vue/compiler-sfc": "~3.2.36",
         "aedes": "^0.46.3",
         "babel-plugin-rewire": "~1.2.0",
@@ -153,7 +153,7 @@
         "timezones-list": "~3.0.1",
         "typescript": "~4.4.4",
         "v-pagination-3": "~0.1.7",
-        "vite": "~2.9.9",
+        "vite": "~3.1.0",
         "vite-plugin-compression": "^0.5.1",
         "vue": "next",
         "vue-chart-3": "3.0.9",

From 0dd858d5169c3b30ce492ea55d79dae6545413e4 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Mon, 12 Sep 2022 18:45:18 +0800
Subject: [PATCH 022/134] Warn about the backup feature

---
 src/components/settings/Backup.vue | 6 ++++++
 src/languages/en.js                | 2 ++
 2 files changed, 8 insertions(+)

diff --git a/src/components/settings/Backup.vue b/src/components/settings/Backup.vue
index 685a4c6b..4d6756c7 100644
--- a/src/components/settings/Backup.vue
+++ b/src/components/settings/Backup.vue
@@ -1,6 +1,12 @@
 <template>
     <div>
         <div class="my-4">
+            <div class="alert alert-warning" role="alert" style="border-radius: 15px;">
+                {{ $t("backupOutdatedWarning") }}<br />
+                <br />
+                {{ $t("backupRecommend") }}
+            </div>
+
             <h4 class="mt-4 mb-2">{{ $t("Export Backup") }}</h4>
 
             <p>
diff --git a/src/languages/en.js b/src/languages/en.js
index 67fb4bea..7d980f63 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -580,4 +580,6 @@ export default {
     goAlertInfo: "GoAlert is a An open source application for on-call scheduling, automated escalations and notifications (like SMS or voice calls). Automatically engage the right person, the right way, and at the right time! {0}",
     goAlertIntegrationKeyInfo: "Get generic API integration key for the service in this format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" usually the value of token parameter of copied URL.",
     goAlert: "GoAlert",
+    backupOutdatedWarning: "Deprecated: Since a lot of features added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.",
+    backupRecommend: "Please backup the volume or the data folder (./data/) directly instead.",
 };

From d63022676a68b59f13382bc7bd9a6330eaaacc3d Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 13 Sep 2022 15:17:39 +0800
Subject: [PATCH 023/134] Fix build issue after updated vite

---
 package-lock.json | 87 +++++++++++++++++++++++++++++++++++++++++------
 package.json      |  7 ++--
 2 files changed, 80 insertions(+), 14 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 7c32fd43..65e380d1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -84,9 +84,9 @@
                 "favico.js": "^0.3.10",
                 "jest": "~27.2.5",
                 "jest-puppeteer": "~6.0.3",
-                "postcss-html": "^1.3.1",
-                "postcss-rtlcss": "~3.4.1",
-                "postcss-scss": "~4.0.3",
+                "postcss-html": "~1.5.0",
+                "postcss-rtlcss": "~3.7.2",
+                "postcss-scss": "~4.0.4",
                 "prismjs": "^1.27.0",
                 "puppeteer": "~13.1.3",
                 "qrcode": "~1.5.0",
@@ -94,6 +94,7 @@
                 "sass": "~1.42.1",
                 "stylelint": "~14.7.1",
                 "stylelint-config-standard": "~25.0.0",
+                "terser": "^5.15.0",
                 "timezones-list": "~3.0.1",
                 "typescript": "~4.4.4",
                 "v-pagination-3": "~0.1.7",
@@ -3098,6 +3099,16 @@
                 "node": ">=6.0.0"
             }
         },
+        "node_modules/@jridgewell/source-map": {
+            "version": "0.3.2",
+            "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
+            "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
+            "dev": true,
+            "dependencies": {
+                "@jridgewell/gen-mapping": "^0.3.0",
+                "@jridgewell/trace-mapping": "^0.3.9"
+            }
+        },
         "node_modules/@jridgewell/sourcemap-codec": {
             "version": "1.4.14",
             "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
@@ -13738,12 +13749,12 @@
             "dev": true
         },
         "node_modules/postcss-rtlcss": {
-            "version": "3.4.1",
-            "resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-3.4.1.tgz",
-            "integrity": "sha512-4SOkC34IJ086dYjmqGCeIOqQe4JTDk+jwETvq1M/57+bQA6CXEWAjGtqifjcSH75nd0vfW7+hve0Ec4ZYHmMtA==",
+            "version": "3.7.2",
+            "resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-3.7.2.tgz",
+            "integrity": "sha512-GurrGedCKvOTe1QrifI+XpDKXA3bJky1v8KiOa/TYYHs1bfJOxI53GIRvVSqLJLly7e1WcNMz8KMESTN01vbZQ==",
             "dev": true,
             "dependencies": {
-                "rtlcss": "^3.3.0"
+                "rtlcss": "^3.5.0"
             },
             "engines": {
                 "node": ">=12.0.0"
@@ -16030,6 +16041,30 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "node_modules/terser": {
+            "version": "5.15.0",
+            "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz",
+            "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==",
+            "dev": true,
+            "dependencies": {
+                "@jridgewell/source-map": "^0.3.2",
+                "acorn": "^8.5.0",
+                "commander": "^2.20.0",
+                "source-map-support": "~0.5.20"
+            },
+            "bin": {
+                "terser": "bin/terser"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/terser/node_modules/commander": {
+            "version": "2.20.3",
+            "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+            "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+            "dev": true
+        },
         "node_modules/test-exclude": {
             "version": "6.0.0",
             "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@@ -19580,6 +19615,16 @@
             "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
             "dev": true
         },
+        "@jridgewell/source-map": {
+            "version": "0.3.2",
+            "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
+            "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
+            "dev": true,
+            "requires": {
+                "@jridgewell/gen-mapping": "^0.3.0",
+                "@jridgewell/trace-mapping": "^0.3.9"
+            }
+        },
         "@jridgewell/sourcemap-codec": {
             "version": "1.4.14",
             "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
@@ -27665,12 +27710,12 @@
             "dev": true
         },
         "postcss-rtlcss": {
-            "version": "3.4.1",
-            "resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-3.4.1.tgz",
-            "integrity": "sha512-4SOkC34IJ086dYjmqGCeIOqQe4JTDk+jwETvq1M/57+bQA6CXEWAjGtqifjcSH75nd0vfW7+hve0Ec4ZYHmMtA==",
+            "version": "3.7.2",
+            "resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-3.7.2.tgz",
+            "integrity": "sha512-GurrGedCKvOTe1QrifI+XpDKXA3bJky1v8KiOa/TYYHs1bfJOxI53GIRvVSqLJLly7e1WcNMz8KMESTN01vbZQ==",
             "dev": true,
             "requires": {
-                "rtlcss": "^3.3.0"
+                "rtlcss": "^3.5.0"
             }
         },
         "postcss-safe-parser": {
@@ -29420,6 +29465,26 @@
                 "supports-hyperlinks": "^2.0.0"
             }
         },
+        "terser": {
+            "version": "5.15.0",
+            "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz",
+            "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==",
+            "dev": true,
+            "requires": {
+                "@jridgewell/source-map": "^0.3.2",
+                "acorn": "^8.5.0",
+                "commander": "^2.20.0",
+                "source-map-support": "~0.5.20"
+            },
+            "dependencies": {
+                "commander": {
+                    "version": "2.20.3",
+                    "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+                    "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+                    "dev": true
+                }
+            }
+        },
         "test-exclude": {
             "version": "6.0.0",
             "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
diff --git a/package.json b/package.json
index 34936fdf..219042aa 100644
--- a/package.json
+++ b/package.json
@@ -140,9 +140,9 @@
         "favico.js": "^0.3.10",
         "jest": "~27.2.5",
         "jest-puppeteer": "~6.0.3",
-        "postcss-html": "^1.3.1",
-        "postcss-rtlcss": "~3.4.1",
-        "postcss-scss": "~4.0.3",
+        "postcss-html": "~1.5.0",
+        "postcss-rtlcss": "~3.7.2",
+        "postcss-scss": "~4.0.4",
         "prismjs": "^1.27.0",
         "puppeteer": "~13.1.3",
         "qrcode": "~1.5.0",
@@ -150,6 +150,7 @@
         "sass": "~1.42.1",
         "stylelint": "~14.7.1",
         "stylelint-config-standard": "~25.0.0",
+        "terser": "^5.15.0",
         "timezones-list": "~3.0.1",
         "typescript": "~4.4.4",
         "v-pagination-3": "~0.1.7",

From 527e479f2d0a5e9b7dff6b9027bba71f5950cfcd Mon Sep 17 00:00:00 2001
From: Gilas Amalanda <chunkz.ring@gmail.com>
Date: Tue, 13 Sep 2022 21:30:15 +0700
Subject: [PATCH 024/134] chore: update existing and add new text for language
 id-ID

---
 src/languages/id-ID.js | 309 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 304 insertions(+), 5 deletions(-)

diff --git a/src/languages/id-ID.js b/src/languages/id-ID.js
index 0a065308..6f069f65 100644
--- a/src/languages/id-ID.js
+++ b/src/languages/id-ID.js
@@ -2,6 +2,8 @@ export default {
     languageName: "Bahasa Indonesia (Indonesian)",
     checkEverySecond: "Cek Setiap {0} detik.",
     retryCheckEverySecond: "Coba lagi setiap {0} detik.",
+    resendEveryXTimes: "Kirim ulang setiap {0} kali",
+    resendDisabled: "Kirim ulang dinonaktifkan",
     retriesDescription: "Percobaan ulang maksimum sebelum layanan dinyatakan tidak aktif dan notifikasi dikirim",
     ignoreTLSError: "Abaikan kesalahan TLS/SSL untuk situs web HTTPS",
     upsideDownModeDescription: "Balikkan statusnya. Jika layanan dapat dijangkau, TIDAK AKTIF.",
@@ -13,6 +15,7 @@ export default {
     pauseDashboardHome: "Jeda",
     deleteMonitorMsg: "Apakah Anda mau menghapus monitor ini?",
     deleteNotificationMsg: "Apakah Anda mau menghapus notifikasi untuk semua monitor?",
+    dnsPortDescription: "Port server DNS. Bawaan menggunakan 53. Anda dapat mengubah port kapan saja.",
     resolverserverDescription: "Cloudflare adalah server bawaan, Anda dapat mengubah server resolver kapan saja.",
     rrtypeDescription: "Pilih RR-Type yang mau Anda monitor",
     pauseMonitorMsg: "Apakah Anda yakin mau menjeda?",
@@ -33,6 +36,7 @@ export default {
     Appearance: "Tampilan",
     Theme: "Tema",
     General: "Umum",
+    "Primary Base URL": "URL Dasar Utama",
     Version: "Versi",
     "Check Update On GitHub": "Cek Pembaruan di GitHub",
     List: "Daftar",
@@ -54,7 +58,7 @@ export default {
     Delete: "Hapus",
     Current: "Saat ini",
     Uptime: "Waktu aktif",
-    "Cert Exp.": "Cert Exp.",
+    "Cert Exp.": "Batas kedaluwarsa SSL",
     day: "hari | hari-hari",
     "-day": "-hari",
     hour: "Jam",
@@ -62,7 +66,7 @@ export default {
     Response: "Tanggapan",
     Ping: "Ping",
     "Monitor Type": "Tipe Monitor",
-    Keyword: "Keyword",
+    Keyword: "Kata Kunci",
     "Friendly Name": "Nama yang Ramah",
     URL: "URL",
     Hostname: "Hostname",
@@ -70,10 +74,14 @@ export default {
     "Heartbeat Interval": "Jarak Waktu Heartbeat ",
     Retries: "Coba lagi",
     "Heartbeat Retry Interval": "Jarak Waktu Heartbeat Mencoba kembali ",
+    "Resend Notification if Down X times consequently": "Kirim Ulang Notifikasi jika Tidak Aktif X kali",
     Advanced: "Tingkat Lanjut",
     "Upside Down Mode": "Mode Terbalik",
     "Max. Redirects": "Maksimal Pengalihan",
     "Accepted Status Codes": "Kode Status yang Diterima",
+    "Push URL": "Push URL",
+    needPushEvery: "Anda harus memanggil URL berikut setiap {0} detik..",
+    pushOptionalParams: "Parameter tambahan: {0}",
     Save: "Simpan",
     Notifications: "Notifikasi",
     "Not available, please setup.": "Tidak tersedia, silakan atur.",
@@ -155,7 +163,7 @@ export default {
     "Show URI": "Lihat URI",
     Tags: "Tanda",
     "Add New below or Select...": "Tambahkan Baru di bawah atau Pilih...",
-    "Tag with this name already exist.": "Tanda dengan nama ini sudah ada.",
+    "Tag with this name already exist.": "Tandadengan nama ini sudah ada.",
     "Tag with this value already exist.": "Tanda dengan nilai ini sudah ada.",
     color: "warna",
     "value (optional)": "nilai (harus diisi)",
@@ -187,7 +195,7 @@ export default {
     Required: "Dibutuhkan",
     telegram: "Telegram",
     "Bot Token": "Bot Token",
-    "You can get a token from": "Anda bisa mendapatkan token dari",
+    wayToGetTelegramToken: "Anda dapat mendapatkan token dari {0}.",
     "Chat ID": "Chat ID",
     supportTelegramChatID: "Mendukung Obrolan Langsung / Grup / Channel Chat ID",
     wayToGetTelegramChatID: "Anda bisa mendapatkan chat id Anda dengan mengirim pesan ke bot dan pergi ke url ini untuk melihat chat_id:",
@@ -203,6 +211,7 @@ export default {
     secureOptionTLS: "TLS (465)",
     "Ignore TLS Error": "Abaikan Kesalahan TLS",
     "From Email": "Dari Surel",
+    emailCustomSubject: "Subjek",
     "To Email": "Ke Surel",
     smtpCC: "CC",
     smtpBCC: "BCC",
@@ -236,10 +245,13 @@ export default {
     "rocket.chat": "Rocket.chat",
     pushover: "Pushover",
     pushy: "Pushy",
+    PushByTechulus: "Push by Techulus",
     octopush: "Octopush",
     promosms: "PromoSMS",
+    clicksendsms: "ClickSend SMS",
     lunasea: "LunaSea",
     apprise: "Apprise (Mendukung 50+ layanan notifikasi)",
+    GoogleChat: "Google Chat (hanya Google Workspace)",
     pushbullet: "Pushbullet",
     line: "Line Messenger",
     mattermost: "Mattermost",
@@ -253,6 +265,9 @@ export default {
     "SMS Type": "Tipe SMS",
     octopushTypePremium: "Premium (Cepat - direkomendasikan untuk mengingatkan)",
     octopushTypeLowCost: "Low Cost (Lambat, terkadang diblokir oleh operator)",
+    checkPrice: "Check {0} prices:",
+    apiCredentials: "Kredensial API",
+    octopushLegacyHint: "Apakah Anda menggunakan Octopush versi lama (2011-2020) atau versi baru?",
     "Check octopush prices": "Cek harga octopush {0}.",
     octopushPhoneNumber: "Nomer Telpon/HP (format internasional, contoh : +33612345678) ",
     octopushSMSSender: "Nama Pengirim SMS : 3-11 karakter alfanumerik dan spasi (a-zA-Z0-9)",
@@ -278,9 +293,293 @@ export default {
     matrix: "Matrix",
     promosmsTypeEco: "SMS ECO - murah tapi lambat dan sering kelebihan beban. Terbatas hanya untuk penerima Polandia.",
     promosmsTypeFlash: "SMS FLASH - Pesan akan otomatis muncul di perangkat penerima. Terbatas hanya untuk penerima Polandia.",
-    promosmsTypeFull: "SMS FULL - SMS tingkat premium, Anda dapat menggunakan Nama Pengirim Anda (Anda harus mendaftarkan nama terlebih dahulu). Dapat diAndalkan untuk peringatan.",
+    promosmsTypeFull: "SMS FULL - SMS tingkat premium, Anda dapat menggunakan Nama Pengirim Anda (Anda harus mendaftarkan nama terlebih dahulu). DapatdiAndalkan untuk peringatan.",
     promosmsTypeSpeed: "SMS SPEED - Prioritas tertinggi dalam sistem. Sangat cepat dan dapat diandalkan tetapi mahal (sekitar dua kali lipat dari harga SMS FULL).",
     promosmsPhoneNumber: "Nomor telepon (untuk penerima Polandia Anda dapat melewati kode area)",
     promosmsSMSSender: "Nama Pengirim SMS : Nama pra-registrasi atau salah satu bawaan: InfoSMS, Info SMS, MaxSMS, INFO, SMS",
     "Feishu WebHookUrl": "Feishu WebHookUrl",
+    matrixHomeserverURL: "Homeserver URL (dengan http(s):// dan port tambahan)",
+    "Internal Room Id": "Internal Room ID",
+    matrixDesc1: "Kamu dapat menemukan Internal Room ID dengan melihat di bagian konfigurasi ruang di Matrix. Seharusnya berbentuk seperti !QMdRCpUIfLwsfjxye6:home.server.",
+    matrixDesc2: "Sangat direkomendasikan kepada Anda untuk membuat akun baru dan jangan menggunakan token atas akun terkini yang memiliki token akses secara penuh terhadap akun dan seluruh ruang yang terdaftar. Alih - alih, buat akun baru dan undang akun tsb ke ruang tempat anda ingin menerima notifikasi. Untuk mendapatkan token akses anda dapat menjalankan {0}",
+    Method: "Method",
+    Body: "Body",
+    Headers: "Headers",
+    PushUrl: "Push URL",
+    HeadersInvalidFormat: "Request Headers memiliki format JSON yang tidak sesuai: ",
+    BodyInvalidFormat: "Request Body memiliki format JSON yang tidak sesuai: ",
+    "Monitor History": "Riyawat Monitor",
+    clearDataOlderThan: "Simpan data riwayat monitoring selama {0} hari.",
+    PasswordsDoNotMatch: "Passwords tidak sama.",
+    records: "catatan",
+    "One record": "Satu catatan",
+    steamApiKeyDescription: "Untuk monitoring Steam Game Server Anda membutuhkan kunci Steam Web-API. Anda dapat mendaftarkan Kunci API Anda melalui: ",
+    "Current User": "Pengguna Saat Ini",
+    topic: "Topic",
+    topicExplanation: "MQTT topic untuk dimonitor",
+    successMessage: "Pesan Berhasil",
+    successMessageExplanation: "Pesan MQTT yang akan dianggap berhasil",
+    recent: "Baru saja",
+    Done: "Selesai",
+    Info: "Info",
+    Security: "Keamaan",
+    "Steam API Key": "Steam API Key",
+    "Shrink Database": "Shrink Database",
+    "Pick a RR-Type...": "Pilih RR-Type...",
+    "Pick Accepted Status Codes...": "Pilih Kode Status yang Diterima...",
+    Default: "Default",
+    "HTTP Options": "HTTP Options",
+    "Create Incident": "Buat Incident",
+    Title: "Judul",
+    Content: "Konten",
+    Style: "Gaya",
+    info: "info",
+    warning: "peringatan",
+    danger: "bahaya",
+    error: "kesalahan",
+    critical: "kritis",
+    primary: "utama",
+    light: "terang",
+    dark: "gelap",
+    Post: "Post",
+    "Please input title and content": "Masukkan judul dan konten",
+    Created: "Dibuat",
+    "Last Updated": "Terakhir Diperbarui",
+    Unpin: "Lepaskan Semat",
+    "Switch to Light Theme": "Ubah ke Tema Terang",
+    "Switch to Dark Theme": "Ubah ke Tema Gelap",
+    "Show Tags": "Tampilkan Tags",
+    "Hide Tags": "Sembunyikan Tags",
+    Description: "Deskripsi",
+    "No monitors available.": "Tidak ada monitor yang tersedia.",
+    "Add one": "Tambahkan",
+    "No Monitors": "Tidak ada monitor",
+    "Untitled Group": "Group Tanpa Judul",
+    Services: "Layanan",
+    Discard: "Buang",
+    Cancel: "Batal",
+    "Powered by": "Dipersembahkan oleh",
+    shrinkDatabaseDescription: "Trigger database VACUUM untuk SQLite. Jika database Anda dibuat setelah 1.10.0, AUTO_VACUUM sudah otomatis diaktifkan dan aksi berikut tidak dibutuhkan.",
+    serwersms: "SerwerSMS.pl",
+    serwersmsAPIUser: "Nama Pengguna API ( termamsuk awalan webapi_ )",
+    serwersmsAPIPassword: "Kata Sandi API",
+    serwersmsPhoneNumber: "Nomor Telepon",
+    serwersmsSenderName: "Nama Pengirim SMS (didaftarkan melalui portal pelanggan)",
+    stackfield: "Stackfield",
+    Customize: "Kustomisasi",
+    "Custom Footer": "Tambahan Footer",
+    "Custom CSS": "Tambahan CSS",
+    smtpDkimSettings: "Pengaturan DKIM",
+    smtpDkimDesc: "Silakan merujuk ke Nodemailer DKIM {0} untuk penggunaan.",
+    documentation: "dokumentasi",
+    smtpDkimDomain: "Nama Domain",
+    smtpDkimKeySelector: "Key Selector",
+    smtpDkimPrivateKey: "Private Key",
+    smtpDkimHashAlgo: "Algoritma Hash (Opsional)",
+    smtpDkimheaderFieldNames: "Header Keys untuk ditambahkan (Optional)",
+    smtpDkimskipFields: "Header Keys not untuk ditambahkan (Optional)",
+    wayToGetPagerDutyKey: "Anda dapat menambahkan melalui Service -> Service Directory -> (Select a service) -> Integrations -> Add integration. Lalu Anda dapat menjadi dengan kata kunci \"Events API V2\". Informasi tambahan {0}",
+    "Integration Key": "Integration Key",
+    "Integration URL": "Integration URL",
+    "Auto resolve or acknowledged": "Penyelesaian otomatis atau diakui",
+    "do nothing": "tidak melakukan apapun",
+    "auto acknowledged": "otomatis diakui",
+    "auto resolve": "otomatis terselesaikan",
+    gorush: "Gorush",
+    alerta: "Alerta",
+    alertaApiEndpoint: "API Endpoint",
+    alertaEnvironment: "Lingkungan",
+    alertaApiKey: "Kunci API",
+    alertaAlertState: "Status Siaga",
+    alertaRecoverState: "Status Pemulihan",
+    deleteStatusPageMsg: "Apakah Anda yakin untuk menghapus halaman status berikut?",
+    Proxies: "Proxies",
+    default: "Bawaan",
+    enabled: "Diaktifkan",
+    setAsDefault: "Tetapkan sebagai bawaan",
+    deleteProxyMsg: "Apakah Anda yakin ingin menghapus proxy berikut untuk seluruh monitor?",
+    proxyDescription: "Proxy harus ditambahkan ke monitor agar berfungsi.",
+    enableProxyDescription: "Proxy berikut tidak akan berdampak ke monitor hingga diaktifkan. Anda dapat mengontrol menonaktifkan sementara proxy dari semua monitor dengan status aktivasi.",
+    setAsDefaultProxyDescription: "Proxy berikut akan diaktifkan sebagai bawaan untuk monitor baru. Anda masih dapat menonaktifkan proxy secara terpisah untuk setiap monitor.",
+    "Certificate Chain": "Certificate Chain",
+    Valid: "Sahih",
+    Invalid: "Tidak Sahih",
+    AccessKeyId: "AccessKey ID",
+    SecretAccessKey: "AccessKey Secret",
+    PhoneNumbers: "Nomor Telepon",
+    TemplateCode: "Kode Template",
+    SignName: "Nama Tanda",
+    "Sms template must contain parameters: ": "Template SMS harus memuat parameter: ",
+    "Bark Endpoint": "Bark Endpoint",
+    "Bark Group": "Bark Group",
+    "Bark Sound": "Bark Sound",
+    WebHookUrl: "WebHookUrl",
+    SecretKey: "SecretKey",
+    "For safety, must use secret key": "Untuk keamaan Anda harus menggunakan kunci rahasia",
+    "Device Token": "Token Perangkat",
+    Platform: "Platform",
+    iOS: "iOS",
+    Android: "Android",
+    Huawei: "Huawei",
+    High: "Tinggi",
+    Retry: "Ulang",
+    Topic: "Topik",
+    "WeCom Bot Key": "Kunci WeCom Bot",
+    "Setup Proxy": "Siapkan Proxy",
+    "Proxy Protocol": "Protokol Proxy",
+    "Proxy Server": "Server Proxy",
+    "Proxy server has authentication": "Server Proxy memiliki autentikasi",
+    User: "Pengguna",
+    Installed: "Terpasang",
+    "Not installed": "Tidak terpasang",
+    Running: "Berlari",
+    "Not running": "Tidak berlari",
+    "Remove Token": "Hapus Token",
+    Start: "Mulai",
+    Stop: "Berhenti",
+    "Uptime Kuma": "Uptime Kuma",
+    "Add New Status Page": "Tambahkan Halaman Status Baru",
+    Slug: "Slug",
+    "Accept characters:": "Terima karakter:",
+    startOrEndWithOnly: "Mulai atau akhiri hanya dengan {0}",
+    "No consecutive dashes": "Tanda hubung tidak berurutan",
+    Next: "Selanjutnya",
+    "The slug is already taken. Please choose another slug.": "Slug is telah digunakan. Silakan pilih slug lain.",
+    "No Proxy": "TIdak ada Proxy",
+    Authentication: "Autentikasi",
+    "HTTP Basic Auth": "HTTP Basic Auth",
+    "New Status Page": "Halaman Status Baru",
+    "Page Not Found": "Halaman Tidak Ditemukan",
+    "Reverse Proxy": "Proxy Terbalik",
+    Backup: "Cadangan",
+    About: "Tentang",
+    wayToGetCloudflaredURL: "(Unduh cloudflared dari {0})",
+    cloudflareWebsite: "Situs Cloudflare",
+    "Message:": "Pesan:",
+    "Don't know how to get the token? Please read the guide:": "Tidak tahu cara mendapatkan token? Silakan baca panduannya:",
+    "The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Koneksi saat ini mungkin hilang jika Anda saat ini terhubung melalui CloudflareTunnel. Apakah Anda yakin ingin menghentikannya? Ketik kata sandi Anda saat ini untuk mengonfirmasinya.",
+    "HTTP Headers": "HTTP Headers",
+    "Trust Proxy": "Proxy Terpercaya",
+    "Other Software": "Perangkat Lunak lainnya",
+    "For example: nginx, Apache and Traefik.": "Sebagai contoh: nginx, Apache and Traefik.",
+    "Please read": "Harap dibaca",
+    "Subject:": "Subjek:",
+    "Valid To:": "Berlaku Untuk:",
+    "Days Remaining:": "Hari Tersisa:",
+    "Issuer:": "Penerbit:",
+    "Fingerprint:": "Sidik jari:",
+    "No status pages": "Tidak ada halaman status",
+    "Domain Name Expiry Notification": "Pemberitahuan Kedaluwarsa Nama Domain",
+    Proxy: "Proxy",
+    "Date Created": "Tanggal Dibuat",
+    HomeAssistant: "Home Assistant",
+    onebotHttpAddress: "Alamat HTTP OneBot",
+    onebotMessageType: "Jenis Pesan OneBot",
+    onebotGroupMessage: "Grup",
+    onebotPrivateMessage: "Pribadi",
+    onebotUserOrGroupId: "Grup/Pengguna ID",
+    onebotSafetyTips: "Untuk keamanan, harus mengatur token akses",
+    "PushDeer Key": "Kunci PushDeer",
+    "Footer Text": "Tulisan Footer",
+    "Show Powered By": "Tampilkan Dipersembahkan oleh",
+    "Domain Names": "Nama Domain",
+    signedInDisp: "Masuk sebagai {0}",
+    signedInDispDisabled: "Autentikasi dinonaktifkan.",
+    RadiusSecret: "Radius Secret",
+    RadiusSecretDescription: "Shared Secret antara klien dan server",
+    RadiusCalledStationId: "Called Station Id",
+    RadiusCalledStationIdDescription: "Pengenal perangkat yang dipanggil",
+    RadiusCallingStationId: "Calling Station Id",
+    RadiusCallingStationIdDescription: "Pengenal perangkat panggilan",
+    "Certificate Expiry Notification": "Pemberitahuan Kedaluwarsa Sertifikat",
+    "API Username": "Nama Pengguna API",
+    "API Key": "Kunci API",
+    "Recipient Number": "Nomor Penerima Recipient Number",
+    "From Name/Number": "Dari Nama/Nomor",
+    "Leave blank to use a shared sender number.": "Biarkan kosong untuk menggunakan nomor pengirim bersama.",
+    "Octopush API Version": "Versi API Octopush",
+    "Legacy Octopush-DM": "Legacy Octopush-DM",
+    endpoint: "endpoint",
+    octopushAPIKey: "\"API key\" dari kredensial HTTP API di panel kontrol",
+    octopushLogin: "\"Login\" dari kredensial HTTP API di panel kontrol",
+    promosmsLogin: "Nama Masuk API",
+    promosmsPassword: "Kata Sandi API",
+    "pushoversounds pushover": "Pushover (default)",
+    "pushoversounds bike": "Bike",
+    "pushoversounds bugle": "Bugle",
+    "pushoversounds cashregister": "Cash Register",
+    "pushoversounds classical": "Classical",
+    "pushoversounds cosmic": "Cosmic",
+    "pushoversounds falling": "Falling",
+    "pushoversounds gamelan": "Gamelan",
+    "pushoversounds incoming": "Incoming",
+    "pushoversounds intermission": "Intermission",
+    "pushoversounds magic": "Magic",
+    "pushoversounds mechanical": "Mechanical",
+    "pushoversounds pianobar": "Piano Bar",
+    "pushoversounds siren": "Siren",
+    "pushoversounds spacealarm": "Space Alarm",
+    "pushoversounds tugboat": "Tug Boat",
+    "pushoversounds alien": "Alien Alarm (long)",
+    "pushoversounds climb": "Climb (long)",
+    "pushoversounds persistent": "Persistent (long)",
+    "pushoversounds echo": "Pushover Echo (long)",
+    "pushoversounds updown": "Up Down (long)",
+    "pushoversounds vibrate": "Vibrate Only",
+    "pushoversounds none": "None (silent)",
+    pushyAPIKey: "Secret API Key",
+    pushyToken: "Device token",
+    "Show update if available": "Tampilkan pembaruan jika tersedia",
+    "Also check beta release": "Periksa juga rilis beta",
+    "Using a Reverse Proxy?": "Menggunakan Proxy Terbalik?",
+    "Check how to config it for WebSocket": "Periksa cara mengonfigurasinya untuk A WebSocket",
+    "Steam Game Server": "Steam Game Server",
+    "Most likely causes:": "Kemungkinan besar penyebabnya:",
+    "The resource is no longer available.": "Sumber daya tidak lagi tersedia.",
+    "There might be a typing error in the address.": "Mungkin ada kesalahan pengetikan di alamat.",
+    "What you can try:": "Apa yang dapat kamu coba:",
+    "Retype the address.": "Ketik ulang alamat.",
+    "Go back to the previous page.": "Kembali ke halaman sebelumnya.",
+    "Coming Soon": "Segera",
+    wayToGetClickSendSMSToken: "Anda bisa mendapatkan Nama Pengguna API dan Kunci API dari {0} .",
+    "Connection String": "Connection String",
+    Query: "Query",
+    settingsCertificateExpiry: "Kedaluwarsa Sertifikat TLS",
+    certificationExpiryDescription: "Monitor HTTPS memicu pemberitahuan saat sertifikat TLS kedaluwarsa dalam:",
+    "Setup Docker Host": "Siapkan Host Docker",
+    "Connection Type": "Jenis Koneksi",
+    "Docker Daemon": "Docker Daemon",
+    deleteDockerHostMsg: "Apakah Anda yakin ingin menghapus host docker berikut untuk semua monitor?",
+    socket: "Socket",
+    tcp: "TCP / HTTP",
+    "Docker Container": "Docker Container",
+    "Container Name / ID": "Container Name / ID",
+    "Docker Host": "Docker Host",
+    "Docker Hosts": "Docker Hosts",
+    "ntfy Topic": "ntfy Topic",
+    Domain: "Domain",
+    Workstation: "Workstation",
+    disableCloudflaredNoAuthMsg: "Anda berada dalam mode Tanpa Otentikasi, kata sandi tidak diperlukan.",
+    trustProxyDescription: "Trust 'X-Forwarded-*' headers. Jika Anda ingin mendapatkan IP klien yang benar dan Uptime Kuma Anda dibalik layanan seperti Nginxor Apache, Anda harus mengaktifkan ini.",
+    wayToGetLineNotifyToken: "Anda bisa mendapatkan token akses dari {0}",
+    Examples: "Contoh",
+    "Home Assistant URL": "Home Assistant URL",
+    "Long-Lived Access Token": "Token Akses Berumur Panjang",
+    "Long-Lived Access Token canbe created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Token Akses Berumur Panjang dapat dibuat dengan mengklik nama profil Anda (kiri bawah) dan menggulir ke bawah lalu klik Buat Token. ",
+    "Notification Service": "Layanan Pemberitahuan",
+    "default: notify all devices": "bawaan: notifikasi seluruh perangkat",
+    "A listof Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Daftar Layanan Pemberitahuan dapat ditemukan di Home Assistant pada \"Developer Tools > Services\" cari \"notification\" lalu cari nama perangkat Anda.",
+    "Automations can optionally be triggered in Home Assistant:": "Otomatisasi dapat dipicu secara opsional di Home Assistant:",
+    "Trigger type:": "Trigger type:",
+    "Event type:": "Event type:",
+    "Event data:": "Event data:",
+    "Then choose an action, for example switch the scene to where an RGB light is red.": "Kemudian pilih tindakan, misalnya alihkan ke tempat dimana lampu RGB berwarna merah.",
+    "Frontend Version": "Versi Frontend",
+    "Frontend Version do not match backend version!": "Versi Frontend tidak sama dengan versi backend!",
+    "Base URL": "URL Dasar",
+    goAlertInfo: "GoAlert adalah aplikasi open source untuk penjadwalan panggilan, eskalasi otomatis dan pemberitahuan (seperti SMS atau panggilan suara). Secara otomatis melibatkan orang yang tepat, dengan cara yang benar, dan pada waktu yang tepat! {0}",
+    goAlertIntegrationKeyInfo: "Dapatkan kunci integrasi API generik untuk layanan dalam format ini \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" biasanya nilai parameter token dari URL yang disalin.",
+    goAlert: "GoAlert",
+    backupOutdatedWarning: "Usang: Karena banyak fitur ditambahkan dan fitur cadangan ini agak tidak terawat, itu tidak dapat menghasilkan atau memulihkan cadangan lengkap.",
+    backupRecommend: "Harap cadangkan volume atau folder data (./data/) secara langsung.",
 };

From 0ae801015696ac19b3c8c9c5cb7b0a12dcae754d Mon Sep 17 00:00:00 2001
From: Gilas Amalanda <chunkz.ring@gmail.com>
Date: Tue, 13 Sep 2022 21:37:58 +0700
Subject: [PATCH 025/134] chore: update typo for promosmsTypeFull at language
 id-ID

---
 src/languages/id-ID.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/languages/id-ID.js b/src/languages/id-ID.js
index 6f069f65..d05d59f5 100644
--- a/src/languages/id-ID.js
+++ b/src/languages/id-ID.js
@@ -293,7 +293,7 @@ export default {
     matrix: "Matrix",
     promosmsTypeEco: "SMS ECO - murah tapi lambat dan sering kelebihan beban. Terbatas hanya untuk penerima Polandia.",
     promosmsTypeFlash: "SMS FLASH - Pesan akan otomatis muncul di perangkat penerima. Terbatas hanya untuk penerima Polandia.",
-    promosmsTypeFull: "SMS FULL - SMS tingkat premium, Anda dapat menggunakan Nama Pengirim Anda (Anda harus mendaftarkan nama terlebih dahulu). DapatdiAndalkan untuk peringatan.",
+    promosmsTypeFull: "SMS FULL - SMS tingkat premium, Anda dapat menggunakan Nama Pengirim Anda (Anda harus mendaftarkan nama terlebih dahulu). Dapat diandalkan untuk peringatan.",
     promosmsTypeSpeed: "SMS SPEED - Prioritas tertinggi dalam sistem. Sangat cepat dan dapat diandalkan tetapi mahal (sekitar dua kali lipat dari harga SMS FULL).",
     promosmsPhoneNumber: "Nomor telepon (untuk penerima Polandia Anda dapat melewati kode area)",
     promosmsSMSSender: "Nama Pengirim SMS : Nama pra-registrasi atau salah satu bawaan: InfoSMS, Info SMS, MaxSMS, INFO, SMS",

From b673cfbe9436ef379f6d9bc5479828ccb78a8d21 Mon Sep 17 00:00:00 2001
From: Gilas Amalanda <chunkz.ring@gmail.com>
Date: Tue, 13 Sep 2022 21:38:58 +0700
Subject: [PATCH 026/134] chore: update typo for Tag with this name already
 exist at language id-ID

---
 src/languages/id-ID.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/languages/id-ID.js b/src/languages/id-ID.js
index d05d59f5..36ee0341 100644
--- a/src/languages/id-ID.js
+++ b/src/languages/id-ID.js
@@ -163,7 +163,7 @@ export default {
     "Show URI": "Lihat URI",
     Tags: "Tanda",
     "Add New below or Select...": "Tambahkan Baru di bawah atau Pilih...",
-    "Tag with this name already exist.": "Tandadengan nama ini sudah ada.",
+    "Tag with this name already exist.": "Tanda dengan nama ini sudah ada.",
     "Tag with this value already exist.": "Tanda dengan nilai ini sudah ada.",
     color: "warna",
     "value (optional)": "nilai (harus diisi)",

From db6fdf5e267138f5718cb28ec0606c7472e569be Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Wed, 14 Sep 2022 02:36:18 +0800
Subject: [PATCH 027/134] Update Project Plan URL

Migrated to the new GitHub Project
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index b104b2c1..496aed04 100644
--- a/README.md
+++ b/README.md
@@ -106,7 +106,7 @@ https://github.com/louislam/uptime-kuma/milestones
 
 Project Plan:
 
-https://github.com/louislam/uptime-kuma/projects/1
+https://github.com/users/louislam/projects/4/views/1
 
 ## ❤️ Sponsors
 

From 1c3da995e31038d48bc3d9a18b201be9b62457fa Mon Sep 17 00:00:00 2001
From: jakubenglicky <jakubenglicky@gmail.com>
Date: Thu, 15 Sep 2022 09:11:05 +0200
Subject: [PATCH 028/134] Add support notification via SMSManager

---
 server/notification-providers/smsmanager.js | 25 +++++++++++++++++
 server/notification.js                      |  2 ++
 src/components/notifications/SMSManager.vue | 31 +++++++++++++++++++++
 src/components/notifications/index.js       |  2 ++
 src/languages/cs-CZ.js                      |  3 ++
 5 files changed, 63 insertions(+)
 create mode 100644 server/notification-providers/smsmanager.js
 create mode 100644 src/components/notifications/SMSManager.vue

diff --git a/server/notification-providers/smsmanager.js b/server/notification-providers/smsmanager.js
new file mode 100644
index 00000000..833bff60
--- /dev/null
+++ b/server/notification-providers/smsmanager.js
@@ -0,0 +1,25 @@
+const NotificationProvider = require("./notification-provider");
+const axios = require("axios");
+
+class SMSManager extends NotificationProvider {
+
+    name = "SMSManager";
+
+    async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
+        try {
+            let data = {
+                apikey: notification.smsmanagerApiKey,
+                endpoint: "https://http-api.smsmanager.cz/Send",
+                message: msg.replace(/[^\x00-\x7F]/g, ""),
+                to: notification.numbers,
+                messageType: notification.messageType,
+            };
+            await axios.get(`${data.endpoint}?apikey=${data.apikey}&message=${data.message}&number=${data.to}&gateway=${data.messageType}`);
+            return "SMS sent sucessfully.";
+        } catch (error) {
+            this.throwGeneralAxiosError(error);
+        }
+    }
+}
+
+module.exports = SMSManager;
diff --git a/server/notification.js b/server/notification.js
index 3306a53a..6f71783b 100644
--- a/server/notification.js
+++ b/server/notification.js
@@ -39,6 +39,7 @@ const Telegram = require("./notification-providers/telegram");
 const Webhook = require("./notification-providers/webhook");
 const WeCom = require("./notification-providers/wecom");
 const GoAlert = require("./notification-providers/goalert");
+const SMSManager = require("./notification-providers/smsmanager");
 
 class Notification {
 
@@ -81,6 +82,7 @@ class Notification {
             new RocketChat(),
             new SerwerSMS(),
             new Signal(),
+            new SMSManager(),
             new Slack(),
             new SMTP(),
             new Stackfield(),
diff --git a/src/components/notifications/SMSManager.vue b/src/components/notifications/SMSManager.vue
new file mode 100644
index 00000000..25db624f
--- /dev/null
+++ b/src/components/notifications/SMSManager.vue
@@ -0,0 +1,31 @@
+<template>
+    <div class="mb-3">
+        <label for="smsmanager-key" class="form-label">API Key</label>
+        <div class="form-text">
+            {{ $t("SMSManager API Docs ") }}
+            <a href="https://smsmanager.cz/api/http#send" target="_blank">{{ $t("here") }}</a>
+        </div>
+        <input id="smsmanager-key" v-model="$parent.notification.smsmanagerApiKey" type="text" class="form-control">
+    </div>
+    <div class="mb-3">
+        <label for="smsmanager-numbers" class="form-label"> {{ $t("Recipients") }}</label>
+        <div class="form-text">
+            {{ $t("You can divide numbers with") }} <b>,</b> {{ $t("or") }} <b>;</b>
+        </div>
+        <input id="smsmanager-numbers" v-model="$parent.notification.numbers" type="text" class="form-control">
+    </div>
+    <div class="mb-3">
+        <label for="smsmanager-messageType" class="form-label">{{ $t("Gateway Type") }}</label>
+        <select id="smsmanager-messageType" v-model="$parent.notification.messageType" class="form-select">
+            <option value="economy">Economy</option>
+            <option value="lowcost">Lowcost</option>
+            <option value="high" selected>High</option>
+        </select>
+    </div>
+    <div class="mb-3">
+        <div class="form-text">
+            {{ $t("checkPrice", [$t("SMSManager")]) }}
+            <a href="https://smsmanager.cz/rozesilani-sms/ceny/ceska-republika/" target="_blank">{{ $t("here") }}</a>
+        </div>
+    </div>
+</template>
diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js
index e5570c80..07ed2cd1 100644
--- a/src/components/notifications/index.js
+++ b/src/components/notifications/index.js
@@ -28,6 +28,7 @@ import Pushy from "./Pushy.vue";
 import RocketChat from "./RocketChat.vue";
 import SerwerSMS from "./SerwerSMS.vue";
 import Signal from "./Signal.vue";
+import SMSManager from "./SMSManager.vue";
 import Slack from "./Slack.vue";
 import Stackfield from "./Stackfield.vue";
 import STMP from "./SMTP.vue";
@@ -75,6 +76,7 @@ const NotificationFormList = {
     "rocket.chat": RocketChat,
     "serwersms": SerwerSMS,
     "signal": Signal,
+    "SMSManager": SMSManager,
     "slack": Slack,
     "smtp": STMP,
     "stackfield": Stackfield,
diff --git a/src/languages/cs-CZ.js b/src/languages/cs-CZ.js
index b2b9331a..c71075be 100644
--- a/src/languages/cs-CZ.js
+++ b/src/languages/cs-CZ.js
@@ -576,4 +576,7 @@ export default {
     "Then choose an action, for example switch the scene to where an RGB light is red.": "Následně vyberte akci, například přepnutí scény z RGB světla na červenou.",
     "Frontend Version": "Verze frontendu",
     "Frontend Version do not match backend version!": "Verze frontendu neodpovídá verzi backendu!",
+    "You can divide numbers with": "Čísla můžete rozdělit pomocí ",
+    "or": "nebo",
+    "Gateway Type": "Typ brány",
 };

From 2d5096317fa3a90cfcb6618b6cc19bf305188d41 Mon Sep 17 00:00:00 2001
From: jakubenglicky <jakubenglicky@gmail.com>
Date: Thu, 15 Sep 2022 09:11:27 +0200
Subject: [PATCH 029/134] Fix warning at goalert.js

---
 server/notification-providers/goalert.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/notification-providers/goalert.js b/server/notification-providers/goalert.js
index c757e599..73e0375a 100644
--- a/server/notification-providers/goalert.js
+++ b/server/notification-providers/goalert.js
@@ -22,7 +22,7 @@ class GoAlert extends NotificationProvider {
             let config = {
                 headers: headers
             };
-            let resp = await axios.post(`${notification.goAlertBaseURL}/api/v2/generic/incoming?token=${notification.goAlertToken}`, data, config);
+            await axios.post(`${notification.goAlertBaseURL}/api/v2/generic/incoming?token=${notification.goAlertToken}`, data, config);
             return okMsg;
 
         } catch (error) {

From bec3b0d2dcdc410be32b3ce8c2fb1636aebf12a3 Mon Sep 17 00:00:00 2001
From: burakurer <58211081+burakurer@users.noreply.github.com>
Date: Fri, 16 Sep 2022 00:16:08 +0300
Subject: [PATCH 030/134] Update tr-TR.js

---
 src/languages/tr-TR.js | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/src/languages/tr-TR.js b/src/languages/tr-TR.js
index 606fa7e1..75bc103c 100644
--- a/src/languages/tr-TR.js
+++ b/src/languages/tr-TR.js
@@ -557,8 +557,8 @@ export default {
     "Docker Host": "Docker Ana Bilgisayarı",
     "Docker Hosts": "Docker Ana Bilgisayarları",
     "ntfy Topic": "ntfy Konu",
-    "Domain": "Domain",
-    "Workstation": "İş İstasyonu",
+    Domain: "Domain",
+    Workstation: "İş İstasyonu",
     disableCloudflaredNoAuthMsg: "Yetki Yok modundasınız, şifre gerekli değil.",
     trustProxyDescription: "'X-Forwarded-*' başlıklarına güvenin. Doğru istemci IP'sini almak istiyorsanız ve Uptime Kuma'nız Nginx veya Apache'nin arkasındaysa, bunu etkinleştirmelisiniz.",
     wayToGetLineNotifyToken: "{0} adresinden bir erişim jetonu alabilirsiniz.",
@@ -576,4 +576,10 @@ export default {
     "Then choose an action, for example switch the scene to where an RGB light is red.": "Ardından bir eylem seçin, örneğin RGB ışığının kırmızı olduğu sahneyi değiştirin.",
     "Frontend Version": "Frontend Sürümü",
     "Frontend Version do not match backend version!": "Frontend Sürümü, backend sürümüyle eşleşmiyor!",
+    "Base URL": "Temel URL",
+    goAlertInfo: "GoAlert, çağrı üzerine zamanlama, otomatik eskalasyonlar ve bildirimler (SMS veya sesli çağrılar gibi) için açık kaynaklı bir uygulamadır. Doğru kişiyi, doğru şekilde ve doğru zamanda otomatik olarak devreye sokun! {0}",
+    goAlertIntegrationKeyInfo: "Servis için genel API entegrasyon anahtarını, genellikle kopyalanan URL'nin belirteç parametresinin değeri olan \"aaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" biçiminde alın.",
+    goAlert: "GoAlert",
+    backupOutdatedWarning: "Kullanımdan Kaldırıldı: Birçok özellik eklendiğinden ve bu yedekleme özelliği biraz bakımsız olduğundan, tam bir yedekleme oluşturamaz veya geri yükleyemez.",
+    backupRecommend: "Lütfen bunun yerine birimi veya veri klasörünü (./data/) doğrudan yedekleyin.",
 };

From 38c45a3fe3fe35557f8046d268426686595032af Mon Sep 17 00:00:00 2001
From: Super Manito <68613938+SuperManito@users.noreply.github.com>
Date: Fri, 16 Sep 2022 14:21:22 +0800
Subject: [PATCH 031/134] Fix previously PR bug about Bark Notification (#2084)

Co-authored-by: zuosc <zorro.zsc@hotmail.com>
---
 server/notification-providers/bark.js | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/server/notification-providers/bark.js b/server/notification-providers/bark.js
index 3258e7c5..bcd3c682 100644
--- a/server/notification-providers/bark.js
+++ b/server/notification-providers/bark.js
@@ -28,17 +28,17 @@ class Bark extends NotificationProvider {
 
         if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
             let title = "UptimeKuma Monitor Up";
-            return await this.postNotification(title, msg, barkEndpoint);
+            return await this.postNotification(notification, title, msg, barkEndpoint);
         }
 
         if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
             let title = "UptimeKuma Monitor Down";
-            return await this.postNotification(title, msg, barkEndpoint);
+            return await this.postNotification(notification, title, msg, barkEndpoint);
         }
 
         if (msg != null) {
             let title = "UptimeKuma Message";
-            return await this.postNotification(title, msg, barkEndpoint);
+            return await this.postNotification(notification, title, msg, barkEndpoint);
         }
     }
 
@@ -50,7 +50,7 @@ class Bark extends NotificationProvider {
      */
     appendAdditionalParameters(notification, postUrl) {
         // set icon to uptime kuma icon, 11kb should be fine
-        postUrl += "&icon=" + barkNotificationAvatar;
+        postUrl += "?icon=" + barkNotificationAvatar;
         // grouping all our notifications
         if (notification.barkGroup != null) {
             postUrl += "&group=" + notification.barkGroup;
@@ -89,12 +89,12 @@ class Bark extends NotificationProvider {
      * @param {string} endpoint Endpoint to send request to
      * @returns {string}
      */
-    async postNotification(title, subtitle, endpoint) {
+    async postNotification(notification, title, subtitle, endpoint) {
         // url encode title and subtitle
         title = encodeURIComponent(title);
         subtitle = encodeURIComponent(subtitle);
         let postUrl = endpoint + "/" + title + "/" + subtitle;
-        postUrl = this.appendAdditionalParameters(postUrl);
+        postUrl = this.appendAdditionalParameters(notification, postUrl);
         let result = await axios.get(postUrl);
         this.checkResult(result);
         if (result.statusText != null) {

From d23085cddce1c404c3f4c80c5bf0b5c61b0227b8 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 17 Sep 2022 01:18:31 +0800
Subject: [PATCH 032/134] Fix #2100, the monitor name cannot display if too
 long

---
 src/assets/app.scss | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/assets/app.scss b/src/assets/app.scss
index f5e07887..bf8e7004 100644
--- a/src/assets/app.scss
+++ b/src/assets/app.scss
@@ -382,7 +382,7 @@ optgroup {
         overflow-y: auto;
         height: calc(100% - 65px);
     }
-    
+
     @media (max-width: 770px) {
         &.scrollbar {
             height: calc(100% - 40px);
@@ -403,7 +403,6 @@ optgroup {
         .info {
             white-space: nowrap;
             overflow: hidden;
-            text-overflow: ellipsis;
         }
 
         &:hover {

From 1c4e97439ce74efb92f9dfbc097c8b8511ea403f Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 17 Sep 2022 01:59:25 +0800
Subject: [PATCH 033/134] Fix pr-test

---
 extra/checkout-pr.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/extra/checkout-pr.js b/extra/checkout-pr.js
index d327e203..0328770b 100644
--- a/extra/checkout-pr.js
+++ b/extra/checkout-pr.js
@@ -27,7 +27,7 @@ result = childProcess.spawnSync("git", [ "fetch", name, branch ]);
 console.log(result.stdout.toString());
 console.error(result.stderr.toString());
 
-result = childProcess.spawnSync("git", [ "checkout", branch, "--force" ]);
+result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ]);
 
 console.log(result.stdout.toString());
 console.error(result.stderr.toString());

From 2f67d267025ad6cb583291d394fe5190b5596313 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 17 Sep 2022 16:20:10 +0800
Subject: [PATCH 034/134] Merge manually, as this part had been moved

---
 server/model/status_page.js | 35 ++++++++++++++++++++++++++++++++++-
 1 file changed, 34 insertions(+), 1 deletion(-)

diff --git a/server/model/status_page.js b/server/model/status_page.js
index 82d184bf..d296470d 100644
--- a/server/model/status_page.js
+++ b/server/model/status_page.js
@@ -83,6 +83,8 @@ class StatusPage extends BeanModel {
             incident = incident.toPublicJSON();
         }
 
+        let maintenance = await StatusPage.getMaintenanceList(statusPage.id);
+
         // Public Group List
         const publicGroupList = [];
         const showTags = !!statusPage.show_tags;
@@ -100,7 +102,8 @@ class StatusPage extends BeanModel {
         return {
             config: await statusPage.toPublicJSON(),
             incident,
-            publicGroupList
+            publicGroupList,
+            maintenance,
         };
     }
 
@@ -259,6 +262,36 @@ class StatusPage extends BeanModel {
         }
     }
 
+    /**
+     * Get list of maintenances
+     * @param {number} statusPageId ID of status page to get maintenance for
+     * @returns {Object} Object representing maintenances sanitized for public
+     */
+    static async getMaintenanceList(statusPageId) {
+        try {
+            const publicMaintenanceList = [];
+
+            let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
+            SELECT m.*
+            FROM maintenance m
+            JOIN maintenance_status_page msp
+            ON msp.maintenance_id = m.id
+            WHERE datetime(m.start_date) <= datetime('now')
+              AND datetime(m.end_date) >= datetime('now')
+              AND msp.status_page_id = ?
+            ORDER BY m.end_date
+        `, [ statusPageId ]));
+
+            for (const bean of maintenanceBeanList) {
+                publicMaintenanceList.push(await bean.toPublicJSON());
+            }
+
+            return publicMaintenanceList;
+
+        } catch (error) {
+            return [];
+        }
+    }
 }
 
 module.exports = StatusPage;

From 7017c2e62502c6272c080a5f477f153cc8d65297 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 17 Sep 2022 16:54:21 +0800
Subject: [PATCH 035/134] Move maintenance code to
 `maintenance-socket-handler.js`

---
 server/server.js                              | 264 +-----------------
 .../maintenance-socket-handler.js             | 235 ++++++++++++++++
 server/uptime-kuma-server.js                  |  37 +++
 3 files changed, 275 insertions(+), 261 deletions(-)
 create mode 100644 server/socket-handlers/maintenance-socket-handler.js

diff --git a/server/server.js b/server/server.js
index b9363109..8d375b62 100644
--- a/server/server.js
+++ b/server/server.js
@@ -127,7 +127,7 @@ const StatusPage = require("./model/status_page");
 const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
 const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
 const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
-const apicache = require("./modules/apicache");
+const { maintenanceSocketHandler } = require("./socket-handlers/maintenance-socket-handler");
 
 app.use(express.json());
 
@@ -146,12 +146,6 @@ app.use(function (req, res, next) {
  */
 let jwtSecret = null;
 
-/**
-* Main maintenance list
-* @type {{}}
-*/
-let maintenanceList = {};
-
 /**
  * Show Setup Page
  * @type {boolean}
@@ -733,135 +727,6 @@ let needSetup = false;
             }
         });
 
-        // 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,
-                });
-            }
-        });
-
-        // Add a new monitor_maintenance
-        socket.on("addMaintenanceStatusPage", async (maintenanceID, statusPages, callback) => {
-            try {
-                checkLogin(socket);
-
-                await R.exec("DELETE FROM maintenance_status_page WHERE maintenance_id = ?", [
-                    maintenanceID
-                ]);
-
-                for await (const statusPage of statusPages) {
-                    let bean = R.dispense("maintenance_status_page");
-
-                    bean.import({
-                        status_page_id: statusPage.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) => {
             try {
                 checkLogin(socket);
@@ -878,22 +743,6 @@ let needSetup = false;
             }
         });
 
-        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) => {
             try {
                 checkLogin(socket);
@@ -942,54 +791,6 @@ let needSetup = false;
             }
         });
 
-        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("getMaintenanceStatusPage", async (maintenanceID, callback) => {
-            try {
-                checkLogin(socket);
-
-                console.log(`Get Status Pages for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
-
-                let statusPages = await R.getAll("SELECT status_page.id, status_page.title FROM maintenance_status_page msp JOIN status_page ON msp.status_page_id = status_page.id WHERE msp.maintenance_id = ? ", [
-                    maintenanceID,
-                ]);
-
-                callback({
-                    ok: true,
-                    statusPages,
-                });
-
-            } catch (e) {
-                console.error(e);
-                callback({
-                    ok: false,
-                    msg: e.message,
-                });
-            }
-        });
-
         socket.on("getMonitorBeats", async (monitorID, period, callback) => {
             try {
                 checkLogin(socket);
@@ -1094,36 +895,6 @@ let needSetup = false;
             }
         });
 
-        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) => {
             try {
                 checkLogin(socket);
@@ -1704,6 +1475,7 @@ let needSetup = false;
         databaseSocketHandler(socket);
         proxySocketHandler(socket);
         dockerSocketHandler(socket);
+        maintenanceSocketHandler(socket);
 
         log.debug("server", "added all socket handlers");
 
@@ -1794,17 +1566,6 @@ async function checkOwner(userID, monitorID) {
     }
 }
 
-/**
- * Send maintenance list to client
- * @param {Socket} socket Socket.io instance to send to
- * @returns {Object}
- */
-async function sendMaintenanceList(socket) {
-    let list = await getMaintenanceJSONList(socket.userID);
-    io.to(socket.userID).emit("maintenanceList", list);
-    return list;
-}
-
 /**
  * Function called after user login
  * This function is used to send the heartbeat list of a monitor.
@@ -1817,7 +1578,7 @@ async function afterLogin(socket, user) {
     socket.join(user.id);
 
     let monitorList = await server.sendMonitorList(socket);
-    sendMaintenanceList(socket);
+    server.sendMaintenanceList(socket);
     sendNotificationList(socket);
     sendProxyList(socket);
     sendDockerHostList(socket);
@@ -1839,25 +1600,6 @@ async function afterLogin(socket, user) {
     }
 }
 
-/**
- * Get a list of maintenances for the given user.
- * @param {string} userID - The ID of the user to get maintenances for.
- * @returns {Promise<Object>} A promise that resolves to an object with maintenance IDs as keys and maintenances objects as values.
- */
-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;
-}
-
 /**
  * Initialize the database
  * @param {boolean} [testMode=false] Should the connection be
diff --git a/server/socket-handlers/maintenance-socket-handler.js b/server/socket-handlers/maintenance-socket-handler.js
new file mode 100644
index 00000000..e93d8d60
--- /dev/null
+++ b/server/socket-handlers/maintenance-socket-handler.js
@@ -0,0 +1,235 @@
+const { checkLogin } = require("../util-server");
+const { log } = require("../../src/util");
+const { R } = require("redbean-node");
+const apicache = require("../modules/apicache");
+const { UptimeKumaServer } = require("../uptime-kuma-server");
+const server = UptimeKumaServer.getInstance();
+
+/**
+ * Handlers for Maintenance
+ * @param {Socket} socket Socket.io instance
+ */
+module.exports.maintenanceSocketHandler = (socket) => {
+    // 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 server.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 server.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,
+            });
+        }
+    });
+
+    // Add a new monitor_maintenance
+    socket.on("addMaintenanceStatusPage", async (maintenanceID, statusPages, callback) => {
+        try {
+            checkLogin(socket);
+
+            await R.exec("DELETE FROM maintenance_status_page WHERE maintenance_id = ?", [
+                maintenanceID
+            ]);
+
+            for await (const statusPage of statusPages) {
+                let bean = R.dispense("maintenance_status_page");
+
+                bean.import({
+                    status_page_id: statusPage.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("getMaintenanceList", async (callback) => {
+        try {
+            checkLogin(socket);
+            await server.sendMaintenanceList(socket);
+            callback({
+                ok: true,
+            });
+        } catch (e) {
+            console.error(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("getMaintenanceStatusPage", async (maintenanceID, callback) => {
+        try {
+            checkLogin(socket);
+
+            console.log(`Get Status Pages for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+
+            let statusPages = await R.getAll("SELECT status_page.id, status_page.title FROM maintenance_status_page msp JOIN status_page ON msp.status_page_id = status_page.id WHERE msp.maintenance_id = ? ", [
+                maintenanceID,
+            ]);
+
+            callback({
+                ok: true,
+                statusPages,
+            });
+
+        } catch (e) {
+            console.error(e);
+            callback({
+                ok: false,
+                msg: e.message,
+            });
+        }
+    });
+
+    socket.on("deleteMaintenance", async (maintenanceID, callback) => {
+        try {
+            checkLogin(socket);
+
+            console.log(`Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+
+            if (maintenanceID in server.maintenanceList) {
+                delete server.maintenanceList[maintenanceID];
+            }
+
+            await R.exec("DELETE FROM maintenance WHERE id = ? AND user_id = ? ", [
+                maintenanceID,
+                socket.userID,
+            ]);
+
+            callback({
+                ok: true,
+                msg: "Deleted Successfully.",
+            });
+
+            await server.sendMaintenanceList(socket);
+
+        } catch (e) {
+            callback({
+                ok: false,
+                msg: e.message,
+            });
+        }
+    });
+};
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index 98de65a4..a8a36593 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -26,6 +26,13 @@ class UptimeKumaServer {
      * @type {{}}
      */
     monitorList = {};
+
+    /**
+     * Main maintenance list
+     * @type {{}}
+     */
+    maintenanceList = {};
+
     entryPage = "dashboard";
     app = undefined;
     httpServer = undefined;
@@ -104,6 +111,36 @@ class UptimeKumaServer {
         return result;
     }
 
+    /**
+     * Send maintenance list to client
+     * @param {Socket} socket Socket.io instance to send to
+     * @returns {Object}
+     */
+    async sendMaintenanceList(socket) {
+        let list = await this.getMaintenanceJSONList(socket.userID);
+        this.io.to(socket.userID).emit("maintenanceList", list);
+        return list;
+    }
+
+    /**
+     * Get a list of maintenances for the given user.
+     * @param {string} userID - The ID of the user to get maintenances for.
+     * @returns {Promise<Object>} A promise that resolves to an object with maintenance IDs as keys and maintenances objects as values.
+     */
+    async 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;
+    }
+
     /**
      * Write error to log file
      * @param {any} error The error to write

From 120e5783989923810bd9f6d92f24aa500783419e Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 17 Sep 2022 16:58:08 +0800
Subject: [PATCH 036/134] Move maintenance code to
 `maintenance-socket-handler.js`

---
 server/server.js                              | 24 ---------------
 .../maintenance-socket-handler.js             | 30 +++++++++++++++++--
 2 files changed, 27 insertions(+), 27 deletions(-)

diff --git a/server/server.js b/server/server.js
index 8d375b62..4aec2b27 100644
--- a/server/server.js
+++ b/server/server.js
@@ -767,30 +767,6 @@ let needSetup = false;
             }
         });
 
-        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("getMonitorBeats", async (monitorID, period, callback) => {
             try {
                 checkLogin(socket);
diff --git a/server/socket-handlers/maintenance-socket-handler.js b/server/socket-handlers/maintenance-socket-handler.js
index e93d8d60..113c336a 100644
--- a/server/socket-handlers/maintenance-socket-handler.js
+++ b/server/socket-handlers/maintenance-socket-handler.js
@@ -139,6 +139,30 @@ module.exports.maintenanceSocketHandler = (socket) => {
         }
     });
 
+    socket.on("getMaintenance", async (maintenanceID, callback) => {
+        try {
+            checkLogin(socket);
+
+            log.debug("maintenance", `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("getMaintenanceList", async (callback) => {
         try {
             checkLogin(socket);
@@ -159,7 +183,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
         try {
             checkLogin(socket);
 
-            console.log(`Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+            log.debug("maintenance", `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,
@@ -183,7 +207,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
         try {
             checkLogin(socket);
 
-            console.log(`Get Status Pages for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+            log.debug("maintenance", `Get Status Pages for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
 
             let statusPages = await R.getAll("SELECT status_page.id, status_page.title FROM maintenance_status_page msp JOIN status_page ON msp.status_page_id = status_page.id WHERE msp.maintenance_id = ? ", [
                 maintenanceID,
@@ -207,7 +231,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
         try {
             checkLogin(socket);
 
-            console.log(`Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+            log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
 
             if (maintenanceID in server.maintenanceList) {
                 delete server.maintenanceList[maintenanceID];

From bb883e6fa0d63d8fcd7345439a5c65880c3ea8b1 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 17 Sep 2022 22:00:11 +0800
Subject: [PATCH 037/134] Move maintenance under `/maintenance`

---
 src/components/MonitorList.vue   | 137 ++++---------------------
 src/layouts/Layout.vue           |  20 ++--
 src/pages/Dashboard.vue          |  38 +------
 src/pages/EditMaintenance.vue    |  31 ++----
 src/pages/MaintenanceDetails.vue |   4 +-
 src/pages/ManageMaintenance.vue  | 168 +++++++++++++++++++++++++++++++
 src/router.js                    |  41 ++++----
 src/util.js                      |   8 +-
 src/util.ts                      |   2 +-
 9 files changed, 240 insertions(+), 209 deletions(-)
 create mode 100644 src/pages/ManageMaintenance.vue

diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue
index 8e1b678b..1c0e3b35 100644
--- a/src/components/MonitorList.vue
+++ b/src/components/MonitorList.vue
@@ -1,13 +1,7 @@
 <template>
     <div class="shadow-box mb-3" :style="boxStyle">
         <div class="list-header">
-            <div class="search-wrapper float-start" style="margin-left: 5px;">
-                <font-awesome-icon icon="filter" />
-                <select v-model="selectedList" class="form-control" style="margin-left: 5px;">
-                    <option value="monitor" selected>{{ $t('Monitors') }}</option>
-                    <option value="maintenance">{{ $t('Maintenance') }}</option>
-                </select>
-            </div>
+            <div class="placeholder"></div>
             <div class="search-wrapper">
                 <a v-if="searchText == ''" class="search-icon">
                     <font-awesome-icon icon="search" />
@@ -21,62 +15,32 @@
             </div>
         </div>
         <div class="monitor-list" :class="{ scrollbar: scrollbar }">
-            <div v-if="Object.keys($root.monitorList).length === 0 && selectedList === 'monitor'" class="text-center mt-3">
+            <div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
                 {{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
             </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>
 
-            <template v-if="selectedList === 'maintenance'">
-                <router-link
-                    v-for="(item, index) in sortedMaintenanceList" :key="index" :to="maintenanceURL(item.id)"
-                    class="item" :class="{ 'disabled': !$root.isActiveMaintenance(item.end_date) }"
-                >
-                    <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>
+            <router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
+                <div class="row">
+                    <div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
+                        <div class="info">
+                            <Uptime :monitor="item" type="24" :pill="true" />
+                            {{ item.name }}
+                        </div>
+                        <div class="tags">
+                            <Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
                         </div>
                     </div>
-                </router-link>
-            </template>
-
-            <template v-if="selectedList === 'monitor'">
-                <router-link
-                    v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)"
-                    class="item" :class="{ 'disabled': ! item.active }"
-                >
-                    <div class="row">
-                        <div
-                            class="col-9 col-md-8 small-padding"
-                            :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }"
-                        >
-                            <div class="info">
-                                <Uptime :monitor="item" type="24" :pill="true" />
-                                {{ item.name }}
-                            </div>
-                            <div class="tags">
-                                <Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
-                            </div>
-                        </div>
-                        <div
-                            v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar"
-                            class="col-3 col-md-4"
-                        >
-                            <HeartbeatBar size="small" :monitor-id="item.id" />
-                        </div>
+                    <div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
+                        <HeartbeatBar size="small" :monitor-id="item.id" />
                     </div>
+                </div>
 
-                    <div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
-                        <div class="col-12 bottom-style">
-                            <HeartbeatBar size="small" :monitor-id="item.id" />
-                        </div>
+                <div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
+                    <div class="col-12 bottom-style">
+                        <HeartbeatBar size="small" :monitor-id="item.id" />
                     </div>
-                </router-link>
-            </template>
+                </div>
+            </router-link>
         </div>
     </div>
 </template>
@@ -85,7 +49,7 @@
 import HeartbeatBar from "../components/HeartbeatBar.vue";
 import Tag from "../components/Tag.vue";
 import Uptime from "../components/Uptime.vue";
-import { getMaintenanceRelativeURL, getMonitorRelativeURL } from "../util.ts";
+import { getMonitorRelativeURL } from "../util.ts";
 
 export default {
     components: {
@@ -102,7 +66,6 @@ export default {
     data() {
         return {
             searchText: "",
-            selectedList: "monitor",
             windowTop: 0,
         };
     },
@@ -125,56 +88,6 @@ export default {
 
         },
 
-        sortedMaintenanceList() {
-            let result = Object.values(this.$root.maintenanceList);
-
-            result.sort((m1, m2) => {
-
-                if (this.$root.isActiveMaintenance(m1.end_date) !== this.$root.isActiveMaintenance(m2.end_date)) {
-                    if (!this.$root.isActiveMaintenance(m2.end_date)) {
-                        return -1;
-                    }
-                    if (!this.$root.isActiveMaintenance(m1.end_date)) {
-                        return 1;
-                    }
-                }
-
-                if (this.$root.isActiveMaintenance(m1.end_date) && this.$root.isActiveMaintenance(m2.end_date)) {
-                    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 (!this.$root.isActiveMaintenance(m1.end_date) && !this.$root.isActiveMaintenance(m2.end_date)) {
-                    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() {
             let result = Object.values(this.$root.monitorList);
 
@@ -240,9 +153,6 @@ export default {
         monitorURL(id) {
             return getMonitorRelativeURL(id);
         },
-        maintenanceURL(id) {
-            return getMaintenanceRelativeURL(id);
-        },
         /** Clear the search bar */
         clearSearchText() {
             this.searchText = "";
@@ -319,11 +229,4 @@ export default {
     margin-top: 5px;
 }
 
-.bg-maintenance {
-    background-color: $maintenance;
-}
-
-select {
-    text-align: center;
-}
 </style>
diff --git a/src/layouts/Layout.vue b/src/layouts/Layout.vue
index 56e1813d..7ece4982 100644
--- a/src/layouts/Layout.vue
+++ b/src/layouts/Layout.vue
@@ -37,19 +37,32 @@
                             <div class="profile-pic">{{ $root.usernameFirstChar }}</div>
                             <font-awesome-icon icon="angle-down" />
                         </div>
+
+                        <!-- Header's Dropdown Menu -->
                         <ul class="dropdown-menu">
+                            <!-- Username -->
                             <li>
                                 <i18n-t v-if="$root.username != null" tag="span" keypath="signedInDisp" class="dropdown-item-text">
                                     <strong>{{ $root.username }}</strong>
                                 </i18n-t>
                                 <span v-if="$root.username == null" class="dropdown-item-text">{{ $t("signedInDispDisabled") }}</span>
                             </li>
+
                             <li><hr class="dropdown-divider"></li>
+
+                            <!-- Functions -->
+                            <li>
+                                <router-link to="/maintenance" class="dropdown-item" :class="{ active: $route.path.includes('manage-maintenance') }">
+                                    <font-awesome-icon icon="wrench" /> {{ $t("Maintenance") }}
+                                </router-link>
+                            </li>
+
                             <li>
                                 <router-link to="/settings" class="dropdown-item" :class="{ active: $route.path.includes('settings') }">
                                     <font-awesome-icon icon="cog" /> {{ $t("Settings") }}
                                 </router-link>
                             </li>
+
                             <li v-if="$root.loggedIn && $root.socket.token !== 'autoLogin'">
                                 <button class="dropdown-item" @click="$root.logout">
                                     <font-awesome-icon icon="sign-out-alt" />
@@ -77,7 +90,7 @@
 
         <!-- Mobile Only -->
         <div v-if="$root.isMobile" style="width: 100%; height: 60px;" />
-        <nav v-if="$root.isMobile && $root.loggedIn" class="bottom-nav scroll">
+        <nav v-if="$root.isMobile && $root.loggedIn" class="bottom-nav">
             <router-link to="/dashboard" class="nav-link">
                 <div><font-awesome-icon icon="tachometer-alt" /></div>
                 {{ $t("Dashboard") }}
@@ -93,11 +106,6 @@
                 {{ $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 to="/settings" class="nav-link">
                 <div><font-awesome-icon icon="cog" /></div>
                 {{ $t("Settings") }}
diff --git a/src/pages/Dashboard.vue b/src/pages/Dashboard.vue
index e2a5275f..20425c61 100644
--- a/src/pages/Dashboard.vue
+++ b/src/pages/Dashboard.vue
@@ -2,23 +2,7 @@
     <div class="container-fluid">
         <div class="row">
             <div v-if="!$root.isMobile" class="col-12 col-md-5 col-xl-4">
-                <div class="dropdown dropdown-create">
-                    <button class="btn btn-primary mb-3 dropdown-toggle" type="button" data-bs-toggle="dropdown">
-                        <font-awesome-icon icon="plus" /> {{ $t("Create") }}
-                    </button>
-                    <ul class="dropdown-menu dropdown-menu-end">
-                        <li>
-                            <button type="button" class="dropdown-item" @click="$router.push('/add')">
-                                <font-awesome-icon icon="heartbeat" /> {{ $t("Monitor") }}
-                            </button>
-                        </li>
-                        <li>
-                            <button type="button" class="dropdown-item" @click="$router.push('/addMaintenance')">
-                                <font-awesome-icon icon="wrench" /> {{ $t("Maintenance") }}
-                            </button>
-                        </li>
-                    </ul>
-                </div>
+                <router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
                 <MonitorList :scrollbar="true" />
             </div>
 
@@ -45,8 +29,6 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-@import "../assets/vars.scss";
-
 .container-fluid {
     width: 98%;
 }
@@ -55,22 +37,4 @@ export default {
     display: flex;
     justify-content: end;
 }
-
-.dark {
-    .dropdown-create {
-        ul {
-            background-color: $dark-bg;
-            border-color: $dark-bg2;
-            border-width: 2px;
-
-            li button {
-                color: $dark-font-color;
-            }
-
-            li button:hover {
-                background-color: $dark-bg2;
-            }
-        }
-    }
-}
 </style>
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 281f1241..604bf89b 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -6,14 +6,12 @@
                 <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
+                                    required
                                 >
                             </div>
 
@@ -22,7 +20,6 @@
                                 <label for="description" class="form-label">{{ $t("Description") }}</label>
                                 <textarea
                                     id="description" v-model="maintenance.description" class="form-control"
-                                    :placeholder="descriptionPlaceholder"
                                 ></textarea>
                             </div>
 
@@ -54,8 +51,7 @@
 
                             <!-- Start Date Time -->
                             <div class="my-3">
-                                <label for="start_date" class="form-label">{{ $t("Start of maintenance") }}
-                                    ({{ $root.timezone }})</label>
+                                <label for="start_date" class="form-label">{{ $t("Start Date") }}</label>
                                 <input
                                     id="start_date" v-model="maintenance.start_date" :type="'datetime-local'"
                                     class="form-control" :class="{'dark-calendar': dark }" required
@@ -64,8 +60,7 @@
 
                             <!-- End Date Time -->
                             <div class="my-3">
-                                <label for="end_date" class="form-label">{{ $t("Expected end of maintenance") }}
-                                    ({{ $root.timezone }})</label>
+                                <label for="end_date" class="form-label">{{ $t("End Date") }}</label>
                                 <input
                                     id="end_date" v-model="maintenance.end_date" :type="'datetime-local'"
                                     class="form-control" :class="{'dark-calendar': dark }" required
@@ -79,14 +74,14 @@
                                     type="checkbox"
                                 >
                                 <label class="form-check-label" for="show-powered-by">{{
-                                    $t("Show on all pages")
+                                    $t("Show this Maintenance Message on ALL Status Pages")
                                 }}</label>
                             </div>
 
                             <!-- Status pages to display maintenance info on -->
                             <div v-if="!showOnAllPages" class="my-3">
                                 <label for="selected_status_pages" class="form-label">{{
-                                    $t("Selected status pages")
+                                    $t("Show this Maintenance Message on which Status Pages")
                                 }}</label>
 
                                 <VueMultiselect
@@ -155,25 +150,17 @@ export default {
     computed: {
 
         pageName() {
-            return this.$t((this.isAdd) ? "Schedule maintenance" : "Edit");
+            return this.$t((this.isAdd) ? "Schedule Maintenance" : "Edit Maintenance");
         },
 
         isAdd() {
-            return this.$route.path === "/addMaintenance";
+            return this.$route.path === "/add-maintenance";
         },
 
         isEdit() {
-            return this.$route.path.startsWith("/editMaintenance");
+            return this.$route.path.startsWith("/maintenance/edit");
         },
 
-        titlePlaceholder() {
-            return this.$t("maintenanceTitleExample");
-        },
-
-        descriptionPlaceholder() {
-            return this.$t("maintenanceDescriptionExample");
-        }
-
     },
     watch: {
         "$route.fullPath"() {
@@ -281,7 +268,7 @@ export default {
                                 toast.success(res.msg);
                                 this.processing = false;
                                 this.$root.getMaintenanceList();
-                                this.$router.push("/dashboard/maintenance/" + res.maintenanceID);
+                                this.$router.push("/maintenance/" + res.maintenanceID);
                             });
                         });
                     } else {
diff --git a/src/pages/MaintenanceDetails.vue b/src/pages/MaintenanceDetails.vue
index 432e7c3f..947a89b9 100644
--- a/src/pages/MaintenanceDetails.vue
+++ b/src/pages/MaintenanceDetails.vue
@@ -9,7 +9,7 @@
             </p>
 
             <div class="functions" style="margin-top: 10px;">
-                <router-link :to=" '/editMaintenance/' + maintenance.id " class="btn btn-secondary">
+                <router-link :to=" '/maintenance/edit/' + maintenance.id " class="btn btn-secondary">
                     <font-awesome-icon icon="edit" /> {{ $t("Edit") }}
                 </router-link>
                 <button class="btn btn-danger" @click="deleteDialog">
@@ -27,7 +27,7 @@
             </button>
             <br />
 
-            <label for="selected_status_pages" class="form-label" style="margin-top: 20px;">{{ $t("Selected status pages") }}</label>
+            <label for="selected_status_pages" class="form-label" style="margin-top: 20px;">{{ $t("Show this Maintenance Message on which Status Pages") }}</label>
             <br>
             <button v-for="statusPage in selectedStatusPages" :key="statusPage.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: 500;">
                 {{ statusPage }}
diff --git a/src/pages/ManageMaintenance.vue b/src/pages/ManageMaintenance.vue
new file mode 100644
index 00000000..0041f3d2
--- /dev/null
+++ b/src/pages/ManageMaintenance.vue
@@ -0,0 +1,168 @@
+<template>
+    <transition name="slide-fade" appear>
+        <div>
+            <h1 class="mb-3">
+                {{ $t("Maintenance") }}
+            </h1>
+
+            <div>
+                <router-link to="/add-maintenance" class="btn btn-primary mb-3">
+                    <font-awesome-icon icon="plus" /> {{ $t("Schedule Maintenance") }}
+                </router-link>
+            </div>
+
+            <div class="shadow-box">
+                <span v-if="Object.keys(sortedMaintenanceList).length === 0" class="d-flex align-items-center justify-content-center my-3">
+                    {{ $t("No maintenance") }}
+                </span>
+
+                <router-link
+                    v-for="(item, index) in sortedMaintenanceList"
+                    :key="index"
+                    :to="maintenanceURL(item.id)"
+                    class="item"
+                    :class="{ 'disabled': !$root.isActiveMaintenance(item.end_date) }"
+                >
+                    <div>
+                    </div>
+                    <div class="info">
+                        <div class="title">{{ item.title }}</div>
+                        <div>{{ item.description }}</div>
+                    </div>
+                </router-link>
+            </div>
+        </div>
+    </transition>
+</template>
+
+<script>
+import { getResBaseURL } from "../util-frontend";
+import { getMaintenanceRelativeURL } from "../util.ts";
+
+export default {
+    components: {
+    },
+    data() {
+        return {
+        };
+    },
+    computed: {
+        sortedMaintenanceList() {
+            let result = Object.values(this.$root.maintenanceList);
+
+            result.sort((m1, m2) => {
+
+                if (this.$root.isActiveMaintenance(m1.end_date) !== this.$root.isActiveMaintenance(m2.end_date)) {
+                    if (!this.$root.isActiveMaintenance(m2.end_date)) {
+                        return -1;
+                    }
+                    if (!this.$root.isActiveMaintenance(m1.end_date)) {
+                        return 1;
+                    }
+                }
+
+                if (this.$root.isActiveMaintenance(m1.end_date) && this.$root.isActiveMaintenance(m2.end_date)) {
+                    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 (!this.$root.isActiveMaintenance(m1.end_date) && !this.$root.isActiveMaintenance(m2.end_date)) {
+                    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);
+            });
+
+            return result;
+        },
+    },
+    mounted() {
+
+    },
+    methods: {
+        /**
+         * Get the correct URL for the icon
+         * @param {string} icon Path for icon
+         * @returns {string} Correctly formatted path including port numbers
+         */
+        icon(icon) {
+            if (icon === "/icon.svg") {
+                return icon;
+            } else {
+                return getResBaseURL() + icon;
+            }
+        },
+
+        maintenanceURL(id) {
+            return getMaintenanceRelativeURL(id);
+        },
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+    @import "../assets/vars.scss";
+
+    .item {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+        text-decoration: none;
+        border-radius: 10px;
+        transition: all ease-in-out 0.15s;
+        padding: 10px;
+        min-height: 90px;
+
+        &:hover {
+            background-color: $highlight-white;
+        }
+
+        &.active {
+            background-color: #cdf8f4;
+        }
+
+        $logo-width: 70px;
+
+        .logo {
+            width: $logo-width;
+            height: $logo-width;
+
+            // Better when the image is loading
+            min-height: 1px;
+        }
+
+        .info {
+            .title {
+                font-weight: bold;
+                font-size: 20px;
+            }
+
+            .slug {
+                font-size: 14px;
+            }
+        }
+    }
+
+    .dark {
+        .item {
+            &:hover {
+                background-color: $dark-bg2;
+            }
+
+            &.active {
+                background-color: $dark-bg2;
+            }
+        }
+    }
+</style>
diff --git a/src/router.js b/src/router.js
index f58e7ba7..a3414774 100644
--- a/src/router.js
+++ b/src/router.js
@@ -15,6 +15,9 @@ import Entry from "./pages/Entry.vue";
 import ManageStatusPage from "./pages/ManageStatusPage.vue";
 import AddStatusPage from "./pages/AddStatusPage.vue";
 import NotFound from "./pages/NotFound.vue";
+import DockerHosts from "./components/settings/Docker.vue";
+import MaintenanceDetails from "./pages/MaintenanceDetails.vue";
+import ManageMaintenance from "./pages/ManageMaintenance.vue";
 
 // Settings - Sub Pages
 import Appearance from "./components/settings/Appearance.vue";
@@ -26,8 +29,6 @@ const Security = () => import("./components/settings/Security.vue");
 import Proxies from "./components/settings/Proxies.vue";
 import Backup from "./components/settings/Backup.vue";
 import About from "./components/settings/About.vue";
-import DockerHosts from "./components/settings/Docker.vue";
-import MaintenanceDetails from "./pages/MaintenanceDetails.vue";
 
 const routes = [
     {
@@ -64,28 +65,12 @@ const routes = [
                                     },
                                 ],
                             },
-                            {
-                                path: "/dashboard/maintenance/:id",
-                                component: EmptyLayout,
-                                children: [
-                                    {
-                                        path: "",
-                                        component: MaintenanceDetails,
-                                    },
-                                    {
-                                        path: "/editMaintenance/:id",
-                                        component: EditMaintenance,
-                                    },
-                                ],
-                            },
+
                             {
                                 path: "/add",
                                 component: EditMonitor,
                             },
-                            {
-                                path: "/addMaintenance",
-                                component: EditMaintenance,
-                            },
+
                         ],
                     },
                     {
@@ -146,6 +131,22 @@ const routes = [
                         path: "/add-status-page",
                         component: AddStatusPage,
                     },
+                    {
+                        path: "/maintenance",
+                        component: ManageMaintenance,
+                    },
+                    {
+                        path: "/maintenance/:id",
+                        component: MaintenanceDetails,
+                    },
+                    {
+                        path: "/add-maintenance",
+                        component: EditMaintenance,
+                    },
+                    {
+                        path: "/maintenance/edit/:id",
+                        component: EditMaintenance,
+                    },
                 ],
             },
         ],
diff --git a/src/util.js b/src/util.js
index ca21e0cb..c5fca856 100644
--- a/src/util.js
+++ b/src/util.js
@@ -282,9 +282,9 @@ function getCryptoRandomInt(min, max) {
 }
 exports.getCryptoRandomInt = getCryptoRandomInt;
 /**
- * Generate a secret
- * @param length Lenght of secret to generate
- * @returns
+ * Generate a random alphanumeric string of fixed length
+ * @param length Length of string to generate
+ * @returns string
  */
 function genSecret(length = 64) {
     let secret = "";
@@ -306,6 +306,6 @@ function getMonitorRelativeURL(id) {
 }
 exports.getMonitorRelativeURL = getMonitorRelativeURL;
 function getMaintenanceRelativeURL(id) {
-    return "/dashboard/maintenance/" + id;
+    return "/maintenance/" + id;
 }
 exports.getMaintenanceRelativeURL = getMaintenanceRelativeURL;
diff --git a/src/util.ts b/src/util.ts
index 3704999c..0dd8a62a 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -340,5 +340,5 @@ export function getMonitorRelativeURL(id: string) {
 }
 
 export function getMaintenanceRelativeURL(id: string) {
-    return "/dashboard/maintenance/" + id;
+    return "/maintenance/" + id;
 }

From 80698a58b83830ad81a56e1861f3e4f120b0cb24 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 17 Sep 2022 22:09:09 +0800
Subject: [PATCH 038/134] Tidy up

---
 src/pages/Dashboard.vue     | 9 +++------
 src/pages/DashboardHome.vue | 8 --------
 src/pages/Details.vue       | 4 ----
 src/router.js               | 2 --
 4 files changed, 3 insertions(+), 20 deletions(-)

diff --git a/src/pages/Dashboard.vue b/src/pages/Dashboard.vue
index 20425c61..c49f5f3a 100644
--- a/src/pages/Dashboard.vue
+++ b/src/pages/Dashboard.vue
@@ -2,7 +2,9 @@
     <div class="container-fluid">
         <div class="row">
             <div v-if="!$root.isMobile" class="col-12 col-md-5 col-xl-4">
-                <router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
+                <div>
+                    <router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
+                </div>
                 <MonitorList :scrollbar="true" />
             </div>
 
@@ -32,9 +34,4 @@ export default {
 .container-fluid {
     width: 98%;
 }
-
-.dropdown-create {
-    display: flex;
-    justify-content: end;
-}
 </style>
diff --git a/src/pages/DashboardHome.vue b/src/pages/DashboardHome.vue
index 0f706912..2745d91b 100644
--- a/src/pages/DashboardHome.vue
+++ b/src/pages/DashboardHome.vue
@@ -147,14 +147,6 @@ export default {
     display: block;
 }
 
-.text-maintenance {
-    color: $maintenance;
-}
-
-.bg-maintenance {
-    background-color: $maintenance;
-}
-
 .shadow-box {
     padding: 20px;
 }
diff --git a/src/pages/Details.vue b/src/pages/Details.vue
index 5583eeff..7cf25892 100644
--- a/src/pages/Details.vue
+++ b/src/pages/Details.vue
@@ -514,8 +514,4 @@ table {
     margin-left: 0 !important;
 }
 
-.bg-maintenance {
-    background-color: $maintenance;
-}
-
 </style>
diff --git a/src/router.js b/src/router.js
index a3414774..38048826 100644
--- a/src/router.js
+++ b/src/router.js
@@ -65,12 +65,10 @@ const routes = [
                                     },
                                 ],
                             },
-
                             {
                                 path: "/add",
                                 component: EditMonitor,
                             },
-
                         ],
                     },
                     {

From a29eae3213a5cf97a75005e8ad148bed4524445c Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sun, 18 Sep 2022 02:02:18 +0800
Subject: [PATCH 039/134] Update Maintenance UI

---
 src/pages/EditMaintenance.vue    |   3 +-
 src/pages/MaintenanceDetails.vue |   2 +-
 src/pages/ManageMaintenance.vue  | 106 ++++++++++++++++++++++---------
 3 files changed, 80 insertions(+), 31 deletions(-)

diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 604bf89b..97c4ec2a 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -268,7 +268,7 @@ export default {
                                 toast.success(res.msg);
                                 this.processing = false;
                                 this.$root.getMaintenanceList();
-                                this.$router.push("/maintenance/" + res.maintenanceID);
+                                this.$router.push("/maintenance");
                             });
                         });
                     } else {
@@ -285,6 +285,7 @@ export default {
                                 this.processing = false;
                                 this.$root.toastRes(res);
                                 this.init();
+                                this.$router.push("/maintenance");
                             });
                         });
                     } else {
diff --git a/src/pages/MaintenanceDetails.vue b/src/pages/MaintenanceDetails.vue
index 947a89b9..04c21691 100644
--- a/src/pages/MaintenanceDetails.vue
+++ b/src/pages/MaintenanceDetails.vue
@@ -91,7 +91,7 @@ export default {
             this.$root.deleteMaintenance(this.maintenance.id, (res) => {
                 if (res.ok) {
                     toast.success(res.msg);
-                    this.$router.push("/dashboard");
+                    this.$router.push("/maintenance");
                 } else {
                     toast.error(res.msg);
                 }
diff --git a/src/pages/ManageMaintenance.vue b/src/pages/ManageMaintenance.vue
index 0041f3d2..3d5f0784 100644
--- a/src/pages/ManageMaintenance.vue
+++ b/src/pages/ManageMaintenance.vue
@@ -16,21 +16,37 @@
                     {{ $t("No maintenance") }}
                 </span>
 
-                <router-link
+                <div
                     v-for="(item, index) in sortedMaintenanceList"
                     :key="index"
-                    :to="maintenanceURL(item.id)"
                     class="item"
-                    :class="{ 'disabled': !$root.isActiveMaintenance(item.end_date) }"
+                    :class="{ 'ended': !$root.isActiveMaintenance(item.end_date) }"
                 >
-                    <div>
+                    <div class="left-part">
+                        <div
+                            class="circle"
+                        ></div>
+                        <div class="info">
+                            <div class="title">{{ item.title }}</div>
+                            <div>{{ item.description }}</div>
+                        </div>
                     </div>
-                    <div class="info">
-                        <div class="title">{{ item.title }}</div>
-                        <div>{{ item.description }}</div>
+
+                    <div class="buttons">
+                        <router-link :to="maintenanceURL(item.id)" class="btn btn-light">{{ $t("Details") }}</router-link>
+                        <router-link :to="'/maintenance/edit/' + item.id" class="btn btn-secondary">
+                            <font-awesome-icon icon="edit" /> {{ $t("Edit") }}
+                        </router-link>
+                        <button class="btn btn-danger" @click="deleteDialog(item.id)">
+                            <font-awesome-icon icon="trash" /> {{ $t("Delete") }}
+                        </button>
                     </div>
-                </router-link>
+                </div>
             </div>
+
+            <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMaintenance">
+                {{ $t("deleteMaintenanceMsg") }}
+            </Confirm>
         </div>
     </transition>
 </template>
@@ -38,12 +54,17 @@
 <script>
 import { getResBaseURL } from "../util-frontend";
 import { getMaintenanceRelativeURL } from "../util.ts";
+import Confirm from "../components/Confirm.vue";
+import { useToast } from "vue-toastification";
+const toast = useToast();
 
 export default {
     components: {
+        Confirm,
     },
     data() {
         return {
+            selectedMaintenanceID: undefined,
         };
     },
     computed: {
@@ -107,6 +128,22 @@ export default {
         maintenanceURL(id) {
             return getMaintenanceRelativeURL(id);
         },
+
+        deleteDialog(maintenanceID) {
+            this.selectedMaintenanceID = maintenanceID;
+            this.$refs.confirmDelete.show();
+        },
+
+        deleteMaintenance() {
+            this.$root.deleteMaintenance(this.selectedMaintenanceID, (res) => {
+                if (res.ok) {
+                    toast.success(res.msg);
+                    this.$router.push("/maintenance");
+                } else {
+                    toast.error(res.msg);
+                }
+            });
+        },
     },
 };
 </script>
@@ -121,6 +158,7 @@ export default {
         text-decoration: none;
         border-radius: 10px;
         transition: all ease-in-out 0.15s;
+        justify-content: space-between;
         padding: 10px;
         min-height: 90px;
 
@@ -128,29 +166,43 @@ export default {
             background-color: $highlight-white;
         }
 
-        &.active {
-            background-color: #cdf8f4;
+        &.ended {
+            .left-part {
+                opacity: 0.5;
+                .circle {
+                    background-color: $dark-font-color;
+                }
+            }
         }
 
-        $logo-width: 70px;
+        .left-part {
+            display: flex;
+            gap: 12px;
+            align-items: center;
 
-        .logo {
-            width: $logo-width;
-            height: $logo-width;
+            .circle {
+                width: 25px;
+                height: 25px;
+                border-radius: 50rem;
+                background-color: $maintenance;
 
-            // Better when the image is loading
-            min-height: 1px;
+            }
+
+            .info {
+                .title {
+                    font-weight: bold;
+                    font-size: 20px;
+                }
+
+                .slug {
+                    font-size: 14px;
+                }
+            }
         }
 
-        .info {
-            .title {
-                font-weight: bold;
-                font-size: 20px;
-            }
-
-            .slug {
-                font-size: 14px;
-            }
+        .buttons {
+            display: flex;
+            gap: 8px;
         }
     }
 
@@ -159,10 +211,6 @@ export default {
             &:hover {
                 background-color: $dark-bg2;
             }
-
-            &.active {
-                background-color: $dark-bg2;
-            }
         }
     }
 </style>

From 9fe07742ea8f2dd7b0a380333f6077092b5f0ef3 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sun, 18 Sep 2022 02:07:32 +0800
Subject: [PATCH 040/134] Linting

---
 src/pages/ManageMaintenance.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/ManageMaintenance.vue b/src/pages/ManageMaintenance.vue
index 3d5f0784..2165d7a5 100644
--- a/src/pages/ManageMaintenance.vue
+++ b/src/pages/ManageMaintenance.vue
@@ -169,6 +169,7 @@ export default {
         &.ended {
             .left-part {
                 opacity: 0.5;
+
                 .circle {
                     background-color: $dark-font-color;
                 }
@@ -185,7 +186,6 @@ export default {
                 height: 25px;
                 border-radius: 50rem;
                 background-color: $maintenance;
-
             }
 
             .info {

From f61c1c47aa19113f03b22573552d14e5adc446c0 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sun, 18 Sep 2022 02:13:29 +0800
Subject: [PATCH 041/134] Update Maintenance UI

---
 src/assets/app.scss           | 4 ++++
 src/pages/EditMaintenance.vue | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/assets/app.scss b/src/assets/app.scss
index 207c0f3a..691adb68 100644
--- a/src/assets/app.scss
+++ b/src/assets/app.scss
@@ -252,6 +252,10 @@ optgroup {
         color: $dark-font-color2;
     }
 
+    .bg-maintenance {
+        background-color: $maintenance;
+    }
+
     .btn-secondary {
         color: white;
     }
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 97c4ec2a..3e650faa 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -140,7 +140,7 @@ export default {
             maintenance: {},
             affectedMonitors: [],
             affectedMonitorsOptions: [],
-            showOnAllPages: true,
+            showOnAllPages: false,
             selectedStatusPages: [],
             selectedStatusPagesOptions: [],
             dark: (this.$root.theme === "dark"),

From 7853c2cc380b95d24324832ba998d4858b19cf73 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sun, 18 Sep 2022 22:34:05 +0800
Subject: [PATCH 042/134] Update Maintenance UI

---
 src/assets/app.scss             | 17 +++++--
 src/languages/en.js             |  4 +-
 src/pages/EditMaintenance.vue   | 88 ++++++++++++++++-----------------
 src/pages/ManageMaintenance.vue |  2 +-
 src/pages/StatusPage.vue        |  7 ++-
 5 files changed, 61 insertions(+), 57 deletions(-)

diff --git a/src/assets/app.scss b/src/assets/app.scss
index 691adb68..81cf7724 100644
--- a/src/assets/app.scss
+++ b/src/assets/app.scss
@@ -22,6 +22,19 @@ textarea.form-control {
     width: 10px;
 }
 
+.bg-maintenance {
+    color: white !important;
+    background-color: $maintenance !important;
+}
+
+.bg-dark {
+    color: white;
+}
+
+.text-maintenance {
+    color: $maintenance !important;
+}
+
 .list-group {
     border-radius: 0.75rem;
 
@@ -252,10 +265,6 @@ optgroup {
         color: $dark-font-color2;
     }
 
-    .bg-maintenance {
-        background-color: $maintenance;
-    }
-
     .btn-secondary {
         color: white;
     }
diff --git a/src/languages/en.js b/src/languages/en.js
index 59c0ea14..1a5956f7 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -10,17 +10,17 @@ export default {
     maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
     acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
     Maintenance: "Maintenance",
-    "Monitors": "Monitors",
     "Schedule maintenance": "Schedule maintenance",
     "Affected Monitors": "Affected Monitors",
     "Pick Affected Monitors...": "Pick Affected Monitors...",
     "Start of maintenance": "Start of maintenance",
     "Expected end of maintenance": "Expected end of maintenance",
-    "Show on all pages": "Show on all status pages",
+    "All Status Pages": "All Status Pages",
     "Selected status pages": "Selected status pages",
     "Select status pages...": "Select status pages...",
     End: "End",
     affectedMonitorsDescription: "Select monitors that are affected by current maintenance",
+    affectedStatusPages: "Show this maintenance message on selected status pages",
     atLeastOneMonitor: "Select at least one affected monitor",
     maintenanceInvalidDate: "Invalid maintenance end date entered",
     selectedStatusPagesDescription: "Select status pages to display maintenance info on",
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 3e650faa..65f2d724 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -5,7 +5,7 @@
             <form @submit.prevent="submit">
                 <div class="shadow-box">
                     <div class="row">
-                        <div class="col-md-6">
+                        <div class="col-xl-7">
                             <!-- Title -->
                             <div class="my-3">
                                 <label for="name" class="form-label">{{ $t("Title") }}</label>
@@ -24,9 +24,10 @@
                             </div>
 
                             <!-- Affected Monitors -->
-                            <div class="my-3">
-                                <label for="affected_monitors" class="form-label">{{ $t("Affected Monitors") }}</label>
+                            <h2 class="mt-5">{{ $t("Affected Monitors") }}</h2>
+                            {{ $t("affectedMonitorsDescription") }}
 
+                            <div class="my-3">
                                 <VueMultiselect
                                     id="affected_monitors"
                                     v-model="affectedMonitors"
@@ -43,12 +44,46 @@
                                     :max-height="600"
                                     :taggable="false"
                                 ></VueMultiselect>
+                            </div>
 
-                                <div class="form-text">
-                                    {{ $t("affectedMonitorsDescription") }}
+                            <!-- Status pages to display maintenance info on -->
+                            <h2 class="mt-5">{{ $t("Status Pages") }}</h2>
+                            {{ $t("affectedStatusPages") }}
+
+                            <div class="my-3">
+                                <!-- Show on all pages -->
+                                <div class="form-check mb-2">
+                                    <input
+                                        id="show-on-all-pages" v-model="showOnAllPages" class="form-check-input"
+                                        type="checkbox"
+                                    >
+                                    <label class="form-check-label" for="show-powered-by">{{
+                                        $t("All Status Pages")
+                                    }}</label>
+                                </div>
+
+                                <div v-if="!showOnAllPages">
+                                    <VueMultiselect
+                                        id="selected_status_pages"
+                                        v-model="selectedStatusPages"
+                                        :options="selectedStatusPagesOptions"
+                                        track-by="id"
+                                        label="name"
+                                        :multiple="true"
+                                        :allow-empty="true"
+                                        :close-on-select="false"
+                                        :clear-on-select="false"
+                                        :preserve-search="true"
+                                        :placeholder="$t('Select status pages...')"
+                                        :preselect-first="false"
+                                        :max-height="600"
+                                        :taggable="false"
+                                    ></VueMultiselect>
                                 </div>
                             </div>
 
+                            <h2 class="mt-5">{{ $t("Effective Date Range") }}</h2>
+
                             <!-- Start Date Time -->
                             <div class="my-3">
                                 <label for="start_date" class="form-label">{{ $t("Start Date") }}</label>
@@ -67,46 +102,7 @@
                                 >
                             </div>
 
-                            <!-- Show on all pages -->
-                            <div class="my-3 form-check">
-                                <input
-                                    id="show-on-all-pages" v-model="showOnAllPages" class="form-check-input"
-                                    type="checkbox"
-                                >
-                                <label class="form-check-label" for="show-powered-by">{{
-                                    $t("Show this Maintenance Message on ALL Status Pages")
-                                }}</label>
-                            </div>
-
-                            <!-- Status pages to display maintenance info on -->
-                            <div v-if="!showOnAllPages" class="my-3">
-                                <label for="selected_status_pages" class="form-label">{{
-                                    $t("Show this Maintenance Message on which Status Pages")
-                                }}</label>
-
-                                <VueMultiselect
-                                    id="selected_status_pages"
-                                    v-model="selectedStatusPages"
-                                    :options="selectedStatusPagesOptions"
-                                    track-by="id"
-                                    label="name"
-                                    :multiple="true"
-                                    :allow-empty="false"
-                                    :close-on-select="false"
-                                    :clear-on-select="false"
-                                    :preserve-search="true"
-                                    :placeholder="$t('Select status pages...')"
-                                    :preselect-first="false"
-                                    :max-height="600"
-                                    :taggable="false"
-                                ></VueMultiselect>
-
-                                <div class="form-text">
-                                    {{ $t("selectedStatusPagesDescription") }}
-                                </div>
-                            </div>
-
-                            <div class="mt-5 mb-1">
+                            <div class="mt-4 mb-1">
                                 <button
                                     id="monitor-submit-btn" class="btn btn-primary" type="submit"
                                     :disabled="processing"
@@ -329,7 +325,7 @@ export default {
 }
 
 textarea {
-    min-height: 200px;
+    min-height: 150px;
 }
 
 .dark-calendar::-webkit-calendar-picker-indicator {
diff --git a/src/pages/ManageMaintenance.vue b/src/pages/ManageMaintenance.vue
index 2165d7a5..bcd47acf 100644
--- a/src/pages/ManageMaintenance.vue
+++ b/src/pages/ManageMaintenance.vue
@@ -33,7 +33,7 @@
                     </div>
 
                     <div class="buttons">
-                        <router-link :to="maintenanceURL(item.id)" class="btn btn-light">{{ $t("Details") }}</router-link>
+                        <router-link v-if="false" :to="maintenanceURL(item.id)" class="btn btn-light">{{ $t("Details") }}</router-link>
                         <router-link :to="'/maintenance/edit/' + item.id" class="btn btn-secondary">
                             <font-awesome-icon icon="edit" /> {{ $t("Edit") }}
                         </router-link>
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index 96ff6085..699c236d 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -199,14 +199,14 @@
             <template v-if="maintenance.length">
                 <div
                     v-for="maintenanceItem in maintenance" :key="maintenanceItem.id"
-                    class="shadow-box alert mb-4 p-4 maintenance mt-4 position-relative" role="alert"
+                    class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert"
                 >
                     <div class="item">
                         <div class="row">
                             <div class="col-1 col-md-1 d-flex justify-content-center align-items-center">
                                 <font-awesome-icon
                                     icon="wrench"
-                                    class="maintenance-icon maintenance-bg-info"
+                                    class="maintenance-icon"
                                 />
                             </div>
                             <div class="col-11 col-md-11">
@@ -215,7 +215,6 @@
 
                                 <div class="date mt-3">
                                     {{ $t("End") }}: {{ $root.datetimeMaintenance(maintenanceItem.end_date) }}
-                                    ({{ dateFromNow(maintenanceItem.start_date) }})<br />
                                 </div>
                             </div>
                         </div>
@@ -996,7 +995,7 @@ footer {
 }
 
 .maintenance-icon {
-    font-size: 30px;
+    font-size: 35px;
     vertical-align: middle;
 }
 

From 617ba49e6c270d3892a12e3aec2d02739a6134ca Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sun, 18 Sep 2022 22:40:53 +0800
Subject: [PATCH 043/134] Fix race condition of `selectedStatusPagesOptions`

---
 src/pages/EditMaintenance.vue | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 65f2d724..f010ca8e 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -138,13 +138,21 @@ export default {
             affectedMonitorsOptions: [],
             showOnAllPages: false,
             selectedStatusPages: [],
-            selectedStatusPagesOptions: [],
             dark: (this.$root.theme === "dark"),
         };
     },
 
     computed: {
 
+        selectedStatusPagesOptions() {
+            return Object.values(this.$root.statusPageList).map(statusPage => {
+                return {
+                    id: statusPage.id,
+                    name: statusPage.title
+                };
+            });
+        },
+
         pageName() {
             return this.$t((this.isAdd) ? "Schedule Maintenance" : "Edit Maintenance");
         },
@@ -177,13 +185,6 @@ export default {
                 });
             }
         });
-
-        Object.values(this.$root.statusPageList).map(statusPage => {
-            this.selectedStatusPagesOptions.push({
-                id: statusPage.id,
-                name: statusPage.title
-            });
-        });
     },
     methods: {
         init() {

From e4e47c39765c5f5e4e35bda0332e5055208faa87 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sun, 18 Sep 2022 23:07:17 +0800
Subject: [PATCH 044/134] Update README.md

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 496aed04..b81c0be2 100644
--- a/README.md
+++ b/README.md
@@ -177,4 +177,4 @@ If you want to translate Uptime Kuma into your language, please read: https://gi
 Feel free to correct my grammar in this README, source code, or wiki, as my mother language is not English and my grammar is not that great.
 
 ### Create Pull Requests
-If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
+If you want to modify Uptime Kuma, please read this guide and follow the rules here: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md

From 1f1825dbff2a066f76ece00150350e811d942c50 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Mon, 19 Sep 2022 19:39:30 +0800
Subject: [PATCH 045/134] Update CONTRIBUTING.md

---
 CONTRIBUTING.md | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c6bfb073..e0bc7cd2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -177,7 +177,18 @@ npm test
 
 By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
 
-## Update Dependencies
+## Dependencies
+
+Both frontend and backend share the same package.json. However, the frontend dependencies are eventually not be used in production environment, because it is usually also baked into dist files. So:
+
+- Frontend dependencies = "devDependencies"
+  - Examples: vue, chart.js
+- Backend dependencies = "dependencies"
+  - Examples: socket.io, sqlite3
+- Development dependencies = "devDependencies"
+  - Examples: eslint, sass
+
+### Update Dependencies
 
 Install `ncu`
 https://github.com/raineorshine/npm-check-updates

From 3193533a607247175b54e3fea59854c1c6624103 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Mon, 19 Sep 2022 19:56:30 +0800
Subject: [PATCH 046/134] Update CONTRIBUTING.md

---
 CONTRIBUTING.md | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e0bc7cd2..e9829c96 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -27,13 +27,11 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
 
 ## Can I create a pull request for Uptime Kuma?
 
-Yes, you can. However, since I don't want to waste your time, be sure to **create empty draft pull request, so we can discuss first** if it is a large pull request or you don't know it will be merged or not.
+Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create empty draft pull request or open an issue, so we can discuss first**. Especially for a large pull request or you don't know it will be merged or not.
 
-Also, please don't rush or ask for ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
+Here are some references:
 
-I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
-
-✅ Accept:
+✅ Usually Accept:
 - Bug/Security fix
 - Translations
 - Adding notification providers
@@ -47,8 +45,14 @@ I will mark your pull request in the [milestones](https://github.com/louislam/up
 - Any breaking changes
 - Duplicated pull request
 - Buggy
+- UI/UX is not close to Uptime Kuma 
 - Existing logic is completely modified or deleted for no reason
 - A function that is completely out of scope
+- Unnesscary large code changes (Hard to review, casuse code conflicts to other pull requests)
+
+I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
+
+Also, please don't rush or ask for ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
 
 
 ### Recommended Pull Request Guideline

From c4cb825fef0652ff7fd555ea2399aae70e154101 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Mon, 19 Sep 2022 23:14:25 +0800
Subject: [PATCH 047/134] Update PULL_REQUEST_TEMPLATE.md

---
 .github/PULL_REQUEST_TEMPLATE.md | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 7b870297..1595750b 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,4 +1,10 @@
-👉 Delete this line if you have read and agree our pull request rules and guidelines: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma
+# 
+
+⚠️⚠️⚠️ Since we do not accept all types of pull requests and do not want waste your time. Please be sure that you have read pull request rules:
+https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma
+
+Tick the checkbox if you understand [x]: 
+- [ ] I have read and understand the pull request rules.
 
 # Description
 

From 9a7c2d562ab6baeea96ded002ac5b8664bfb193c Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Mon, 19 Sep 2022 23:15:19 +0800
Subject: [PATCH 048/134] Update PULL_REQUEST_TEMPLATE.md

---
 .github/PULL_REQUEST_TEMPLATE.md | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 1595750b..4d2105d4 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,6 +1,4 @@
-# 
-
-⚠️⚠️⚠️ Since we do not accept all types of pull requests and do not want waste your time. Please be sure that you have read pull request rules:
+⚠️⚠️⚠️ Since we do not accept all types of pull requests and do not want to waste your time. Please be sure that you have read pull request rules:
 https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma
 
 Tick the checkbox if you understand [x]: 

From 565e9233fe13855d2e642892639bc6dbf3f23d4e Mon Sep 17 00:00:00 2001
From: rezzorix <72935517+rezzorix@users.noreply.github.com>
Date: Wed, 21 Sep 2022 18:27:18 +0800
Subject: [PATCH 049/134] Update stale-bot.yml

Adding "operations-per-run: 90" to ensure the action catches all 600+ items that need to be processed etc.

If not defined, the default is 30 which captures only about 200 items a run which is not enough.
---
 .github/workflows/stale-bot.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml
index 34a42d60..3f13b115 100644
--- a/.github/workflows/stale-bot.yml
+++ b/.github/workflows/stale-bot.yml
@@ -20,3 +20,4 @@ jobs:
           exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,feature-request'
           exempt-issue-assignees: 'louislam'
           exempt-pr-assignees: 'louislam'
+          operations-per-run: 90

From c6cf600722a5167cb148d9ce898652dc87bbf1cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Buchti=C4=8D?= <martin.buchta@gmail.com>
Date: Thu, 22 Sep 2022 19:30:43 +0200
Subject: [PATCH 050/134] Update cs-CZ.js

localization improvements
---
 src/languages/cs-CZ.js | 37 ++++++++++++++++++++-----------------
 1 file changed, 20 insertions(+), 17 deletions(-)

diff --git a/src/languages/cs-CZ.js b/src/languages/cs-CZ.js
index c71075be..e9cdb992 100644
--- a/src/languages/cs-CZ.js
+++ b/src/languages/cs-CZ.js
@@ -47,10 +47,10 @@ export default {
     Down: "Nedostupný",
     Pending: "Čekám",
     Unknown: "Neznámý",
-    Pause: "Pozastavit",
+    Pause: "Pozastaveno",
     Name: "Název",
     Status: "Stav",
-    DateTime: "DateTime",
+    DateTime: "Časové razítko",
     Message: "Zpráva",
     "No important events": "Žádné důležité události",
     Resume: "Pokračovat",
@@ -77,7 +77,7 @@ export default {
     "Resend Notification if Down X times consequently": "Znovu zaslat oznámení, pokud je služba nedostupná Xkrát za sebou",
     Advanced: "Rozšířené",
     "Upside Down Mode": "Inverzní režim",
-    "Max. Redirects": "Max. Přesměrování",
+    "Max. Redirects": "Max. přesměrování",
     "Accepted Status Codes": "Akceptované stavové kódy",
     "Push URL": "Push URL",
     needPushEvery: "Tuto URL adresu byste měli volat každých {0} sekund.",
@@ -107,7 +107,7 @@ export default {
     "disableauth.message1": "Opravdu chcete <strong>deaktivovat autentifikaci</strong>?",
     "disableauth.message2": "Tato možnost je určena pro případy, kdy <strong>máte autentifikaci zajištěnou třetí stranou</strong> ještě před přístupem do Uptime Kuma, například prostřednictvím Cloudflare Access.",
     "Please use this option carefully!": "Používejte ji prosím s rozmyslem.",
-    Logout: "Odhlášení",
+    Logout: "Odhlásit",
     Leave: "Odejít",
     "I understand, please disable": "Rozumím, chci ji deaktivovat",
     Confirm: "Potvrzení",
@@ -132,7 +132,7 @@ export default {
     "Export Backup": "Exportovat zálohu",
     Export: "Exportovat",
     Import: "Importovat",
-    respTime: "Odezva Čas (ms)",
+    respTime: "Doba odezvy (ms)",
     notAvailableShort: "N/A",
     "Default enabled": "Standardně povoleno",
     "Apply on all existing monitors": "Použít pro všechny existující dohledy",
@@ -313,7 +313,7 @@ export default {
     PasswordsDoNotMatch: "Hesla se neshodují.",
     records: "záznamů",
     "One record": "Jeden záznam",
-    steamApiKeyDescription: "Pro monitorování Steam Game Servere je nutné zadat Steam Web-API klíč. Svůj API klíč získáte na následující stránce: ",
+    steamApiKeyDescription: "Pro monitorování Steam Game Serveru je nutné zadat Steam Web-API klíč. Svůj API klíč získáte na následující stránce: ",
     "Current User": "Aktuálně přihlášený uživatel",
     topic: "Topic",
     topicExplanation: "MQTT topic, který chcete sledovat",
@@ -327,7 +327,7 @@ export default {
     "Shrink Database": "Zmenšit databázi",
     "Pick a RR-Type...": "Vyberte typ záznamu o prostředku…",
     "Pick Accepted Status Codes...": "Vyberte stavové kódy, které chcete akceptovat…",
-    Default: "Standardní",
+    Default: "Výchozí",
     "HTTP Options": "Možnosti protokolu HTTP",
     "Create Incident": "Vytvořit incident",
     Title: "Předmět",
@@ -347,7 +347,7 @@ export default {
     "Last Updated": "Poslední aktualizace",
     Unpin: "Odepnout",
     "Switch to Light Theme": "Přepnout na světlý motiv",
-    "Switch to Dark Theme": "Přepnutí na tmavý motiv",
+    "Switch to Dark Theme": "Přepnout na tmavý motiv",
     "Show Tags": "Zobrazit štítky",
     "Hide Tags": "Skrýt štítky",
     Description: "Popis",
@@ -425,8 +425,8 @@ export default {
     Retry: "Opakovat",
     Topic: "Topic",
     "WeCom Bot Key": "WeCom Bot Key",
-    "Setup Proxy": "Setup Proxy",
-    "Proxy Protocol": "Proxy Protocol",
+    "Setup Proxy": "Nastavit proxy",
+    "Proxy Protocol": "Protokol proxy",
     "Proxy Server": "Proxy Server",
     "Proxy server has authentication": "Proxy server vyžaduje ověření",
     User: "Uživatel",
@@ -481,7 +481,7 @@ export default {
     onebotSafetyTips: "Z důvodu bezpečnosti je nutné zadat přístupový token",
     "PushDeer Key": "PushDeer klíč",
     "Footer Text": "Text v patičce",
-    "Show Powered By": "Zobrazit \"Zajišťuje\"",
+    "Show Powered By": "Zobrazit \"Poskytuje\"",
     "Domain Names": "Názvy domén",
     signedInDisp: "Přihlášen jako {0}",
     signedInDispDisabled: "Ověření je vypnuté.",
@@ -529,9 +529,9 @@ export default {
     "pushoversounds none": "Žádný (ticho)",
     pushyAPIKey: "Secret API Key",
     pushyToken: "Token zařízení",
-    "Show update if available": "Zobrazit aktualizace, pokud jsou k dispozici",
+    "Show update if available": "Upozornit na aktualizace, pokud jsou k dispozici",
     "Also check beta release": "Kontrolovat také dostupnost beta verzí",
-    "Using a Reverse Proxy?": "Používáte reverzní proxy??",
+    "Using a Reverse Proxy?": "Používáte reverzní proxy?",
     "Check how to config it for WebSocket": "Zjistěte, jak ji nakonfigurovat pro WebSockety",
     "Steam Game Server": "Steam Game Server",
     "Most likely causes:": "Nejčastější důvody:",
@@ -545,7 +545,7 @@ export default {
     "Connection String": "Connection String",
     Query: "Dotaz",
     settingsCertificateExpiry: "Platnost TLS certifikátu",
-    certificationExpiryDescription: "Aktivovat oznámení nad HTTPS dohledy, pokud platnost TSL certifikátu vyprší za:",
+    certificationExpiryDescription: "Aktivovat oznámení nad HTTPS dohledy, pokud platnost TLS certifikátu vyprší za:",
     "Setup Docker Host": "Nastavit Docker hostitele",
     "Connection Type": "Typ připojení",
     "Docker Daemon": "Docker Daemon",
@@ -576,7 +576,10 @@ export default {
     "Then choose an action, for example switch the scene to where an RGB light is red.": "Následně vyberte akci, například přepnutí scény z RGB světla na červenou.",
     "Frontend Version": "Verze frontendu",
     "Frontend Version do not match backend version!": "Verze frontendu neodpovídá verzi backendu!",
-    "You can divide numbers with": "Čísla můžete rozdělit pomocí ",
-    "or": "nebo",
-    "Gateway Type": "Typ brány",
+    "Base URL": "Primární URL adresa",
+    goAlertInfo: "GoAlert je aplikace s otevřeným zdrojovým kódem pro plánování hovorů, automatické eskalace a upozornění (jako jsou SMS nebo hlasové hovory). Automaticky zapojte správnou osobu, správným způsobem a ve správný čas! {0}",
+    goAlertIntegrationKeyInfo: "Obecný API integrační klíč pro danou službu ve formátu \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" se obvykle nachází ve zkopírované URL jako hodnota parametru token.",
+    goAlert: "GoAlert",
+    backupOutdatedWarning: "Zastaralé: V poslední době byla funkčnost aplikace značně rozšířena, nicméně součást pro zálohování nepokrývá všechny možnosti. Z tohoto důvodu není možné vygenerovat úplnou zálohu a zajistit obnovení všech dat.",
+    backupRecommend: "Prosím, zálohujte si ručně celý svazek nebo datovou složku (./data/).",
 };

From dcbd9c12cf4ae5266595efd1e91e67771bb46ac9 Mon Sep 17 00:00:00 2001
From: rezzorix <72935517+rezzorix@users.noreply.github.com>
Date: Fri, 23 Sep 2022 21:59:38 +0800
Subject: [PATCH 051/134] Update stale-bot.yml

1. cron every 6 hours (from 24hrs)
2. close after 2 days scale (from 7)
3. operations per run 200 (from 90)
---
 .github/workflows/stale-bot.yml | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml
index 3f13b115..02ec9714 100644
--- a/.github/workflows/stale-bot.yml
+++ b/.github/workflows/stale-bot.yml
@@ -1,8 +1,8 @@
 name: 'Automatically close stale issues and PRs'
 on:
   schedule:
-    - cron: '0 0 * * *'
-#Run once a day at midnight 
+    - cron: '0 */6 * * *'
+#Run every 6 hours 
 
 jobs:
   stale:
@@ -10,14 +10,14 @@ jobs:
     steps:
       - uses: actions/stale@v5
         with:
-          stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
-          stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
-          close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'
-          close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.'
+          stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
+          stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
+          close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'
+          close-pr-message: 'This PR was closed because it has been stalled for 2 days with no activity.'
           days-before-stale: 90
-          days-before-close: 7
+          days-before-close: 2
           exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request'
           exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,feature-request'
           exempt-issue-assignees: 'louislam'
           exempt-pr-assignees: 'louislam'
-          operations-per-run: 90
+          operations-per-run: 200

From b03624b7e33fc1d9eed953645a2142ed0f3a1088 Mon Sep 17 00:00:00 2001
From: MA Junyi <mjysci@live.com>
Date: Fri, 23 Sep 2022 23:27:22 +0800
Subject: [PATCH 052/134] feat: Add ServerChan Notification support

---
 server/notification-providers/serverchan.js | 36 +++++++++++++++++++++
 server/notification.js                      |  2 ++
 src/components/notifications/ServerChan.vue | 16 +++++++++
 src/components/notifications/index.js       |  2 ++
 4 files changed, 56 insertions(+)
 create mode 100644 server/notification-providers/serverchan.js
 create mode 100644 src/components/notifications/ServerChan.vue

diff --git a/server/notification-providers/serverchan.js b/server/notification-providers/serverchan.js
new file mode 100644
index 00000000..fbf99f80
--- /dev/null
+++ b/server/notification-providers/serverchan.js
@@ -0,0 +1,36 @@
+const NotificationProvider = require("./notification-provider");
+const axios = require("axios");
+const { DOWN, UP } = require("../../src/util");
+
+class ServerChan extends NotificationProvider {
+
+    name = "ServerChan";
+
+    async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
+        let okMsg = "Sent Successfully.";
+        try {
+            await axios.post(`https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`, {
+                "title": this.checkStatus(heartbeatJSON, monitorJSON),
+                "desp": msg,
+            });
+
+            return okMsg;
+
+        } catch (error) {
+            this.throwGeneralAxiosError(error);
+        }
+    }
+
+    checkStatus(heartbeatJSON, monitorJSON) {
+        let title = "UptimeKuma Message";
+        if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
+            title = "UptimeKuma Monitor Up " + monitorJSON["name"];
+        }
+        if (heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
+            title = "UptimeKuma Monitor Down " + monitorJSON["name"];
+        }
+        return title;
+    }
+}
+
+module.exports = ServerChan;
diff --git a/server/notification.js b/server/notification.js
index 6f71783b..3bf51243 100644
--- a/server/notification.js
+++ b/server/notification.js
@@ -40,6 +40,7 @@ const Webhook = require("./notification-providers/webhook");
 const WeCom = require("./notification-providers/wecom");
 const GoAlert = require("./notification-providers/goalert");
 const SMSManager = require("./notification-providers/smsmanager");
+const ServerChan = require("./notification-providers/serverchan");
 
 class Notification {
 
@@ -80,6 +81,7 @@ class Notification {
             new Pushover(),
             new Pushy(),
             new RocketChat(),
+            new ServerChan(),
             new SerwerSMS(),
             new Signal(),
             new SMSManager(),
diff --git a/src/components/notifications/ServerChan.vue b/src/components/notifications/ServerChan.vue
new file mode 100644
index 00000000..cec75675
--- /dev/null
+++ b/src/components/notifications/ServerChan.vue
@@ -0,0 +1,16 @@
+<template>
+    <div class="mb-3">
+        <label for="serverchan-sendkey" class="form-label">{{ $t("SendKey") }}</label>
+        <HiddenInput id="serverchan-sendkey" v-model="$parent.notification.serverChanSendKey" :required="true" autocomplete="one-time-code"></HiddenInput>
+    </div>
+</template>
+
+<script>
+import HiddenInput from "../HiddenInput.vue";
+
+export default {
+    components: {
+        HiddenInput,
+    },
+};
+</script>
diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js
index 07ed2cd1..6add06ea 100644
--- a/src/components/notifications/index.js
+++ b/src/components/notifications/index.js
@@ -26,6 +26,7 @@ import PushDeer from "./PushDeer.vue";
 import Pushover from "./Pushover.vue";
 import Pushy from "./Pushy.vue";
 import RocketChat from "./RocketChat.vue";
+import ServerChan from "./ServerChan.vue";
 import SerwerSMS from "./SerwerSMS.vue";
 import Signal from "./Signal.vue";
 import SMSManager from "./SMSManager.vue";
@@ -85,6 +86,7 @@ const NotificationFormList = {
     "webhook": Webhook,
     "WeCom": WeCom,
     "GoAlert": GoAlert,
+    "ServerChan": ServerChan,
 };
 
 export default NotificationFormList;

From 443235b20b9db8fab84f2bf580bf50fbaa3a53c6 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 24 Sep 2022 00:11:22 +0800
Subject: [PATCH 053/134] Update stale-bot.yml

---
 .github/workflows/stale-bot.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml
index 02ec9714..ae9177af 100644
--- a/.github/workflows/stale-bot.yml
+++ b/.github/workflows/stale-bot.yml
@@ -1,5 +1,6 @@
 name: 'Automatically close stale issues and PRs'
 on:
+  workflow_dispatch:
   schedule:
     - cron: '0 */6 * * *'
 #Run every 6 hours 

From 9d99c39f30dc9c2d5b9aeb0128385fc972e4a03e Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 24 Sep 2022 02:33:29 +0800
Subject: [PATCH 054/134] Update Maintenance UI for recurring

---
 package-lock.json               |  25 ++++
 package.json                    |   1 +
 server/model/maintenance.js     |  10 +-
 src/assets/vue-datepicker.scss  |  39 +++++
 src/languages/en.js             |  22 ++-
 src/main.js                     |   1 +
 src/mixins/datetime.js          |   9 ++
 src/mixins/theme.js             |   4 +
 src/pages/EditMaintenance.vue   | 258 +++++++++++++++++++++++++++++---
 src/pages/ManageMaintenance.vue |   4 +
 10 files changed, 342 insertions(+), 31 deletions(-)
 create mode 100644 src/assets/vue-datepicker.scss

diff --git a/package-lock.json b/package-lock.json
index 65e380d1..82f290b4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -68,6 +68,7 @@
                 "@vitejs/plugin-legacy": "~2.1.0",
                 "@vitejs/plugin-vue": "~3.1.0",
                 "@vue/compiler-sfc": "~3.2.36",
+                "@vuepic/vue-datepicker": "^3.4.8",
                 "aedes": "^0.46.3",
                 "babel-plugin-rewire": "~1.2.0",
                 "bootstrap": "5.1.3",
@@ -3941,6 +3942,21 @@
             "integrity": "sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg==",
             "dev": true
         },
+        "node_modules/@vuepic/vue-datepicker": {
+            "version": "3.4.8",
+            "resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-3.4.8.tgz",
+            "integrity": "sha512-nbuMA7IgjtT99LqcjSTSUcl7omTZSB+7vYSWQ9gQm31Frm/1wn54fT1Q0HaRD9nHXX982AACbqeND4K80SKONw==",
+            "dev": true,
+            "dependencies": {
+                "date-fns": "^2.29.2"
+            },
+            "engines": {
+                "node": ">=14"
+            },
+            "peerDependencies": {
+                "vue": ">=3.2.0"
+            }
+        },
         "node_modules/abab": {
             "version": "2.0.6",
             "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -20409,6 +20425,15 @@
             "integrity": "sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg==",
             "dev": true
         },
+        "@vuepic/vue-datepicker": {
+            "version": "3.4.8",
+            "resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-3.4.8.tgz",
+            "integrity": "sha512-nbuMA7IgjtT99LqcjSTSUcl7omTZSB+7vYSWQ9gQm31Frm/1wn54fT1Q0HaRD9nHXX982AACbqeND4K80SKONw==",
+            "dev": true,
+            "requires": {
+                "date-fns": "^2.29.2"
+            }
+        },
         "abab": {
             "version": "2.0.6",
             "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
diff --git a/package.json b/package.json
index 219042aa..01bf7da0 100644
--- a/package.json
+++ b/package.json
@@ -124,6 +124,7 @@
         "@vitejs/plugin-legacy": "~2.1.0",
         "@vitejs/plugin-vue": "~3.1.0",
         "@vue/compiler-sfc": "~3.2.36",
+        "@vuepic/vue-datepicker": "^3.4.8",
         "aedes": "^0.46.3",
         "babel-plugin-rewire": "~1.2.0",
         "bootstrap": "5.1.3",
diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index 1b0b9ee0..2f3e2000 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -19,6 +19,8 @@ class Maintenance extends BeanModel {
             description: this.description,
             start_date: this.start_date,
             end_date: this.end_date,
+            strategy: this.strategy,
+            active: !!this.active,
         };
     }
 
@@ -27,13 +29,7 @@ class Maintenance extends BeanModel {
      * @returns {Object}
      */
     async toJSON() {
-        return {
-            id: this.id,
-            title: this.title,
-            description: this.description,
-            start_date: this.start_date,
-            end_date: this.end_date,
-        };
+        return this.toPublicJSON();
     }
 }
 
diff --git a/src/assets/vue-datepicker.scss b/src/assets/vue-datepicker.scss
new file mode 100644
index 00000000..dedbc080
--- /dev/null
+++ b/src/assets/vue-datepicker.scss
@@ -0,0 +1,39 @@
+@import "@vuepic/vue-datepicker/dist/main.css";
+@import "vars.scss";
+
+// Must use #{ }
+// Remark: https://stackoverflow.com/questions/50202991/unable-to-set-scss-variable-to-css-variable
+.dp__theme_dark {
+    --dp-background-color: #{$dark-bg2};
+    --dp-text-color: #{$dark-font-color};
+    --dp-hover-color: #484848;
+    --dp-hover-text-color: #ffffff;
+    --dp-hover-icon-color: #959595;
+    --dp-primary-color: #{#5cdd8b};
+    --dp-primary-text-color: #ffffff;
+    --dp-secondary-color: #494949;
+    --dp-border-color: #{$dark-border-color};
+    --dp-menu-border-color: #2d2d2d;
+    --dp-border-color-hover: #{$dark-border-color};
+    --dp-disabled-color: #212121;
+    --dp-scroll-bar-background: #212121;
+    --dp-scroll-bar-color: #484848;
+    --dp-success-color: #{$primary};
+    --dp-success-color-disabled: #428f59;
+    --dp-icon-color: #959595;
+    --dp-danger-color: #e53935;
+    --dp-highlight-color: rgba(0, 92, 178, 0.2);
+}
+
+.dp__input {
+    border-radius: $border-radius;
+}
+
+// Fix: Full width of text input when using "inline textInput inlineWithInput" mode
+.dp__main > div[aria-label="Datepicker input"] {
+    width: 100%;
+}
+
+.dp__main > div[aria-label="Datepicker menu"]:nth-child(2) {
+    margin-top: 20px;
+}
diff --git a/src/languages/en.js b/src/languages/en.js
index 1a5956f7..d729d3a5 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -18,7 +18,8 @@ export default {
     "All Status Pages": "All Status Pages",
     "Selected status pages": "Selected status pages",
     "Select status pages...": "Select status pages...",
-    End: "End",
+    recurringIntervalMessage: "Run once every day | Run once every {0} days",
+    "End": "End",
     affectedMonitorsDescription: "Select monitors that are affected by current maintenance",
     affectedStatusPages: "Show this maintenance message on selected status pages",
     atLeastOneMonitor: "Select at least one affected monitor",
@@ -61,9 +62,7 @@ export default {
     List: "List",
     Add: "Add",
     "Add Monitor": "Add Monitor",
-    "Add Maintenance": "Add Maintenance",
     "Add New Monitor": "Add New Monitor",
-    "Add New Maintenance": "Add New Maintenance",
     "Quick Stats": "Quick Stats",
     Up: "Up",
     Down: "Down",
@@ -605,4 +604,21 @@ export default {
     goAlert: "GoAlert",
     backupOutdatedWarning: "Deprecated: Since a lot of features added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.",
     backupRecommend: "Please backup the volume or the data folder (./data/) directly instead.",
+    recurringInterval: "Interval",
+    "Recurring": "Recurring",
+    strategyManual: "Active/Inactive Manually",
+    warningTimezone: "It is NOT your current browser's timezone. It is your server's timezone.",
+    weekdayShortMon: "Mon",
+    weekdayShortTue: "Tue",
+    weekdayShortWed: "Wed",
+    weekdayShortThu: "Thu",
+    weekdayShortFri: "Fri",
+    weekdayShortSat: "Sat",
+    weekdayShortSun: "Sun",
+    dayOfMonth: "Day of Month",
+    lastDay: "Last Day",
+    lastDay1: "Last Day of Month",
+    lastDay2: "2nd Last Day of Month",
+    lastDay3: "3rd Last Day of Month",
+    lastDay4: "4th Last Day of Month",
 };
diff --git a/src/main.js b/src/main.js
index 18490908..7783882b 100644
--- a/src/main.js
+++ b/src/main.js
@@ -5,6 +5,7 @@ import Toast from "vue-toastification";
 import "vue-toastification/dist/index.css";
 import App from "./App.vue";
 import "./assets/app.scss";
+import "./assets/vue-datepicker.scss";
 import { i18n } from "./i18n";
 import { FontAwesomeIcon } from "./icon.js";
 import datetime from "./mixins/datetime";
diff --git a/src/mixins/datetime.js b/src/mixins/datetime.js
index b8e4db45..0e5317c6 100644
--- a/src/mixins/datetime.js
+++ b/src/mixins/datetime.js
@@ -26,6 +26,15 @@ export default {
             return dayjs.tz(value, this.timezone).utc().format();
         },
 
+        /**
+         * Used for <input type="datetime" />
+         * @param value
+         * @returns {string}
+         */
+        toDateTimeInputFormat(value) {
+            return this.datetimeFormat(value, "YYYY-MM-DDTHH:mm");
+        },
+
         /**
          * Return a given value in the format YYYY-MM-DD HH:mm:ss
          * @param {any} value Value to format as date time
diff --git a/src/mixins/theme.js b/src/mixins/theme.js
index 58f4d91b..8d225267 100644
--- a/src/mixins/theme.js
+++ b/src/mixins/theme.js
@@ -46,6 +46,10 @@ export default {
                 }
                 return this.userTheme;
             }
+        },
+
+        isDark() {
+            return this.theme === "dark";
         }
     },
 
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index f010ca8e..98d5c2b2 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -7,7 +7,7 @@
                     <div class="row">
                         <div class="col-xl-7">
                             <!-- Title -->
-                            <div class="my-3">
+                            <div class="mb-3">
                                 <label for="name" class="form-label">{{ $t("Title") }}</label>
                                 <input
                                     id="name" v-model="maintenance.title" type="text" class="form-control"
@@ -35,7 +35,6 @@
                                     track-by="id"
                                     label="name"
                                     :multiple="true"
-                                    :allow-empty="false"
                                     :close-on-select="false"
                                     :clear-on-select="false"
                                     :preserve-search="true"
@@ -70,7 +69,6 @@
                                         track-by="id"
                                         label="name"
                                         :multiple="true"
-                                        :allow-empty="true"
                                         :close-on-select="false"
                                         :clear-on-select="false"
                                         :preserve-search="true"
@@ -82,25 +80,131 @@
                                 </div>
                             </div>
 
-                            <h2 class="mt-5">{{ $t("Effective Date Range") }}</h2>
+                            <h2 class="mt-5">{{ $t("Date and Time") }}</h2>
 
-                            <!-- Start Date Time -->
+                            <div>⚠️ {{ $t("warningTimezone") }}</div>
+
+                            <!-- Strategy -->
                             <div class="my-3">
-                                <label for="start_date" class="form-label">{{ $t("Start Date") }}</label>
-                                <input
-                                    id="start_date" v-model="maintenance.start_date" :type="'datetime-local'"
-                                    class="form-control" :class="{'dark-calendar': dark }" required
-                                >
+                                <label for="strategy" class="form-label">{{ $t("Strategy") }}</label>
+                                <select id="strategy" v-model="maintenance.strategy" class="form-select">
+                                    <option value="manual">{{ $t("strategyManual") }}</option>
+                                    <option value="single">Single Maintenance Window</option>
+                                    <option value="recurring-interval">{{ $t("Recurring") }} - {{ $t("recurringInterval") }}</option>
+                                    <option value="recurring-weekday">{{ $t("Recurring") }} - Weekday</option>
+                                    <option value="recurring-day-of-month">{{ $t("Recurring") }} - Day of Month</option>
+                                    <option v-if="false" value="recurring-day-of-year">{{ $t("Recurring") }} - Day of Year</option>
+                                </select>
                             </div>
 
-                            <!-- End Date Time -->
-                            <div class="my-3">
-                                <label for="end_date" class="form-label">{{ $t("End Date") }}</label>
-                                <input
-                                    id="end_date" v-model="maintenance.end_date" :type="'datetime-local'"
-                                    class="form-control" :class="{'dark-calendar': dark }" required
-                                >
-                            </div>
+                            <!-- Single Maintenance Window -->
+                            <template v-if="maintenance.strategy === 'single'">
+                                <!-- DateTime Range -->
+                                <div class="my-3">
+                                    <label class="form-label">{{ $t("DateTime Range") }}</label>
+                                    <Datepicker
+                                        v-model="maintenance.dateTimeRange"
+                                        :dark="$root.isDark"
+                                        range textInput
+                                        :monthChangeOnScroll="false"
+                                        :minDate="minDate"
+                                        format="yyyy-MM-dd HH:mm"
+                                        utc="preserve"
+                                    />
+                                </div>
+                            </template>
+
+                            <!-- Recurring - Interval -->
+                            <template v-if="maintenance.strategy === 'recurring-interval'">
+                                <div class="my-3">
+                                    <label for="interval-day" class="form-label">
+                                        {{ $t("recurringInterval") }}
+
+                                        <template v-if="maintenance.intervalDay >= 1">
+                                            ({{
+                                                $tc("recurringIntervalMessage", maintenance.intervalDay, [
+                                                    maintenance.intervalDay
+                                                ])
+                                            }})
+                                        </template>
+                                    </label>
+                                    <input id="interval-day" v-model="maintenance.intervalDay" type="number" class="form-control" required min="1" max="3650" step="1">
+                                </div>
+                            </template>
+
+                            <!-- Recurring - Weekday -->
+                            <template v-if="maintenance.strategy === 'recurring-weekday'">
+                                <div class="my-3">
+                                    <label for="interval-day" class="form-label">
+                                        {{ $t("Weekday") }}
+                                    </label>
+
+                                    <!-- Weekday Picker -->
+                                    <div class="weekday-picker">
+                                        <div v-for="(weekday, index) in weekdays" :key="index">
+                                            <label class="form-check-label" :for="weekday.id">{{ $t(weekday.langKey) }}</label>
+                                            <div class="form-check-inline"><input :id="weekday.id" v-model="maintenance.weekdays" type="checkbox" :value="weekday.value" class="form-check-input"></div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </template>
+
+                            <!-- Recurring - Day of month -->
+                            <template v-if="maintenance.strategy === 'recurring-day-of-month'">
+                                <div class="my-3">
+                                    <label for="interval-day" class="form-label">
+                                        {{ $t("dayOfMonth") }}
+                                    </label>
+
+                                    <!-- Day Picker -->
+                                    <div class="day-picker">
+                                        <div v-for="index in 31" :key="index">
+                                            <label class="form-check-label" :for="'day' + index">{{ index }}</label>
+                                            <div class="form-check-inline">
+                                                <input :id="'day' + index" v-model="maintenance.daysOfMonth" type="checkbox" :value="index" class="form-check-input">
+                                            </div>
+                                        </div>
+                                    </div>
+
+                                    <div class="mt-3 mb-2">{{ $t("lastDay") }}</div>
+
+                                    <div v-for="(lastDay, index) in lastDays" :key="index" class="form-check">
+                                        <input :id="lastDay.langKey" v-model="maintenance.daysOfMonth" type="checkbox" :value="lastDay.value" class="form-check-input">
+                                        <label class="form-check-label" :for="lastDay.langKey">
+                                            {{ $t(lastDay.langKey) }}
+                                        </label>
+                                    </div>
+                                </div>
+                            </template>
+
+                            <!-- For any recurring types -->
+                            <template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month'">
+                                <!-- Maintenance Time Window of a Day -->
+                                <div class="my-3">
+                                    <label class="form-label">{{ $t("Maintenance Time Window of a Day") }}</label>
+                                    <Datepicker
+                                        v-model="maintenance.timeRange"
+                                        :dark="$root.isDark"
+                                        timePicker disableTimeRangeValidation range
+                                        placeholder="Select Time"
+                                        textInput
+                                    />
+                                </div>
+
+                                <!-- Date Range -->
+                                <div class="my-3">
+                                    <label class="form-label">{{ $t("Effective Date Range") }}</label>
+                                    <Datepicker
+                                        v-model="maintenance.dateRange"
+                                        :dark="$root.isDark"
+                                        range textInput datePicker
+                                        :monthChangeOnScroll="false"
+                                        :minDate="minDate"
+                                        :enableTimePicker="false"
+                                        utc="preserve"
+                                    />
+                                </div>
+                            </template>
 
                             <div class="mt-4 mb-1">
                                 <button
@@ -122,12 +226,15 @@
 
 import { useToast } from "vue-toastification";
 import VueMultiselect from "vue-multiselect";
+import dayjs from "dayjs";
+import Datepicker from "@vuepic/vue-datepicker";
 
 const toast = useToast();
 
 export default {
     components: {
         VueMultiselect,
+        Datepicker
     },
 
     data() {
@@ -139,6 +246,63 @@ export default {
             showOnAllPages: false,
             selectedStatusPages: [],
             dark: (this.$root.theme === "dark"),
+            neverEnd: false,
+            minDate: this.$root.date(dayjs()) + " 00:00",
+            lastDays: [
+                {
+                    langKey: "lastDay1",
+                    value: "lastDay1",
+                },
+                {
+                    langKey: "lastDay2",
+                    value: "lastDay2",
+                },
+                {
+                    langKey: "lastDay3",
+                    value: "lastDay3",
+                },
+                {
+                    langKey: "lastDay4",
+                    value: "lastDay4",
+                }
+            ],
+            weekdays: [
+                {
+                    id: "weekday1",
+                    langKey: "weekdayShortMon",
+                    value: 1,
+                },
+                {
+                    id: "weekday2",
+                    langKey: "weekdayShortTue",
+                    value: 2,
+                },
+                {
+                    id: "weekday3",
+                    langKey: "weekdayShortWed",
+                    value: 3,
+                },
+                {
+                    id: "weekday4",
+                    langKey: "weekdayShortTue",
+                    value: 4,
+                },
+                {
+                    id: "weekday5",
+                    langKey: "weekdayShortFri",
+                    value: 5,
+                },
+                {
+                    id: "weekday6",
+                    langKey: "weekdayShortSat",
+                    value: 6,
+                },
+                {
+                    id: "weekday7",
+                    langKey: "weekdayShortSun",
+                    value: 7,
+                },
+            ],
         };
     },
 
@@ -169,8 +333,13 @@ export default {
     watch: {
         "$route.fullPath"() {
             this.init();
-        }
+        },
 
+        neverEnd(value) {
+            if (value) {
+                this.maintenance.recurringEndDate = "";
+            }
+        },
     },
     mounted() {
         this.init();
@@ -195,8 +364,21 @@ export default {
                 this.maintenance = {
                     title: "",
                     description: "",
-                    start_date: "",
-                    end_date: "",
+                    strategy: "single",
+                    active: 1,
+                    recurringStartDate: this.$root.date(dayjs()),
+                    recurringEndDate: "",
+                    intervalDay: 1,
+                    dateTimeRange: [ this.minDate ],
+                    timeRange: [{
+                        hours: 2,
+                        minutes: 0,
+                    }, {
+                        hours: 3,
+                        minutes: 0,
+                    }],
+                    weekdays: [],
+                    daysOfMonth: [],
                 };
             } else if (this.isEdit) {
                 this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
@@ -332,4 +514,38 @@ textarea {
 .dark-calendar::-webkit-calendar-picker-indicator {
     filter: invert(1);
 }
+
+.weekday-picker {
+    display: flex;
+    gap: 10px;
+
+    & > div {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        width: 40px;
+
+        .form-check-inline {
+            margin-right: 0;
+        }
+    }
+}
+
+.day-picker {
+    display: flex;
+    gap: 10px;
+    flex-wrap: wrap;
+
+    & > div {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        width: 40px;
+
+        .form-check-inline {
+            margin-right: 0;
+        }
+    }
+}
+
 </style>
diff --git a/src/pages/ManageMaintenance.vue b/src/pages/ManageMaintenance.vue
index bcd47acf..4bfa9059 100644
--- a/src/pages/ManageMaintenance.vue
+++ b/src/pages/ManageMaintenance.vue
@@ -44,6 +44,10 @@
                 </div>
             </div>
 
+            <div class="text-center mt-3" style="font-size: 13px;">
+                <a href="https://github.com/louislam/uptime-kuma/wiki/Maintenance" target="_blank">Learn More</a>
+            </div>
+
             <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMaintenance">
                 {{ $t("deleteMaintenanceMsg") }}
             </Confirm>

From f11dfc8f437cb8e2ee9c77c9450bffc4d5e121af Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 24 Sep 2022 19:18:24 +0800
Subject: [PATCH 055/134] [WIP] Add/Edit Maintenance with new UI and recurring

---
 server/model/maintenance.js                   | 81 ++++++++++++++++++-
 .../maintenance-socket-handler.js             | 11 ++-
 src/pages/EditMaintenance.vue                 |  6 +-
 src/util.js                                   | 44 +++++++++-
 src/util.ts                                   | 49 +++++++++++
 5 files changed, 179 insertions(+), 12 deletions(-)

diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index 2f3e2000..a27a358b 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -4,6 +4,8 @@ let timezone = require("dayjs/plugin/timezone");
 dayjs.extend(utc);
 dayjs.extend(timezone);
 const { BeanModel } = require("redbean-node/dist/bean-model");
+const { parseVueDatePickerTimeFormat, parseTimeFormatFromVueDatePicker } = require("../../src/util");
+const { isArray } = require("chart.js/helpers");
 
 class Maintenance extends BeanModel {
 
@@ -13,15 +15,52 @@ class Maintenance extends BeanModel {
      * @returns {Object}
      */
     async toPublicJSON() {
-        return {
+
+        let dateTimeRange = [];
+        if (this.start_datetime) {
+            dateTimeRange.push( this.start_datetime);
+            if (this.end_datetime) {
+                dateTimeRange.push( this.end_datetime);
+            }
+        }
+
+        let dateRange = [];
+        if (this.start_date) {
+            dateRange.push( this.start_date);
+            if (this.end_date) {
+                dateRange.push( this.end_date);
+            }
+        }
+
+        let timeRange = [];
+        let startTime = parseVueDatePickerTimeFormat(this.start_time);
+        timeRange.push(startTime);
+        let endTime = parseVueDatePickerTimeFormat(this.end_time);
+        timeRange.push(endTime);
+
+        let obj = {
             id: this.id,
             title: this.title,
             description: this.description,
-            start_date: this.start_date,
-            end_date: this.end_date,
             strategy: this.strategy,
+            intervalDay: this.interval_day,
             active: !!this.active,
+            dateTimeRange: dateTimeRange,
+            dateRange: dateRange,
+            timeRange: timeRange,
+            weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
+            daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
         };
+
+        if (!isArray(obj.weekdays)) {
+            obj.weekdays = [];
+        }
+
+        if (!isArray(obj.daysOfMonth)) {
+            obj.daysOfMonth = [];
+        }
+
+        return obj;
     }
 
     /**
@@ -31,6 +70,42 @@ class Maintenance extends BeanModel {
     async toJSON() {
         return this.toPublicJSON();
     }
+
+    static jsonToBean(bean, obj) {
+        if (obj.id) {
+            bean.id = obj.id;
+        }
+
+        bean.title = obj.title;
+        bean.description = obj.description;
+        bean.strategy = obj.strategy;
+        bean.interval_day = obj.intervalDay;
+        bean.active = obj.active;
+
+        if (obj.dateRange[0]) {
+            bean.start_date = obj.dateRange[0];
+
+            if (obj.dateRange[1]) {
+                bean.end_date = obj.dateRange[1];
+            }
+        }
+
+        if (obj.dateTimeRange[0]) {
+            bean.start_datetime = obj.dateTimeRange[0];
+
+            if (obj.dateTimeRange[1]) {
+                bean.end_datetime = obj.dateTimeRange[1];
+            }
+        }
+
+        bean.start_time = parseTimeFormatFromVueDatePicker(obj.timeRange[0]);
+        bean.end_time = parseTimeFormatFromVueDatePicker(obj.timeRange[1]);
+
+        bean.weekdays = JSON.stringify(obj.weekdays);
+        bean.days_of_month = JSON.stringify(obj.daysOfMonth);
+
+        return bean;
+    }
 }
 
 module.exports = Maintenance;
diff --git a/server/socket-handlers/maintenance-socket-handler.js b/server/socket-handlers/maintenance-socket-handler.js
index 113c336a..df4a9a35 100644
--- a/server/socket-handlers/maintenance-socket-handler.js
+++ b/server/socket-handlers/maintenance-socket-handler.js
@@ -3,6 +3,7 @@ const { log } = require("../../src/util");
 const { R } = require("redbean-node");
 const apicache = require("../modules/apicache");
 const { UptimeKumaServer } = require("../uptime-kuma-server");
+const Maintenance = require("../model/maintenance");
 const server = UptimeKumaServer.getInstance();
 
 /**
@@ -14,9 +15,10 @@ module.exports.maintenanceSocketHandler = (socket) => {
     socket.on("addMaintenance", async (maintenance, callback) => {
         try {
             checkLogin(socket);
-            let bean = R.dispense("maintenance");
 
-            bean.import(maintenance);
+            log.debug("maintenance", maintenance);
+
+            let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
             bean.user_id = socket.userID;
             let maintenanceID = await R.store(bean);
 
@@ -47,10 +49,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
                 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;
+            Maintenance.jsonToBean(bean, maintenance);
 
             await R.store(bean);
 
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 98d5c2b2..3d8ab838 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -366,10 +366,9 @@ export default {
                     description: "",
                     strategy: "single",
                     active: 1,
-                    recurringStartDate: this.$root.date(dayjs()),
-                    recurringEndDate: "",
                     intervalDay: 1,
                     dateTimeRange: [ this.minDate ],
+                    dateRange: [],
                     timeRange: [{
                         hours: 2,
                         minutes: 0,
@@ -426,6 +425,8 @@ export default {
                 return this.processing = false;
             }
 
+            /*
+            TODO: Temporary disable
             if (this.maintenance.start_date >= this.maintenance.end_date) {
                 toast.error(this.$t("maintenanceInvalidDate"));
                 return this.processing = false;
@@ -438,6 +439,7 @@ export default {
 
             this.maintenance.start_date = this.$root.toUTC(this.maintenance.start_date);
             this.maintenance.end_date = this.$root.toUTC(this.maintenance.end_date);
+            */
 
             if (this.isAdd) {
                 this.$root.addMaintenance(this.maintenance, async (res) => {
diff --git a/src/util.js b/src/util.js
index c5fca856..73f5369d 100644
--- a/src/util.js
+++ b/src/util.js
@@ -7,7 +7,7 @@
 // Backend uses the compiled file util.js
 // Frontend uses util.ts
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = 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;
+exports.parseTimeFormatFromVueDatePicker = exports.parseVueDatePickerTimeFormat = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = 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 = _dayjs;
 exports.isDev = process.env.NODE_ENV === "development";
@@ -309,3 +309,45 @@ function getMaintenanceRelativeURL(id) {
     return "/maintenance/" + id;
 }
 exports.getMaintenanceRelativeURL = getMaintenanceRelativeURL;
+/**
+ * Parse to Time Object that used in VueDatePicker
+ * @param {string} time E.g. 12:00
+ * @returns object
+ */
+function parseVueDatePickerTimeFormat(time) {
+    if (!time) {
+        return {
+            hours: 0,
+            minutes: 0,
+        };
+    }
+    let array = time.split(":");
+    if (array.length < 2) {
+        throw new Error("parseVueDatePickerTimeFormat: Invalid Time");
+    }
+    let obj = {
+        hours: parseInt(array[0]),
+        minutes: parseInt(array[1]),
+        seconds: 0,
+    };
+    if (array.length >= 3) {
+        obj.seconds = parseInt(array[2]);
+    }
+    return obj;
+}
+exports.parseVueDatePickerTimeFormat = parseVueDatePickerTimeFormat;
+/**
+ * @returns string e.g. 12:00
+ */
+function parseTimeFormatFromVueDatePicker(obj) {
+    if (!obj) {
+        return obj;
+    }
+    let result = "";
+    result += obj.hours.toString().padStart(2, "0") + ":" + obj.minutes.toString().padStart(2, "0");
+    if (obj.seconds) {
+        result += ":" + obj.seconds.toString().padStart(2, "0");
+    }
+    return result;
+}
+exports.parseTimeFormatFromVueDatePicker = parseTimeFormatFromVueDatePicker;
diff --git a/src/util.ts b/src/util.ts
index 0dd8a62a..92da0fd5 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -342,3 +342,52 @@ export function getMonitorRelativeURL(id: string) {
 export function getMaintenanceRelativeURL(id: string) {
     return "/maintenance/" + id;
 }
+
+/**
+ * Parse to Time Object that used in VueDatePicker
+ * @param {string} time E.g. 12:00
+ * @returns object
+ */
+export function parseVueDatePickerTimeFormat(time: string) {
+    if (!time) {
+        return {
+            hours: 0,
+            minutes: 0,
+        };
+    }
+
+    let array = time.split(":");
+
+    if (array.length < 2) {
+        throw new Error("parseVueDatePickerTimeFormat: Invalid Time");
+    }
+
+    let obj =  {
+        hours: parseInt(array[0]),
+        minutes: parseInt(array[1]),
+        seconds: 0,
+    }
+    if (array.length >= 3) {
+        obj.seconds = parseInt(array[2]);
+    }
+    return obj;
+}
+
+/**
+ * @returns string e.g. 12:00
+ */
+export function parseTimeFormatFromVueDatePicker(obj : any) {
+    if (!obj) {
+        return obj;
+    }
+
+    let result = "";
+
+    result += obj.hours.toString().padStart(2, "0") + ":" + obj.minutes.toString().padStart(2, "0")
+
+    if (obj.seconds) {
+        result += ":" +  obj.seconds.toString().padStart(2, "0")
+    }
+
+    return result;
+}

From 3f63cb246b2d973fe175ddbe4e34cca34ab4d8e4 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sun, 25 Sep 2022 19:38:28 +0800
Subject: [PATCH 056/134] [WIP] Handle timezone offset for timeRange

---
 server/model/maintenance.js                   | 41 +++++++++++++----
 .../maintenance-socket-handler.js             | 17 ++++---
 server/util-server.js                         | 46 +++++++++++++++++++
 src/languages/en.js                           |  2 +-
 src/pages/EditMaintenance.vue                 | 21 +++++----
 src/util.js                                   | 10 ++--
 src/util.ts                                   |  5 +-
 7 files changed, 110 insertions(+), 32 deletions(-)

diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index a27a358b..3b07d5f7 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -4,17 +4,19 @@ let timezone = require("dayjs/plugin/timezone");
 dayjs.extend(utc);
 dayjs.extend(timezone);
 const { BeanModel } = require("redbean-node/dist/bean-model");
-const { parseVueDatePickerTimeFormat, parseTimeFormatFromVueDatePicker } = require("../../src/util");
+const { parseTimeObject, parseTimeFromTimeObject } = require("../../src/util");
 const { isArray } = require("chart.js/helpers");
+const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
 
 class Maintenance extends BeanModel {
 
     /**
      * Return an object that ready to parse to JSON for public
      * Only show necessary data to public
+     * @param {string} timezone If not specified, the timeRange will be in UTC
      * @returns {Object}
      */
-    async toPublicJSON() {
+    async toPublicJSON(timezone = null) {
 
         let dateTimeRange = [];
         if (this.start_datetime) {
@@ -33,11 +35,21 @@ class Maintenance extends BeanModel {
         }
 
         let timeRange = [];
-        let startTime = parseVueDatePickerTimeFormat(this.start_time);
+        let startTime = parseTimeObject(this.start_time);
         timeRange.push(startTime);
-        let endTime = parseVueDatePickerTimeFormat(this.end_time);
+        let endTime = parseTimeObject(this.end_time);
         timeRange.push(endTime);
 
+        // Apply timezone offset
+        if (timezone) {
+            if (this.start_time) {
+                timeObjectToLocal(startTime, timezone);
+            }
+            if (this.end_time) {
+                timeObjectToLocal(endTime, timezone);
+            }
+        }
+
         let obj = {
             id: this.id,
             title: this.title,
@@ -65,17 +77,28 @@ class Maintenance extends BeanModel {
 
     /**
      * Return an object that ready to parse to JSON
+     * @param {string} timezone If not specified, the timeRange will be in UTC
      * @returns {Object}
      */
-    async toJSON() {
-        return this.toPublicJSON();
+    async toJSON(timezone = null) {
+        return this.toPublicJSON(timezone);
     }
 
-    static jsonToBean(bean, obj) {
+    static jsonToBean(bean, obj, timezone) {
         if (obj.id) {
             bean.id = obj.id;
         }
 
+        // Apply timezone offset to timeRange, as it cannot apply automatically.
+        if (timezone) {
+            if (obj.timeRange[0]) {
+                timeObjectToUTC(obj.timeRange[0], timezone);
+                if (obj.timeRange[1]) {
+                    timeObjectToUTC(obj.timeRange[1], timezone);
+                }
+            }
+        }
+
         bean.title = obj.title;
         bean.description = obj.description;
         bean.strategy = obj.strategy;
@@ -98,8 +121,8 @@ class Maintenance extends BeanModel {
             }
         }
 
-        bean.start_time = parseTimeFormatFromVueDatePicker(obj.timeRange[0]);
-        bean.end_time = parseTimeFormatFromVueDatePicker(obj.timeRange[1]);
+        bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
+        bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
 
         bean.weekdays = JSON.stringify(obj.weekdays);
         bean.days_of_month = JSON.stringify(obj.daysOfMonth);
diff --git a/server/socket-handlers/maintenance-socket-handler.js b/server/socket-handlers/maintenance-socket-handler.js
index df4a9a35..604f07bd 100644
--- a/server/socket-handlers/maintenance-socket-handler.js
+++ b/server/socket-handlers/maintenance-socket-handler.js
@@ -5,6 +5,11 @@ const apicache = require("../modules/apicache");
 const { UptimeKumaServer } = require("../uptime-kuma-server");
 const Maintenance = require("../model/maintenance");
 const server = UptimeKumaServer.getInstance();
+const dayjs = require("dayjs");
+const utc = require("dayjs/plugin/utc");
+let timezone = require("dayjs/plugin/timezone");
+dayjs.extend(utc);
+dayjs.extend(timezone);
 
 /**
  * Handlers for Maintenance
@@ -12,13 +17,13 @@ const server = UptimeKumaServer.getInstance();
  */
 module.exports.maintenanceSocketHandler = (socket) => {
     // Add a new maintenance
-    socket.on("addMaintenance", async (maintenance, callback) => {
+    socket.on("addMaintenance", async (maintenance, timezone, callback) => {
         try {
             checkLogin(socket);
 
             log.debug("maintenance", maintenance);
 
-            let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
+            let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance, timezone);
             bean.user_id = socket.userID;
             let maintenanceID = await R.store(bean);
 
@@ -39,7 +44,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
     });
 
     // Edit a maintenance
-    socket.on("editMaintenance", async (maintenance, callback) => {
+    socket.on("editMaintenance", async (maintenance, timezone, callback) => {
         try {
             checkLogin(socket);
 
@@ -49,7 +54,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
                 throw new Error("Permission denied.");
             }
 
-            Maintenance.jsonToBean(bean, maintenance);
+            Maintenance.jsonToBean(bean, maintenance, timezone);
 
             await R.store(bean);
 
@@ -138,7 +143,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
         }
     });
 
-    socket.on("getMaintenance", async (maintenanceID, callback) => {
+    socket.on("getMaintenance", async (maintenanceID, timezone, callback) => {
         try {
             checkLogin(socket);
 
@@ -151,7 +156,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
 
             callback({
                 ok: true,
-                maintenance: await bean.toJSON(),
+                maintenance: await bean.toJSON(timezone),
             });
 
         } catch (e) {
diff --git a/server/util-server.js b/server/util-server.js
index 1517bcfe..cc5e478d 100644
--- a/server/util-server.js
+++ b/server/util-server.js
@@ -21,6 +21,11 @@ const {
         rfc2865: { file, attributes },
     },
 } = require("node-radius-utils");
+const dayjs = require("dayjs");
+const utc = require("dayjs/plugin/utc");
+let timezone = require("dayjs/plugin/timezone");
+dayjs.extend(utc);
+dayjs.extend(timezone);
 
 // From ping-lite
 exports.WIN = /^win/.test(process.platform);
@@ -645,3 +650,44 @@ module.exports.send403 = (res, msg = "") => {
         "msg": msg,
     });
 };
+
+function timeObjectConvertTimezone(obj, timezone, timeObjectToUTC = true) {
+    // e.g. +08:00
+    let offsetString = dayjs().tz(timezone).format("Z");
+    let hours = parseInt(offsetString.substring(1, 3));
+    let minutes = parseInt(offsetString.substring(4, 6));
+
+    if (
+        (timeObjectToUTC && offsetString.startsWith("+")) ||
+        (!timeObjectToUTC && offsetString.startsWith("-"))
+    ) {
+        hours *= -1;
+        minutes *= -1;
+    }
+
+    obj.hours += hours;
+    obj.minutes += minutes;
+
+    // Handle out of bound
+    if (obj.hours < 0) {
+        obj.hours += 24;
+    } else if (obj.hours > 24) {
+        obj.hours -= 24;
+    }
+
+    if (obj.minutes < 0) {
+        obj.minutes += 24;
+    } else if (obj.minutes > 24) {
+        obj.minutes -= 24;
+    }
+
+    return obj;
+}
+
+module.exports.timeObjectToUTC = (obj, timezone) => {
+    return timeObjectConvertTimezone(obj, timezone, true);
+};
+
+module.exports.timeObjectToLocal = (obj, timezone) => {
+    return timeObjectConvertTimezone(obj, timezone, false);
+};
diff --git a/src/languages/en.js b/src/languages/en.js
index d729d3a5..e77a31f4 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -607,7 +607,7 @@ export default {
     recurringInterval: "Interval",
     "Recurring": "Recurring",
     strategyManual: "Active/Inactive Manually",
-    warningTimezone: "It is NOT your current browser's timezone. It is your server's timezone.",
+    warningTimezone: "It is using your current Device/PC's timezone.",
     weekdayShortMon: "Mon",
     weekdayShortTue: "Tue",
     weekdayShortWed: "Wed",
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 3d8ab838..640962d7 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -109,7 +109,6 @@
                                         :monthChangeOnScroll="false"
                                         :minDate="minDate"
                                         format="yyyy-MM-dd HH:mm"
-                                        utc="preserve"
                                     />
                                 </div>
                             </template>
@@ -185,8 +184,8 @@
                                     <Datepicker
                                         v-model="maintenance.timeRange"
                                         :dark="$root.isDark"
-                                        timePicker disableTimeRangeValidation range
-                                        placeholder="Select Time"
+                                        timePicker
+                                        disableTimeRangeValidation range
                                         textInput
                                     />
                                 </div>
@@ -201,7 +200,7 @@
                                         :monthChangeOnScroll="false"
                                         :minDate="minDate"
                                         :enableTimePicker="false"
-                                        utc="preserve"
+                                        :utc="true"
                                     />
                                 </div>
                             </template>
@@ -357,6 +356,9 @@ export default {
     },
     methods: {
         init() {
+            // Use browser's timezone!
+            let timezone = dayjs.tz.guess();
+
             this.affectedMonitors = [];
             this.selectedStatusPages = [];
 
@@ -380,10 +382,8 @@ export default {
                     daysOfMonth: [],
                 };
             } else if (this.isEdit) {
-                this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
+                this.$root.getSocket().emit("getMaintenance", this.$route.params.id, timezone, (res) => {
                     if (res.ok) {
-                        res.maintenance.start_date = this.$root.datetimeFormat(res.maintenance.start_date, "YYYY-MM-DDTHH:mm");
-                        res.maintenance.end_date = this.$root.datetimeFormat(res.maintenance.end_date, "YYYY-MM-DDTHH:mm");
                         this.maintenance = res.maintenance;
 
                         this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
@@ -441,8 +441,11 @@ export default {
             this.maintenance.end_date = this.$root.toUTC(this.maintenance.end_date);
             */
 
+            // Use browser's timezone!
+            let timezone = dayjs.tz.guess();
+
             if (this.isAdd) {
-                this.$root.addMaintenance(this.maintenance, async (res) => {
+                this.$root.addMaintenance(this.maintenance, timezone, async (res) => {
                     if (res.ok) {
                         await this.addMonitorMaintenance(res.maintenanceID, async () => {
                             await this.addMaintenanceStatusPage(res.maintenanceID, () => {
@@ -459,7 +462,7 @@ export default {
 
                 });
             } else {
-                this.$root.getSocket().emit("editMaintenance", this.maintenance, async (res) => {
+                this.$root.getSocket().emit("editMaintenance", this.maintenance, timezone, async (res) => {
                     if (res.ok) {
                         await this.addMonitorMaintenance(res.maintenanceID, async () => {
                             await this.addMaintenanceStatusPage(res.maintenanceID, () => {
diff --git a/src/util.js b/src/util.js
index 73f5369d..15427cdd 100644
--- a/src/util.js
+++ b/src/util.js
@@ -7,7 +7,7 @@
 // Backend uses the compiled file util.js
 // Frontend uses util.ts
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.parseTimeFormatFromVueDatePicker = exports.parseVueDatePickerTimeFormat = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = 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;
+exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = 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 = _dayjs;
 exports.isDev = process.env.NODE_ENV === "development";
@@ -314,7 +314,7 @@ exports.getMaintenanceRelativeURL = getMaintenanceRelativeURL;
  * @param {string} time E.g. 12:00
  * @returns object
  */
-function parseVueDatePickerTimeFormat(time) {
+function parseTimeObject(time) {
     if (!time) {
         return {
             hours: 0,
@@ -335,11 +335,11 @@ function parseVueDatePickerTimeFormat(time) {
     }
     return obj;
 }
-exports.parseVueDatePickerTimeFormat = parseVueDatePickerTimeFormat;
+exports.parseTimeObject = parseTimeObject;
 /**
  * @returns string e.g. 12:00
  */
-function parseTimeFormatFromVueDatePicker(obj) {
+function parseTimeFromTimeObject(obj) {
     if (!obj) {
         return obj;
     }
@@ -350,4 +350,4 @@ function parseTimeFormatFromVueDatePicker(obj) {
     }
     return result;
 }
-exports.parseTimeFormatFromVueDatePicker = parseTimeFormatFromVueDatePicker;
+exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
diff --git a/src/util.ts b/src/util.ts
index 92da0fd5..cb51250b 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -348,7 +348,7 @@ export function getMaintenanceRelativeURL(id: string) {
  * @param {string} time E.g. 12:00
  * @returns object
  */
-export function parseVueDatePickerTimeFormat(time: string) {
+export function parseTimeObject(time: string) {
     if (!time) {
         return {
             hours: 0,
@@ -376,7 +376,7 @@ export function parseVueDatePickerTimeFormat(time: string) {
 /**
  * @returns string e.g. 12:00
  */
-export function parseTimeFormatFromVueDatePicker(obj : any) {
+export function parseTimeFromTimeObject(obj : any) {
     if (!obj) {
         return obj;
     }
@@ -391,3 +391,4 @@ export function parseTimeFormatFromVueDatePicker(obj : any) {
 
     return result;
 }
+

From 4157c7d5463a7467c849dd71fd2d17b846b81c95 Mon Sep 17 00:00:00 2001
From: Patrick <patrick@hindmar.sh>
Date: Mon, 26 Sep 2022 22:16:34 +1300
Subject: [PATCH 057/134] Add support for Squadcast incoming webhook

---
 server/notification-providers/squadcast.js | 76 ++++++++++++++++++++++
 server/notification.js                     |  2 +
 src/components/notifications/Squadcast.vue |  6 ++
 src/components/notifications/index.js      |  2 +
 src/languages/en.js                        |  1 +
 5 files changed, 87 insertions(+)
 create mode 100644 server/notification-providers/squadcast.js
 create mode 100644 src/components/notifications/Squadcast.vue

diff --git a/server/notification-providers/squadcast.js b/server/notification-providers/squadcast.js
new file mode 100644
index 00000000..0302cb6f
--- /dev/null
+++ b/server/notification-providers/squadcast.js
@@ -0,0 +1,76 @@
+const NotificationProvider = require("./notification-provider");
+const axios = require("axios");
+const { DOWN, UP } = require("../../src/util");
+
+class Squadcast extends NotificationProvider {
+
+    name = "squadcast";
+
+    async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
+        let okMsg = "Sent Successfully.";
+
+        try {
+
+            let config = {};
+            let data = {
+                message: msg,
+                description: '',
+                tags: {},
+                heartbeat: heartbeatJSON,
+                source: 'uptime-kuma'
+            }
+
+            if (heartbeatJSON !== null) {
+                data.description = heartbeatJSON["msg"];
+                data.event_id = heartbeatJSON["monitorID"];
+
+                if (heartbeatJSON["status"] === DOWN) {
+                    data.message = `${monitorJSON['name']} is DOWN`;
+                    data.status = "trigger";
+                } else {
+                    data.message = `${monitorJSON['name']} is UP`;
+                    data.status = "resolve";
+                }
+
+                let address;
+                switch (monitorJSON["type"]) {
+                    case "ping":
+                        address = monitorJSON["hostname"];
+                        break;
+                    case "port":
+                    case "dns":
+                    case "steam":
+                        address = monitorJSON["hostname"];
+                        if (monitorJSON["port"]) {
+                            address += ":" + monitorJSON["port"];
+                        }
+                        break;
+                    default:
+                        address = monitorJSON["url"];
+                        break;
+                }
+
+                data.tags["AlertAddress"] = address;
+
+                monitorJSON["tags"].forEach(tag => {
+                    data.tags[tag["name"]] = {
+                        value: tag["value"]
+                    };
+                    if (tag["color"] !== null) {
+                        data.tags[tag["name"]]["color"] = tag["color"]
+                    }
+                });
+            }
+
+            await axios.post(notification.squadcastWebhookURL, data, config);
+            return okMsg;
+
+        } catch (error) {
+            this.throwGeneralAxiosError(error);
+        }
+
+    }
+
+}
+
+module.exports = Squadcast;
\ No newline at end of file
diff --git a/server/notification.js b/server/notification.js
index 3bf51243..7a4b4f29 100644
--- a/server/notification.js
+++ b/server/notification.js
@@ -32,6 +32,7 @@ const SerwerSMS = require("./notification-providers/serwersms");
 const Signal = require("./notification-providers/signal");
 const Slack = require("./notification-providers/slack");
 const SMTP = require("./notification-providers/smtp");
+const Squadcast = require("./notification-providers/squadcast");
 const Stackfield = require("./notification-providers/stackfield");
 const Teams = require("./notification-providers/teams");
 const TechulusPush = require("./notification-providers/techulus-push");
@@ -87,6 +88,7 @@ class Notification {
             new SMSManager(),
             new Slack(),
             new SMTP(),
+            new Squadcast(),
             new Stackfield(),
             new Teams(),
             new TechulusPush(),
diff --git a/src/components/notifications/Squadcast.vue b/src/components/notifications/Squadcast.vue
new file mode 100644
index 00000000..6650c44d
--- /dev/null
+++ b/src/components/notifications/Squadcast.vue
@@ -0,0 +1,6 @@
+<template>
+    <div class="mb-3">
+        <label for="webhook-url" class="form-label">{{ $t("Post URL") }}</label>
+        <input id="webhook-url" v-model="$parent.notification.squadcastWebhookURL" type="url" pattern="https?://.+" class="form-control" required>
+    </div>
+</template>
diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js
index 6add06ea..319a7922 100644
--- a/src/components/notifications/index.js
+++ b/src/components/notifications/index.js
@@ -31,6 +31,7 @@ import SerwerSMS from "./SerwerSMS.vue";
 import Signal from "./Signal.vue";
 import SMSManager from "./SMSManager.vue";
 import Slack from "./Slack.vue";
+import Squadcast from "./Squadcast.vue";
 import Stackfield from "./Stackfield.vue";
 import STMP from "./SMTP.vue";
 import Teams from "./Teams.vue";
@@ -79,6 +80,7 @@ const NotificationFormList = {
     "signal": Signal,
     "SMSManager": SMSManager,
     "slack": Slack,
+    "squadcast": Squadcast,
     "smtp": STMP,
     "stackfield": Stackfield,
     "teams": Teams,
diff --git a/src/languages/en.js b/src/languages/en.js
index 7d980f63..1f20c7ea 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -582,4 +582,5 @@ export default {
     goAlert: "GoAlert",
     backupOutdatedWarning: "Deprecated: Since a lot of features added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.",
     backupRecommend: "Please backup the volume or the data folder (./data/) directly instead.",
+    squadcast: "Squadcast",
 };

From bef9cb6a5fb06dbd2fb76c6ce6214b3c3b54b45f Mon Sep 17 00:00:00 2001
From: Patrick <patrick@hindmar.sh>
Date: Mon, 26 Sep 2022 22:30:43 +1300
Subject: [PATCH 058/134] Linting fixes

---
 server/notification-providers/squadcast.js | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/server/notification-providers/squadcast.js b/server/notification-providers/squadcast.js
index 0302cb6f..15553ff7 100644
--- a/server/notification-providers/squadcast.js
+++ b/server/notification-providers/squadcast.js
@@ -1,6 +1,6 @@
 const NotificationProvider = require("./notification-provider");
 const axios = require("axios");
-const { DOWN, UP } = require("../../src/util");
+const { DOWN } = require("../../src/util");
 
 class Squadcast extends NotificationProvider {
 
@@ -14,21 +14,21 @@ class Squadcast extends NotificationProvider {
             let config = {};
             let data = {
                 message: msg,
-                description: '',
+                description: "",
                 tags: {},
                 heartbeat: heartbeatJSON,
-                source: 'uptime-kuma'
-            }
+                source: "uptime-kuma"
+            };
 
             if (heartbeatJSON !== null) {
                 data.description = heartbeatJSON["msg"];
                 data.event_id = heartbeatJSON["monitorID"];
 
                 if (heartbeatJSON["status"] === DOWN) {
-                    data.message = `${monitorJSON['name']} is DOWN`;
+                    data.message = `${monitorJSON["name"]} is DOWN`;
                     data.status = "trigger";
                 } else {
-                    data.message = `${monitorJSON['name']} is UP`;
+                    data.message = `${monitorJSON["name"]} is UP`;
                     data.status = "resolve";
                 }
 
@@ -57,7 +57,7 @@ class Squadcast extends NotificationProvider {
                         value: tag["value"]
                     };
                     if (tag["color"] !== null) {
-                        data.tags[tag["name"]]["color"] = tag["color"]
+                        data.tags[tag["name"]]["color"] = tag["color"];
                     }
                 });
             }
@@ -73,4 +73,4 @@ class Squadcast extends NotificationProvider {
 
 }
 
-module.exports = Squadcast;
\ No newline at end of file
+module.exports = Squadcast;

From 4002b9f57787220eda4863b76417bbc33aaed81a Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 27 Sep 2022 20:44:44 +0800
Subject: [PATCH 059/134] [WIP] Checking maintenance time using
 maintenance_timeslot table

---
 server/model/maintenance_timeslot.js          | 46 +++++++++++++++++++
 server/model/monitor.js                       | 12 ++++-
 server/model/status_page.js                   | 18 ++++----
 .../maintenance-socket-handler.js             |  3 ++
 src/components/Status.vue                     |  2 +-
 src/components/Uptime.vue                     |  2 +-
 src/languages/en.js                           |  1 +
 src/languages/zh-HK.js                        |  2 +
 src/mixins/socket.js                          |  2 +-
 9 files changed, 75 insertions(+), 13 deletions(-)
 create mode 100644 server/model/maintenance_timeslot.js

diff --git a/server/model/maintenance_timeslot.js b/server/model/maintenance_timeslot.js
new file mode 100644
index 00000000..f749caa5
--- /dev/null
+++ b/server/model/maintenance_timeslot.js
@@ -0,0 +1,46 @@
+const { BeanModel } = require("redbean-node/dist/bean-model");
+const { R } = require("redbean-node");
+const dayjs = require("dayjs");
+
+class MaintenanceTimeslot extends BeanModel {
+
+    async toPublicJSON() {
+
+    }
+
+    async toJSON() {
+
+    }
+
+    /**
+     *
+     * @param {Maintenance} maintenance
+     * @param {dayjs} startFrom (For recurring type only) Generate Timeslot from this date, if it is smaller than the current date, it will use the current date instead. As generating a passed timeslot is meaningless.
+     * @param {boolean} removeExist Remove existing timeslot before create
+     * @returns {Promise<void>}
+     */
+    static async generateTimeslot(maintenance, startFrom = null, removeExist = false) {
+        if (!startFrom) {
+            startFrom = dayjs();
+        }
+
+        if (removeExist) {
+            await R.exec("DELETE FROM maintenance_timeslot WHERE maintenance_id = ? ", [
+                maintenance.id
+            ]);
+        }
+
+        if (maintenance.strategy === "single") {
+            let bean = R.dispense("maintenance_timeslot");
+            bean.maintenance_id = maintenance.id;
+            bean.start_date = maintenance.start_datetime;
+            bean.end_date = maintenance.end_datetime;
+            bean.generated_next = true;
+            await R.store(bean);
+        } else {
+            throw new Error("Unknown maintenance strategy");
+        }
+    }
+}
+
+module.exports = MaintenanceTimeslot;
diff --git a/server/model/monitor.js b/server/model/monitor.js
index 5a74215e..a13d7051 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -1105,7 +1105,17 @@ class Monitor extends BeanModel {
      * @returns {Promise<boolean>}
      */
     static async isUnderMaintenance(monitorID) {
-        const maintenance = await R.getRow("SELECT COUNT(*) AS count FROM monitor_maintenance mm JOIN maintenance ON mm.maintenance_id = maintenance.id WHERE mm.monitor_id = ? AND datetime(maintenance.start_date) <= datetime('now') AND datetime(maintenance.end_date) >= datetime('now') LIMIT 1", [ monitorID ]);
+        const maintenance = await R.getRow(`
+            SELECT COUNT(*) AS count
+            FROM monitor_maintenance mm
+            JOIN maintenance
+                ON mm.maintenance_id = maintenance.id
+            JOIN maintenance_timeslot
+                ON maintenance_timeslot.maintenance_id = maintenance.id
+            WHERE mm.monitor_id = ?
+                AND maintenance_timeslot.start_date <= DATETIME('now')
+                AND maintenance_timeslot.end_date >= DATETIME('now')
+            LIMIT 1`, [ monitorID ]);
         return maintenance.count !== 0;
     }
 }
diff --git a/server/model/status_page.js b/server/model/status_page.js
index d296470d..4351db58 100644
--- a/server/model/status_page.js
+++ b/server/model/status_page.js
@@ -272,15 +272,15 @@ class StatusPage extends BeanModel {
             const publicMaintenanceList = [];
 
             let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
-            SELECT m.*
-            FROM maintenance m
-            JOIN maintenance_status_page msp
-            ON msp.maintenance_id = m.id
-            WHERE datetime(m.start_date) <= datetime('now')
-              AND datetime(m.end_date) >= datetime('now')
-              AND msp.status_page_id = ?
-            ORDER BY m.end_date
-        `, [ statusPageId ]));
+                SELECT m.*
+                FROM maintenance m, maintenance_status_page msp, maintenance_timeslot
+                WHERE  msp.maintenance_id = m.id
+                AND maintenance_timeslot.maintenance.id = m.id
+                AND maintenance_timeslot.start_date <= DATETIME('now')
+                AND maintenance_timeslot.end_date >= DATETIME('now')
+                AND msp.status_page_id = ?
+                ORDER BY m.end_date
+            `, [ statusPageId ]));
 
             for (const bean of maintenanceBeanList) {
                 publicMaintenanceList.push(await bean.toPublicJSON());
diff --git a/server/socket-handlers/maintenance-socket-handler.js b/server/socket-handlers/maintenance-socket-handler.js
index 604f07bd..5358b53e 100644
--- a/server/socket-handlers/maintenance-socket-handler.js
+++ b/server/socket-handlers/maintenance-socket-handler.js
@@ -8,6 +8,7 @@ const server = UptimeKumaServer.getInstance();
 const dayjs = require("dayjs");
 const utc = require("dayjs/plugin/utc");
 let timezone = require("dayjs/plugin/timezone");
+const MaintenanceTimeslot = require("../model/maintenance_timeslot");
 dayjs.extend(utc);
 dayjs.extend(timezone);
 
@@ -26,6 +27,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
             let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance, timezone);
             bean.user_id = socket.userID;
             let maintenanceID = await R.store(bean);
+            await MaintenanceTimeslot.generateTimeslot(bean);
 
             await server.sendMaintenanceList(socket);
 
@@ -57,6 +59,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
             Maintenance.jsonToBean(bean, maintenance, timezone);
 
             await R.store(bean);
+            await MaintenanceTimeslot.generateTimeslot(bean, null, true);
 
             await server.sendMaintenanceList(socket);
 
diff --git a/src/components/Status.vue b/src/components/Status.vue
index 391fb6d5..92ed8a6b 100644
--- a/src/components/Status.vue
+++ b/src/components/Status.vue
@@ -47,7 +47,7 @@ export default {
             }
 
             if (this.status === 3) {
-                return this.$t("Maintenance");
+                return this.$t("statusMaintenance");
             }
 
             return this.$t("Unknown");
diff --git a/src/components/Uptime.vue b/src/components/Uptime.vue
index f089d4c1..8565975c 100644
--- a/src/components/Uptime.vue
+++ b/src/components/Uptime.vue
@@ -26,7 +26,7 @@ export default {
         uptime() {
 
             if (this.type === "maintenance") {
-                return this.$t("Maintenance");
+                return this.$t("statusMaintenance");
             }
 
             let key = this.monitor.id + "_" + this.type;
diff --git a/src/languages/en.js b/src/languages/en.js
index e77a31f4..fdcaf98e 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -10,6 +10,7 @@ export default {
     maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
     acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
     Maintenance: "Maintenance",
+    statusMaintenance: "Maintenance",
     "Schedule maintenance": "Schedule maintenance",
     "Affected Monitors": "Affected Monitors",
     "Pick Affected Monitors...": "Pick Affected Monitors...",
diff --git a/src/languages/zh-HK.js b/src/languages/zh-HK.js
index a55f4fb6..cd82be84 100644
--- a/src/languages/zh-HK.js
+++ b/src/languages/zh-HK.js
@@ -380,4 +380,6 @@ export default {
     proxyDescription: "必須將代理伺服器指派給監測器才能運作。",
     enableProxyDescription: "此代理伺服器在啟用前不會在監測器上生效,您可以藉由控制啟用狀態來暫時對所有的監測器停用代理伺服器。",
     setAsDefaultProxyDescription: "預設情況下,新監測器將啟用此代理伺服器。您仍可分別停用各監測器的代理伺服器。",
+    Maintenance: "維護",
+    statusMaintenance: "維護中",
 };
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
index 6da6ee64..74522ffe 100644
--- a/src/mixins/socket.js
+++ b/src/mixins/socket.js
@@ -588,7 +588,7 @@ export default {
 
                 if (this.monitorList[monitorID].maintenance) {
                     result[monitorID] = {
-                        text: this.$t("Maintenance"),
+                        text: this.$t("statusMaintenance"),
                         color: "maintenance",
                     };
                 } else if (! lastHeartBeat) {

From b1465c0282dde17c34171b3978c96cdac2c7dace Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Wed, 28 Sep 2022 00:20:17 +0800
Subject: [PATCH 060/134] - Maintenance standardize datetime format to
 YYYY-MM-DD hh:mm:ss - Import dayjs extensions one time only - Maintenance
 activeCondition centralize

---
 server/model/heartbeat.js                     |  4 --
 server/model/maintenance.js                   | 38 +++++++++++++------
 server/model/maintenance_timeslot.js          |  5 ++-
 server/model/monitor.js                       |  9 ++---
 server/model/status_page.js                   |  9 +++--
 server/server.js                              |  5 +++
 .../maintenance-socket-handler.js             |  4 --
 server/util-server.js                         |  4 --
 src/components/Datetime.vue                   |  6 ---
 src/components/PingChart.vue                  |  6 +--
 src/components/settings/General.vue           |  4 --
 src/main.js                                   |  7 ++++
 src/mixins/datetime.js                        |  6 ---
 src/util-frontend.js                          |  5 ---
 14 files changed, 51 insertions(+), 61 deletions(-)

diff --git a/server/model/heartbeat.js b/server/model/heartbeat.js
index 8c9d8645..fa02cae8 100644
--- a/server/model/heartbeat.js
+++ b/server/model/heartbeat.js
@@ -1,8 +1,4 @@
 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");
 
 /**
diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index 3b07d5f7..945e4d97 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -1,12 +1,9 @@
 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");
 const { parseTimeObject, parseTimeFromTimeObject } = require("../../src/util");
 const { isArray } = require("chart.js/helpers");
 const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
+const { R } = require("redbean-node");
 
 class Maintenance extends BeanModel {
 
@@ -20,17 +17,18 @@ class Maintenance extends BeanModel {
 
         let dateTimeRange = [];
         if (this.start_datetime) {
-            dateTimeRange.push( this.start_datetime);
+
+            dateTimeRange.push(dayjs.utc(this.start_datetime).toISOString());
             if (this.end_datetime) {
-                dateTimeRange.push( this.end_datetime);
+                dateTimeRange.push(dayjs.utc(this.end_datetime).toISOString());
             }
         }
 
         let dateRange = [];
         if (this.start_date) {
-            dateRange.push( this.start_date);
+            dateRange.push(dayjs.utc(this.start_date).toISOString());
             if (this.end_date) {
-                dateRange.push( this.end_date);
+                dateRange.push(dayjs.utc(this.end_date).toISOString());
             }
         }
 
@@ -106,18 +104,18 @@ class Maintenance extends BeanModel {
         bean.active = obj.active;
 
         if (obj.dateRange[0]) {
-            bean.start_date = obj.dateRange[0];
+            bean.start_date = R.isoDate(dayjs(obj.dateRange[0]).utc());
 
             if (obj.dateRange[1]) {
-                bean.end_date = obj.dateRange[1];
+                bean.end_date = R.isoDate(dayjs(obj.dateRange[1]).utc());
             }
         }
 
         if (obj.dateTimeRange[0]) {
-            bean.start_datetime = obj.dateTimeRange[0];
+            bean.start_datetime = R.isoDateTime(dayjs(obj.dateTimeRange[0]).utc());
 
             if (obj.dateTimeRange[1]) {
-                bean.end_datetime = obj.dateTimeRange[1];
+                bean.end_datetime = R.isoDateTime(dayjs(obj.dateTimeRange[1]).utc());
             }
         }
 
@@ -129,6 +127,22 @@ class Maintenance extends BeanModel {
 
         return bean;
     }
+
+    /**
+     * SQL conditions for active maintenance
+     * @returns {string}
+     */
+    static getActiveMaintenanceSQLCondition() {
+        return `
+
+            (maintenance_timeslot.start_date <= DATETIME('now')
+            AND maintenance_timeslot.end_date >= DATETIME('now')
+            AND maintenance.active = 1)
+            AND
+            (maintenance.strategy = 'manual' AND active = 1)
+
+        `;
+    }
 }
 
 module.exports = Maintenance;
diff --git a/server/model/maintenance_timeslot.js b/server/model/maintenance_timeslot.js
index f749caa5..7917dd69 100644
--- a/server/model/maintenance_timeslot.js
+++ b/server/model/maintenance_timeslot.js
@@ -1,6 +1,7 @@
 const { BeanModel } = require("redbean-node/dist/bean-model");
 const { R } = require("redbean-node");
 const dayjs = require("dayjs");
+const { log } = require("../../src/util");
 
 class MaintenanceTimeslot extends BeanModel {
 
@@ -30,7 +31,9 @@ class MaintenanceTimeslot extends BeanModel {
             ]);
         }
 
-        if (maintenance.strategy === "single") {
+        if (maintenance.strategy === "manual") {
+            log.debug("maintenance", "No need to generate timeslot for manual type");
+        } else if (maintenance.strategy === "single") {
             let bean = R.dispense("maintenance_timeslot");
             bean.maintenance_id = maintenance.id;
             bean.start_date = maintenance.start_datetime;
diff --git a/server/model/monitor.js b/server/model/monitor.js
index a13d7051..9df127b0 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -1,9 +1,5 @@
 const https = require("https");
 const dayjs = require("dayjs");
-const utc = require("dayjs/plugin/utc");
-let timezone = require("dayjs/plugin/timezone");
-dayjs.extend(utc);
-dayjs.extend(timezone);
 const axios = require("axios");
 const { Prometheus } = require("../prometheus");
 const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger } = require("../../src/util");
@@ -17,6 +13,7 @@ const version = require("../../package.json").version;
 const apicache = require("../modules/apicache");
 const { UptimeKumaServer } = require("../uptime-kuma-server");
 const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
+const Maintenance = require("./maintenance");
 
 /**
  * status:
@@ -1105,6 +1102,7 @@ class Monitor extends BeanModel {
      * @returns {Promise<boolean>}
      */
     static async isUnderMaintenance(monitorID) {
+        let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
         const maintenance = await R.getRow(`
             SELECT COUNT(*) AS count
             FROM monitor_maintenance mm
@@ -1113,8 +1111,7 @@ class Monitor extends BeanModel {
             JOIN maintenance_timeslot
                 ON maintenance_timeslot.maintenance_id = maintenance.id
             WHERE mm.monitor_id = ?
-                AND maintenance_timeslot.start_date <= DATETIME('now')
-                AND maintenance_timeslot.end_date >= DATETIME('now')
+              AND ${activeCondition}
             LIMIT 1`, [ monitorID ]);
         return maintenance.count !== 0;
     }
diff --git a/server/model/status_page.js b/server/model/status_page.js
index 4351db58..0620a1ee 100644
--- a/server/model/status_page.js
+++ b/server/model/status_page.js
@@ -2,6 +2,7 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
 const { R } = require("redbean-node");
 const cheerio = require("cheerio");
 const { UptimeKumaServer } = require("../uptime-kuma-server");
+const Maintenance = require("./maintenance");
 
 class StatusPage extends BeanModel {
 
@@ -271,14 +272,14 @@ class StatusPage extends BeanModel {
         try {
             const publicMaintenanceList = [];
 
+            let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
             let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
                 SELECT m.*
                 FROM maintenance m, maintenance_status_page msp, maintenance_timeslot
                 WHERE  msp.maintenance_id = m.id
-                AND maintenance_timeslot.maintenance.id = m.id
-                AND maintenance_timeslot.start_date <= DATETIME('now')
-                AND maintenance_timeslot.end_date >= DATETIME('now')
-                AND msp.status_page_id = ?
+                    AND maintenance_timeslot.maintenance.id = m.id
+                    AND msp.status_page_id = ?
+                    AND ${activeCondition}
                 ORDER BY m.end_date
             `, [ statusPageId ]));
 
diff --git a/server/server.js b/server/server.js
index 4aec2b27..1ad99899 100644
--- a/server/server.js
+++ b/server/server.js
@@ -33,6 +33,11 @@ log.info("server", "Importing Node libraries");
 const fs = require("fs");
 
 log.info("server", "Importing 3rd-party libraries");
+
+const dayjs = require("dayjs");
+dayjs.extend(require("dayjs/plugin/utc"));
+dayjs.extend(require("dayjs/plugin/timezone"));
+
 log.debug("server", "Importing express");
 const express = require("express");
 const expressStaticGzip = require("express-static-gzip");
diff --git a/server/socket-handlers/maintenance-socket-handler.js b/server/socket-handlers/maintenance-socket-handler.js
index 5358b53e..9ae36b5c 100644
--- a/server/socket-handlers/maintenance-socket-handler.js
+++ b/server/socket-handlers/maintenance-socket-handler.js
@@ -6,11 +6,7 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
 const Maintenance = require("../model/maintenance");
 const server = UptimeKumaServer.getInstance();
 const dayjs = require("dayjs");
-const utc = require("dayjs/plugin/utc");
-let timezone = require("dayjs/plugin/timezone");
 const MaintenanceTimeslot = require("../model/maintenance_timeslot");
-dayjs.extend(utc);
-dayjs.extend(timezone);
 
 /**
  * Handlers for Maintenance
diff --git a/server/util-server.js b/server/util-server.js
index cc5e478d..1c5f5914 100644
--- a/server/util-server.js
+++ b/server/util-server.js
@@ -22,10 +22,6 @@ const {
     },
 } = require("node-radius-utils");
 const dayjs = require("dayjs");
-const utc = require("dayjs/plugin/utc");
-let timezone = require("dayjs/plugin/timezone");
-dayjs.extend(utc);
-dayjs.extend(timezone);
 
 // From ping-lite
 exports.WIN = /^win/.test(process.platform);
diff --git a/src/components/Datetime.vue b/src/components/Datetime.vue
index b24ab0b3..84bae503 100644
--- a/src/components/Datetime.vue
+++ b/src/components/Datetime.vue
@@ -4,12 +4,6 @@
 
 <script>
 import dayjs from "dayjs";
-import relativeTime from "dayjs/plugin/relativeTime";
-import timezone from "dayjs/plugin/timezone"; // dependent on utc plugin
-import utc from "dayjs/plugin/utc";
-dayjs.extend(utc);
-dayjs.extend(timezone);
-dayjs.extend(relativeTime);
 
 export default {
     props: {
diff --git a/src/components/PingChart.vue b/src/components/PingChart.vue
index 4cece191..2733e68c 100644
--- a/src/components/PingChart.vue
+++ b/src/components/PingChart.vue
@@ -16,18 +16,14 @@
     </div>
 </template>
 
-<script lang="ts">
+<script lang="js">
 import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
 import "chartjs-adapter-dayjs";
 import dayjs from "dayjs";
-import timezone from "dayjs/plugin/timezone";
-import utc from "dayjs/plugin/utc";
 import { LineChart } from "vue-chart-3";
 import { useToast } from "vue-toastification";
 import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts";
 
-dayjs.extend(utc);
-dayjs.extend(timezone);
 const toast = useToast();
 
 Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
diff --git a/src/components/settings/General.vue b/src/components/settings/General.vue
index 19dc8077..242ad853 100644
--- a/src/components/settings/General.vue
+++ b/src/components/settings/General.vue
@@ -145,11 +145,7 @@
 <script>
 import HiddenInput from "../../components/HiddenInput.vue";
 import dayjs from "dayjs";
-import utc from "dayjs/plugin/utc";
-import timezone from "dayjs/plugin/timezone";
 import { timezoneList } from "../../util-frontend";
-dayjs.extend(utc);
-dayjs.extend(timezone);
 
 export default {
     components: {
diff --git a/src/main.js b/src/main.js
index 7783882b..5567023f 100644
--- a/src/main.js
+++ b/src/main.js
@@ -16,6 +16,13 @@ import theme from "./mixins/theme";
 import lang from "./mixins/lang";
 import { router } from "./router";
 import { appName } from "./util.ts";
+import dayjs from "dayjs";
+import timezone from "dayjs/plugin/timezone";
+import utc from "dayjs/plugin/utc";
+import relativeTime from "dayjs/plugin/relativeTime";
+dayjs.extend(utc);
+dayjs.extend(timezone);
+dayjs.extend(relativeTime);
 
 const app = createApp({
     mixins: [
diff --git a/src/mixins/datetime.js b/src/mixins/datetime.js
index 0e5317c6..3bbe1130 100644
--- a/src/mixins/datetime.js
+++ b/src/mixins/datetime.js
@@ -1,10 +1,4 @@
 import dayjs from "dayjs";
-import relativeTime from "dayjs/plugin/relativeTime";
-import timezone from "dayjs/plugin/timezone";
-import utc from "dayjs/plugin/utc";
-dayjs.extend(utc);
-dayjs.extend(timezone);
-dayjs.extend(relativeTime);
 
 /**
  * DateTime Mixin
diff --git a/src/util-frontend.js b/src/util-frontend.js
index 36dac49f..3323f327 100644
--- a/src/util-frontend.js
+++ b/src/util-frontend.js
@@ -1,12 +1,7 @@
 import dayjs from "dayjs";
-import timezone from "dayjs/plugin/timezone";
-import utc from "dayjs/plugin/utc";
 import timezones from "timezones-list";
 import { localeDirection, currentLocale } from "./i18n";
 
-dayjs.extend(utc);
-dayjs.extend(timezone);
-
 /**
  * Returns the offset from UTC in hours for the current locale.
  * @returns {number} The offset from UTC in hours.

From 204339fbedde88443a2968e3ff4c76f174475147 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Wed, 28 Sep 2022 00:48:15 +0800
Subject: [PATCH 061/134] Make two functions to convert ISO 8601  <=>
 YYYY-MM-DD hh:mm:ss

---
 server/model/maintenance.js          | 30 +++++-----------------------
 server/model/maintenance_timeslot.js |  4 ++--
 src/pages/EditMaintenance.vue        |  5 ++---
 src/util.js                          | 16 ++++++++++++---
 src/util.ts                          | 17 ++++++++++++++--
 5 files changed, 37 insertions(+), 35 deletions(-)

diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index 945e4d97..3d0595a7 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -1,9 +1,7 @@
-const dayjs = require("dayjs");
 const { BeanModel } = require("redbean-node/dist/bean-model");
-const { parseTimeObject, parseTimeFromTimeObject } = require("../../src/util");
+const { parseTimeObject, parseTimeFromTimeObject, isoToUTCDateTime, utcToISODateTime } = require("../../src/util");
 const { isArray } = require("chart.js/helpers");
 const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
-const { R } = require("redbean-node");
 
 class Maintenance extends BeanModel {
 
@@ -15,20 +13,11 @@ class Maintenance extends BeanModel {
      */
     async toPublicJSON(timezone = null) {
 
-        let dateTimeRange = [];
-        if (this.start_datetime) {
-
-            dateTimeRange.push(dayjs.utc(this.start_datetime).toISOString());
-            if (this.end_datetime) {
-                dateTimeRange.push(dayjs.utc(this.end_datetime).toISOString());
-            }
-        }
-
         let dateRange = [];
         if (this.start_date) {
-            dateRange.push(dayjs.utc(this.start_date).toISOString());
+            dateRange.push(utcToISODateTime(this.start_date));
             if (this.end_date) {
-                dateRange.push(dayjs.utc(this.end_date).toISOString());
+                dateRange.push(utcToISODateTime(this.end_date));
             }
         }
 
@@ -55,7 +44,6 @@ class Maintenance extends BeanModel {
             strategy: this.strategy,
             intervalDay: this.interval_day,
             active: !!this.active,
-            dateTimeRange: dateTimeRange,
             dateRange: dateRange,
             timeRange: timeRange,
             weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
@@ -104,18 +92,10 @@ class Maintenance extends BeanModel {
         bean.active = obj.active;
 
         if (obj.dateRange[0]) {
-            bean.start_date = R.isoDate(dayjs(obj.dateRange[0]).utc());
+            bean.start_date = isoToUTCDateTime(obj.dateRange[0]);
 
             if (obj.dateRange[1]) {
-                bean.end_date = R.isoDate(dayjs(obj.dateRange[1]).utc());
-            }
-        }
-
-        if (obj.dateTimeRange[0]) {
-            bean.start_datetime = R.isoDateTime(dayjs(obj.dateTimeRange[0]).utc());
-
-            if (obj.dateTimeRange[1]) {
-                bean.end_datetime = R.isoDateTime(dayjs(obj.dateTimeRange[1]).utc());
+                bean.end_date = isoToUTCDateTime(obj.dateRange[1]);
             }
         }
 
diff --git a/server/model/maintenance_timeslot.js b/server/model/maintenance_timeslot.js
index 7917dd69..0ac5158d 100644
--- a/server/model/maintenance_timeslot.js
+++ b/server/model/maintenance_timeslot.js
@@ -36,8 +36,8 @@ class MaintenanceTimeslot extends BeanModel {
         } else if (maintenance.strategy === "single") {
             let bean = R.dispense("maintenance_timeslot");
             bean.maintenance_id = maintenance.id;
-            bean.start_date = maintenance.start_datetime;
-            bean.end_date = maintenance.end_datetime;
+            bean.start_date = maintenance.start_date;
+            bean.end_date = maintenance.end_date;
             bean.generated_next = true;
             await R.store(bean);
         } else {
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 640962d7..be9f7ce4 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -103,7 +103,7 @@
                                 <div class="my-3">
                                     <label class="form-label">{{ $t("DateTime Range") }}</label>
                                     <Datepicker
-                                        v-model="maintenance.dateTimeRange"
+                                        v-model="maintenance.dateRange"
                                         :dark="$root.isDark"
                                         range textInput
                                         :monthChangeOnScroll="false"
@@ -369,8 +369,7 @@ export default {
                     strategy: "single",
                     active: 1,
                     intervalDay: 1,
-                    dateTimeRange: [ this.minDate ],
-                    dateRange: [],
+                    dateRange: [ this.minDate ],
                     timeRange: [{
                         hours: 2,
                         minutes: 0,
diff --git a/src/util.js b/src/util.js
index 15427cdd..1fee0eaa 100644
--- a/src/util.js
+++ b/src/util.js
@@ -7,9 +7,8 @@
 // Backend uses the compiled file util.js
 // Frontend uses util.ts
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = 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 = _dayjs;
+exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = 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");
 exports.isDev = process.env.NODE_ENV === "development";
 exports.appName = "Uptime Kuma";
 exports.DOWN = 0;
@@ -351,3 +350,14 @@ function parseTimeFromTimeObject(obj) {
     return result;
 }
 exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
+function isoToUTCDateTime(input) {
+    return dayjs(input).utc().format("YYYY-MM-DD HH:mm:ss");
+}
+exports.isoToUTCDateTime = isoToUTCDateTime;
+/**
+ * @param input
+ */
+function utcToISODateTime(input) {
+    return dayjs.utc(input).toISOString();
+}
+exports.utcToISODateTime = utcToISODateTime;
diff --git a/src/util.ts b/src/util.ts
index cb51250b..16511afa 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -6,8 +6,10 @@
 // Backend uses the compiled file util.js
 // Frontend uses util.ts
 
-import * as _dayjs from "dayjs";
-const dayjs = _dayjs;
+import * as dayjs  from "dayjs";
+import * as timezone from "dayjs/plugin/timezone";
+import * as utc from "dayjs/plugin/utc";
+import {R} from "redbean-node";
 
 export const isDev = process.env.NODE_ENV === "development";
 export const appName = "Uptime Kuma";
@@ -392,3 +394,14 @@ export function parseTimeFromTimeObject(obj : any) {
     return result;
 }
 
+
+export function isoToUTCDateTime(input : string) {
+    return dayjs(input).utc().format("YYYY-MM-DD HH:mm:ss");
+}
+
+/**
+ * @param input
+ */
+export function utcToISODateTime(input : string) {
+    return dayjs.utc(input).toISOString();
+}

From 929d23810616520a20e5ad875a3e2e9a511af8bf Mon Sep 17 00:00:00 2001
From: FarisDaffa <65797160+Faris0520@users.noreply.github.com>
Date: Sat, 1 Oct 2022 12:17:17 +0700
Subject: [PATCH 062/134] Update id-ID.js

---
 src/languages/id-ID.js | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/src/languages/id-ID.js b/src/languages/id-ID.js
index 36ee0341..3d3c3389 100644
--- a/src/languages/id-ID.js
+++ b/src/languages/id-ID.js
@@ -130,7 +130,7 @@ export default {
     "Repeat Password": "Ulangi Sandi",
     "Import Backup": "Impor Cadangan",
     "Export Backup": "Expor Cadangan",
-    Export: "Expor",
+    Export: "Ekspor",
     Import: "Impor",
     respTime: "Tanggapan. Waktu (milidetik)",
     notAvailableShort: "N/A",
@@ -192,7 +192,7 @@ export default {
     "Status Pages": "Halaman Status",
     defaultNotificationName: "{notification} saya Peringatan ({number})",
     here: "di sini",
-    Required: "Dibutuhkan",
+    Required: "Wajib",
     telegram: "Telegram",
     "Bot Token": "Bot Token",
     wayToGetTelegramToken: "Anda dapat mendapatkan token dari {0}.",
@@ -210,9 +210,9 @@ export default {
     secureOptionNone: "None / STARTTLS (25, 587)",
     secureOptionTLS: "TLS (465)",
     "Ignore TLS Error": "Abaikan Kesalahan TLS",
-    "From Email": "Dari Surel",
+    "From Email": "Dari Email",
     emailCustomSubject: "Subjek",
-    "To Email": "Ke Surel",
+    "To Email": "Ke Email",
     smtpCC: "CC",
     smtpBCC: "BCC",
     discord: "Discord",
@@ -232,7 +232,7 @@ export default {
     signalImportant: "PENTING: Anda tidak dapat mencampur grup dan nomor di penerima!",
     gotify: "Gotify",
     "Application Token": "Token Aplikasi",
-    "Server URL": "URL Peladen",
+    "Server URL": "URL Server",
     Priority: "Prioritas",
     slack: "Slack",
     "Icon Emoji": "Ikon Emoji",
@@ -274,9 +274,9 @@ export default {
     "LunaSea Device ID": "LunaSea Device ID",
     "Apprise URL": "Apprise URL",
     "Example:": "Contoh: {0}",
-    "Read more:": "Baca lebih lajut: {0}",
+    "Read more:": "Baca lebih lanjut: {0}",
     "Status:": "Status: {0}",
-    "Read more": "Baca lebih lajut",
+    "Read more": "Baca lebih lanjut",
     appriseInstalled: "Apprise diinstall.",
     appriseNotInstalled: "Apprise tidak diinstall. {0}",
     "Access Token": "Token Akses",
@@ -310,7 +310,7 @@ export default {
     BodyInvalidFormat: "Request Body memiliki format JSON yang tidak sesuai: ",
     "Monitor History": "Riyawat Monitor",
     clearDataOlderThan: "Simpan data riwayat monitoring selama {0} hari.",
-    PasswordsDoNotMatch: "Passwords tidak sama.",
+    PasswordsDoNotMatch: "Password tidak sama.",
     records: "catatan",
     "One record": "Satu catatan",
     steamApiKeyDescription: "Untuk monitoring Steam Game Server Anda membutuhkan kunci Steam Web-API. Anda dapat mendaftarkan Kunci API Anda melalui: ",
@@ -322,7 +322,7 @@ export default {
     recent: "Baru saja",
     Done: "Selesai",
     Info: "Info",
-    Security: "Keamaan",
+    Security: "Keamanan",
     "Steam API Key": "Steam API Key",
     "Shrink Database": "Shrink Database",
     "Pick a RR-Type...": "Pilih RR-Type...",
@@ -393,7 +393,7 @@ export default {
     alertaAlertState: "Status Siaga",
     alertaRecoverState: "Status Pemulihan",
     deleteStatusPageMsg: "Apakah Anda yakin untuk menghapus halaman status berikut?",
-    Proxies: "Proxies",
+    Proxies: "Proxy",
     default: "Bawaan",
     enabled: "Diaktifkan",
     setAsDefault: "Tetapkan sebagai bawaan",
@@ -403,7 +403,7 @@ export default {
     setAsDefaultProxyDescription: "Proxy berikut akan diaktifkan sebagai bawaan untuk monitor baru. Anda masih dapat menonaktifkan proxy secara terpisah untuk setiap monitor.",
     "Certificate Chain": "Certificate Chain",
     Valid: "Sahih",
-    Invalid: "Tidak Sahih",
+    Invalid: "Tidak Valid",
     AccessKeyId: "AccessKey ID",
     SecretAccessKey: "AccessKey Secret",
     PhoneNumbers: "Nomor Telepon",
@@ -433,7 +433,7 @@ export default {
     Installed: "Terpasang",
     "Not installed": "Tidak terpasang",
     Running: "Berlari",
-    "Not running": "Tidak berlari",
+    "Not running": "Tidak berjalan",
     "Remove Token": "Hapus Token",
     Start: "Mulai",
     Stop: "Berhenti",
@@ -444,7 +444,7 @@ export default {
     startOrEndWithOnly: "Mulai atau akhiri hanya dengan {0}",
     "No consecutive dashes": "Tanda hubung tidak berurutan",
     Next: "Selanjutnya",
-    "The slug is already taken. Please choose another slug.": "Slug is telah digunakan. Silakan pilih slug lain.",
+    "The slug is already taken. Please choose another slug.": "Slug telah digunakan. Silakan pilih slug lain.",
     "No Proxy": "TIdak ada Proxy",
     Authentication: "Autentikasi",
     "HTTP Basic Auth": "HTTP Basic Auth",

From 2e54dee817f02953393be0167ce71128dbfe8fac Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Sat, 1 Oct 2022 15:03:28 +0100
Subject: [PATCH 063/134] Fixed Octopush Notifier not working #2144

The version number was passed as a string from the frontend but was
checked against a number in the backend provider. This caused the if else
if to fall through into an error. The literal it is now being compared
has been changed to a string and the unknown version error is no longer
encountered.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 server/notification-providers/octopush.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/server/notification-providers/octopush.js b/server/notification-providers/octopush.js
index 0eda940b..524a4a88 100644
--- a/server/notification-providers/octopush.js
+++ b/server/notification-providers/octopush.js
@@ -10,7 +10,7 @@ class Octopush extends NotificationProvider {
 
         try {
         // Default - V2
-            if (notification.octopushVersion === 2 || !notification.octopushVersion) {
+            if (notification.octopushVersion === "2" || !notification.octopushVersion) {
                 let config = {
                     headers: {
                         "api-key": notification.octopushAPIKey,
@@ -31,7 +31,7 @@ class Octopush extends NotificationProvider {
                     "sender": notification.octopushSenderName
                 };
                 await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config);
-            } else if (notification.octopushVersion === 1) {
+            } else if (notification.octopushVersion === "1") {
                 let data = {
                     "user_login": notification.octopushDMLogin,
                     "api_key": notification.octopushDMAPIKey,

From 63e408f4f2c3886874fb8a6cbd9e10a5ee6bd33a Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Sat, 1 Oct 2022 15:42:34 +0100
Subject: [PATCH 064/134] Fixed octopush legacy doesn't return error code

The octopush legacy API does not return a HTTP error code and instead
always returns a HTTP 200. This means that no error it thrown even if
something like the parameters are incorrect.
Instead the error code is given in the json response data.
Therefore we must look at the response data and check for the presence
of the "error_code" key in the response data.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 server/notification-providers/octopush.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/server/notification-providers/octopush.js b/server/notification-providers/octopush.js
index 524a4a88..35d88f5f 100644
--- a/server/notification-providers/octopush.js
+++ b/server/notification-providers/octopush.js
@@ -49,7 +49,13 @@ class Octopush extends NotificationProvider {
                     },
                     params: data
                 };
-                await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config);
+
+                // V1 API returns 200 even on error so we must check
+                // response data
+                let response = await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config);
+                if ("error_code" in response.data) {
+                    this.throwGeneralAxiosError(`Octopush error ${JSON.stringify(response.data)}`);
+                }
             } else {
                 throw new Error("Unknown Octopush version!");
             }

From 97de3959cd3c71d17bcfa32a2a3ec131c3255e9b Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Sat, 1 Oct 2022 19:48:00 +0100
Subject: [PATCH 065/134] Updated octopush error handling to accept 000

The legacy octopush API includes an error code with all responses. A
code other than 000 is an error.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 server/notification-providers/octopush.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/server/notification-providers/octopush.js b/server/notification-providers/octopush.js
index 35d88f5f..d5c475d3 100644
--- a/server/notification-providers/octopush.js
+++ b/server/notification-providers/octopush.js
@@ -54,7 +54,9 @@ class Octopush extends NotificationProvider {
                 // response data
                 let response = await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config);
                 if ("error_code" in response.data) {
-                    this.throwGeneralAxiosError(`Octopush error ${JSON.stringify(response.data)}`);
+                    if (response.data.error_code !== "000") {
+                        this.throwGeneralAxiosError(`Octopush error ${JSON.stringify(response.data)}`);
+                    }
                 }
             } else {
                 throw new Error("Unknown Octopush version!");

From 6a3eccf6a6631e249658c749f6a1bd3466347f92 Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Sun, 2 Oct 2022 02:26:38 +0100
Subject: [PATCH 066/134] Fixed alert features unnecessary URL field #2009

The filling of the URL field was incorrect previously. It has been
updated to handle new monitor types.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 server/notification-providers/teams.js | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/server/notification-providers/teams.js b/server/notification-providers/teams.js
index c398e03c..bcfc82c2 100644
--- a/server/notification-providers/teams.js
+++ b/server/notification-providers/teams.js
@@ -63,7 +63,7 @@ class Teams extends NotificationProvider {
             });
         }
 
-        if (monitorUrl) {
+        if (monitorUrl && monitorUrl !== "https://") {
             facts.push({
                 name: "URL",
                 value: monitorUrl,
@@ -127,13 +127,17 @@ class Teams extends NotificationProvider {
 
             let url;
 
-            if (monitorJSON["type"] === "port") {
-                url = monitorJSON["hostname"];
-                if (monitorJSON["port"]) {
-                    url += ":" + monitorJSON["port"];
-                }
-            } else {
-                url = monitorJSON["url"];
+            switch (monitorJSON["type"]) {
+                case "http":
+                case "keywork":
+                    url = monitorJSON["url"];
+                    break;
+                case "docker":
+                    url = monitorJSON["docker_host"];
+                    break;
+                default:
+                    url = monitorJSON["hostname"];
+                    break;
             }
 
             const payload = this._notificationPayloadFactory({

From ed7bc0e6d194f603eb6ca658b47560c10eeb1c2a Mon Sep 17 00:00:00 2001
From: MrEddX <66828538+MrEddX@users.noreply.github.com>
Date: Sun, 2 Oct 2022 09:55:58 +0300
Subject: [PATCH 067/134] Update bg-BG.js

Added New Fields
---
 src/languages/bg-BG.js | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/src/languages/bg-BG.js b/src/languages/bg-BG.js
index a234e56f..2002bc46 100644
--- a/src/languages/bg-BG.js
+++ b/src/languages/bg-BG.js
@@ -562,4 +562,24 @@ export default {
     "Docker Host": "Docker хост",
     "Docker Hosts": "Docker хостове",
     trustProxyDescription: "Trust 'X-Forwarded-*' headers.  Ако искате да получавате правилния IP адрес на клиента, а Uptime Kuma е зад системи като Nginx или Apache, трябва да разрешите тази опция.",
+    Examples: "Примери",
+    "Home Assistant URL": "Home Assistant URL адрес",
+    "Long-Lived Access Token": "Long-Lived Access Token",
+    "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Long-Lived Access Token можете да създадете, като кликнете върху името на профила си (долу ляво) и превъртите до най-долу, след това кликнете върху Създаване на токен. ",
+    "Notification Service": "Услуга за известяване",
+    "default: notify all devices": "по подразбиране: извести всички устройства",
+    "A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Списък с услугите за известяване може да бъде намерен в Home Assistant под \"Developer Tools > Services\", там потърсете \"notification\", за да намерите името на вашето устройство/телефон.",
+    "Automations can optionally be triggered in Home Assistant:": "Автоматизациите могат да се задействат при нужда в Home Assistant:",
+    "Trigger type:": "Задействане тип:",
+    "Event type:": "Събитие тип:",
+    "Event data:": "Събитие данни:",
+    "Then choose an action, for example switch the scene to where an RGB light is red.": "След което изберете действие, например да превключите сцената, където RGB светлината е червена.",
+    "Frontend Version": "Фронтенд версия",
+    "Frontend Version do not match backend version!": "Фронтенд версията не съвпада с Бекенд версията!",
+    "Base URL": "Базов  URL адрес",
+    goAlertInfo: "GoAlert е приложение с отворен код за планиране на повиквания, автоматизирани ескалации и известия (като SMS или гласови повиквания). Автоматично ангажирайте точния човек, по точния начин и в точното време! {0}",
+    goAlertIntegrationKeyInfo: "Вземете общ API интеграционен ключ за услугата във формат \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" обикновено стойността на параметъра token на копирания URL адрес.",
+    goAlert: "GoAlert",
+    backupOutdatedWarning: "Отпаднало: Тъй като са добавени много функции, тази опция за архивиране не е достатъчно поддържана и не може да генерира или възстанови пълен архив.",
+    backupRecommend: "Моля, архивирайте дяла или папката (./data/) директно вместо това.",
 };

From da34685019b6902b3464423a72f3b867a614a1d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?An=20=7C=20Anton=20R=C3=B6hm?=
 <18481195+AnTheMaker@users.noreply.github.com>
Date: Sun, 2 Oct 2022 13:38:33 +0200
Subject: [PATCH 068/134] fix typos

---
 CONTRIBUTING.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e9829c96..12fd6ed4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -27,7 +27,7 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
 
 ## Can I create a pull request for Uptime Kuma?
 
-Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create empty draft pull request or open an issue, so we can discuss first**. Especially for a large pull request or you don't know it will be merged or not.
+Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can discuss first**. Especially for a large pull request or you don't know it will be merged or not.
 
 Here are some references:
 
@@ -48,7 +48,7 @@ Here are some references:
 - UI/UX is not close to Uptime Kuma 
 - Existing logic is completely modified or deleted for no reason
 - A function that is completely out of scope
-- Unnesscary large code changes (Hard to review, casuse code conflicts to other pull requests)
+- Unnecessary large code changes (Hard to review, causes code conflicts to other pull requests)
 
 I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
 
@@ -183,7 +183,7 @@ By default, the Chromium window will be shown up during the test. Specifying `HE
 
 ## Dependencies
 
-Both frontend and backend share the same package.json. However, the frontend dependencies are eventually not be used in production environment, because it is usually also baked into dist files. So:
+Both frontend and backend share the same package.json. However, the frontend dependencies are eventually not used in the production environment, because it is usually also baked into dist files. So:
 
 - Frontend dependencies = "devDependencies"
   - Examples: vue, chart.js

From 8595824b5dd7ce4eb94d9e554be77e5f9fc982ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?An=20=7C=20Anton=20R=C3=B6hm?=
 <18481195+AnTheMaker@users.noreply.github.com>
Date: Sun, 2 Oct 2022 13:49:40 +0200
Subject: [PATCH 069/134] Improve German translation

---
 src/languages/de-DE.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/languages/de-DE.js b/src/languages/de-DE.js
index bd5bf87d..a6ae4077 100644
--- a/src/languages/de-DE.js
+++ b/src/languages/de-DE.js
@@ -68,7 +68,7 @@ export default {
     Timezone: "Zeitzone",
     "Search Engine Visibility": "Sichtbarkeit für Suchmaschinen",
     "Allow indexing": "Indizierung zulassen",
-    "Discourage search engines from indexing site": "Halte Suchmaschinen von der Indexierung der Seite ab",
+    "Discourage search engines from indexing site": "Suchmaschinen darum bitten, die Seite nicht zu indizieren",
     "Change Password": "Passwort ändern",
     "Current Password": "Aktuelles Passwort",
     "New Password": "Neues Passwort",
@@ -78,7 +78,7 @@ export default {
     "Disable Auth": "Authentifizierung deaktivieren",
     "Enable Auth": "Authentifizierung aktivieren",
     "disableauth.message1": "Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?",
-    "disableauth.message2": "Es ist für <strong>jemanden der eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access.",
+    "disableauth.message2": "Dies ist für Szenarien gedacht, <strong>in denen man eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access, Authelia oder andere Authentifizierungsmechanismen.",
     "Please use this option carefully!": "Bitte mit Vorsicht nutzen.",
     Logout: "Ausloggen",
     notificationDescription: "Benachrichtigungen müssen einem Monitor zugewiesen werden, damit diese funktionieren.",
@@ -559,7 +559,7 @@ export default {
     "ntfy Topic": "ntfy Thema",
     Domain: "Domain",
     Workstation: "Workstation",
-    disableCloudflaredNoAuthMsg: "Du bist im nicht-authentifizieren modus, ein Passwort wird nicht benötigt.",
+    disableCloudflaredNoAuthMsg: "Du bist im nicht-authentifizieren Modus, ein Passwort wird nicht benötigt.",
     trustProxyDescription: "Vertraue 'X-Forwarded-*' headern. Wenn man die richtige client IP haben möchte und Uptime Kuma hinter einem Proxy wie Nginx or Apache läuft, wollte dies aktiviert werden.",
     wayToGetLineNotifyToken: "Du kannst hier ein Token erhalten: {0}",
     Examples: "Beispiele",

From f6ac09b7513afccfed2c1e8a01921e3562351869 Mon Sep 17 00:00:00 2001
From: SametKUM <kumsamet@gmail.com>
Date: Sun, 2 Oct 2022 21:14:00 +0300
Subject: [PATCH 070/134] fix some translations

---
 src/languages/tr-TR.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/languages/tr-TR.js b/src/languages/tr-TR.js
index 75bc103c..1bf94993 100644
--- a/src/languages/tr-TR.js
+++ b/src/languages/tr-TR.js
@@ -224,7 +224,7 @@ export default {
     teams: "Microsoft Teams",
     "Webhook URL": "Webhook URL",
     wayToGetTeamsURL: "Bir webhook URL'sinin nasıl oluşturulacağını öğrenebilirsiniz {0}.",
-    signal: "Signal",
+    signal: "Sinyal",
     Number: "Numara",
     Recipients: "Alıcılar",
     needSignalAPI: "REST API ile bir signal istemciniz olması gerekiyor.",
@@ -552,14 +552,14 @@ export default {
     deleteDockerHostMsg: "Bu docker ana bilgisayarını tüm monitörler için silmek istediğinizden emin misiniz?",
     socket: "Soket",
     tcp: "TCP / HTTP",
-    "Docker Container": "Docker Konteyneri",
+    "Docker Container": "Docker Konteyner",
     "Container Name / ID": "Konteyner Adı / Kimliği",
     "Docker Host": "Docker Ana Bilgisayarı",
     "Docker Hosts": "Docker Ana Bilgisayarları",
     "ntfy Topic": "ntfy Konu",
     Domain: "Domain",
     Workstation: "İş İstasyonu",
-    disableCloudflaredNoAuthMsg: "Yetki Yok modundasınız, şifre gerekli değil.",
+    disableCloudflaredNoAuthMsg: "Yetki yok modundasınız, şifre gerekli değil.",
     trustProxyDescription: "'X-Forwarded-*' başlıklarına güvenin. Doğru istemci IP'sini almak istiyorsanız ve Uptime Kuma'nız Nginx veya Apache'nin arkasındaysa, bunu etkinleştirmelisiniz.",
     wayToGetLineNotifyToken: "{0} adresinden bir erişim jetonu alabilirsiniz.",
     Examples: "Örnekler",

From d39a4770e05edf42c545af412bf23cde9c6f8d1a Mon Sep 17 00:00:00 2001
From: 5idereal <nelson22768384@gmail.com>
Date: Mon, 3 Oct 2022 13:11:39 +0800
Subject: [PATCH 071/134] sync

---
 src/languages/zh-TW.js | 56 +++++++++++++++++++++++++++++++++++++-----
 1 file changed, 50 insertions(+), 6 deletions(-)

diff --git a/src/languages/zh-TW.js b/src/languages/zh-TW.js
index 3405c02a..afb1fabc 100644
--- a/src/languages/zh-TW.js
+++ b/src/languages/zh-TW.js
@@ -2,6 +2,8 @@ export default {
     languageName: "繁體中文 (台灣)",
     checkEverySecond: "每 {0} 秒檢查一次",
     retryCheckEverySecond: "每 {0} 秒重試一次",
+    resendEveryXTimes: "Resend every {0} times",
+    resendDisabled: "Resend disabled",
     retriesDescription: "在服務被標記為離線並傳送通知前的最大重試次數",
     ignoreTLSError: "忽略 HTTPS 網站的 TLS/SSL 錯誤",
     upsideDownModeDescription: "反轉顯示狀態。若服務可以連線,將顯示離線。",
@@ -72,6 +74,7 @@ export default {
     "Heartbeat Interval": "心跳間隔",
     Retries: "重試次數",
     "Heartbeat Retry Interval": "心跳重試間隔",
+    "Resend Notification if Down X times consequently": "若 X 次心跳皆離線,重新傳送通知",
     Advanced: "進階",
     "Upside Down Mode": "顛倒模式",
     "Max. Redirects": "最大重新導向次數",
@@ -455,6 +458,8 @@ export default {
     "Message:": "訊息:",
     "Don't know how to get the token? Please read the guide:": "不知道如何取得權杖嗎?請閱讀指南:",
     "The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "如果您目前正透過 Cloudflare Tunnel 連線,可能會導致連線中斷。您確定要停止嗎?請輸入密碼以確認。",
+    "HTTP Headers": "HTTP 標頭",
+    "Trust Proxy": "信任的 Proxy",
     "Other Software": "其他軟體",
     "For example: nginx, Apache and Traefik.": "例如 nginx、Apache 和 Traefik。",
     "Please read": "請閱覽",
@@ -467,6 +472,7 @@ export default {
     "Domain Name Expiry Notification": "網域名稱到期通知",
     Proxy: "Proxy",
     "Date Created": "建立日期",
+    HomeAssistant: "Home Assistant",
     onebotHttpAddress: "OneBot HTTP 位址",
     onebotMessageType: "OneBot 訊息類型",
     onebotGroupMessage: "群組",
@@ -479,6 +485,12 @@ export default {
     "Domain Names": "網域名稱",
     signedInDisp: "以 {0} 身分登入",
     signedInDispDisabled: "驗證已停用。",
+    RadiusSecret: "Radius Secret",
+    RadiusSecretDescription: "Shared Secret between client and server",
+    RadiusCalledStationId: "Called Station Id",
+    RadiusCalledStationIdDescription: "Identifier of the called device",
+    RadiusCallingStationId: "Calling Station Id",
+    RadiusCallingStationIdDescription: "Identifier of the calling device",
     "Certificate Expiry Notification": "憑證到期通知",
     "API Username": "API 使用者名稱",
     "API Key": "API 金鑰",
@@ -504,9 +516,9 @@ export default {
     "pushoversounds intermission": "中場休息",
     "pushoversounds magic": "魔法",
     "pushoversounds mechanical": "機械",
-    "pushoversounds pianobar": "Piano Bar",
-    "pushoversounds siren": "Siren",
-    "pushoversounds spacealarm": "Space Alarm",
+    "pushoversounds pianobar": "鋼琴酒吧",
+    "pushoversounds siren": "警鈴",
+    "pushoversounds spacealarm": "太空鬧鐘",
     "pushoversounds tugboat": "汽笛",
     "pushoversounds alien": "外星鬧鐘 (長)",
     "pushoversounds climb": "爬升 (長)",
@@ -531,11 +543,43 @@ export default {
     "Coming Soon": "即將推出",
     wayToGetClickSendSMSToken: "您可以從 {0} 取得 API 使用者名稱和金鑰。",
     "Connection String": "連線字串",
-    "Query": "查詢",
+    Query: "查詢",
     settingsCertificateExpiry: "TLS 憑證到期",
     certificationExpiryDescription: "TLS 將於 X 天後到期時觸發 HTTPS 監測器通知:",
+    "Setup Docker Host": "設定 Docker 主機",
+    "Connection Type": "連線類型",
+    "Docker Daemon": "Docker 精靈",
+    deleteDockerHostMsg: "您確定要為所有監測器刪除此 Docker 主機嗎?",
+    socket: "通訊端",
+    tcp: "TCP / HTTP",
+    "Docker Container": "Docker 容器",
+    "Container Name / ID": "容器名稱 / ID",
+    "Docker Host": "Docker 主機",
+    "Docker Hosts": "Docker 主機",
     "ntfy Topic": "ntfy 主題",
-    "Domain": "網域",
-    "Workstation": "工作站",
+    Domain: "網域",
+    Workstation: "工作站",
     disableCloudflaredNoAuthMsg: "您處於無驗證模式。無須輸入密碼。",
+    trustProxyDescription: "Trust 'X-Forwarded-*' headers. If you want to get the correct client IP and your Uptime Kuma is behind such as Nginx or Apache, you should enable this. 如果您的 Uptime Kuma 架設於 Nginx 或 Apache ",
+    wayToGetLineNotifyToken: "您可以從 {0} 取得存取權杖",
+    Examples: "範例",
+    "Home Assistant URL": "Home Assistant 網址",
+    "Long-Lived Access Token": "長期有效存取權杖",
+    "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "若要建立長期有效存取權杖,請點擊您的個人檔案名稱 (左下角),捲動至最下方,然後點擊建立權杖。",
+    "Notification Service": "通知服務",
+    "default: notify all devices": "預設:通知所有服務",
+    "A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.",
+    "Automations can optionally be triggered in Home Assistant:": " 	自動化程序可以 can optionally be triggered in Home Assistant:",
+    "Trigger type:": "觸發器類型:",
+    "Event type:": "事件類型:",
+    "Event data:": "事件資料:",
+    "Then choose an action, for example switch the scene to where an RGB light is red.": "Then choose an action, for example switch the scene to where an RGB light is red.",
+    "Frontend Version": "前端版本",
+    "Frontend Version do not match backend version!": "前端版本與後端版本不符!",
+    "Base URL": "Base URL",
+    goAlertInfo: "GoAlert 是用於待命排程、升級自動化,以及通知 (如簡訊或語音通話) 的開源應用程式。自動在正確的時間、用洽當的方法、聯絡合適的人! {0}",
+    goAlertIntegrationKeyInfo: "Get generic API integration key for the service in this format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" usually the value of token parameter of copied URL.",
+    goAlert: "GoAlert",
+    backupOutdatedWarning: "Deprecated: Since a lot of features added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup. 由於新功能的增加,且此備份功能,故",
+    backupRecommend: "Please backup the volume or the data folder (./data/) directly instead. 請直接備份或 ./data/ 資料夾",
 };

From 7672057319c28ec2957622abbcb2559e3cf5abc6 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Mon, 3 Oct 2022 15:51:29 +0800
Subject: [PATCH 072/134] [ntfy] Do not autofill

---
 src/components/notifications/Ntfy.vue | 14 +++++++-------
 src/languages/en.js                   |  1 +
 2 files changed, 8 insertions(+), 7 deletions(-)

diff --git a/src/components/notifications/Ntfy.vue b/src/components/notifications/Ntfy.vue
index a42dca30..ddcc3917 100644
--- a/src/components/notifications/Ntfy.vue
+++ b/src/components/notifications/Ntfy.vue
@@ -12,21 +12,21 @@
         </div>
     </div>
     <div class="mb-3">
-        <label for="ntfy-username" class="form-label">{{ $t("Username") }}</label>
+        <label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
+        <input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
+    </div>
+    <div class="mb-3">
+        <label for="ntfy-username" class="form-label">{{ $t("Username") }} ({{ $t("Optional") }})</label>
         <div class="input-group mb-3">
             <input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control" required>
         </div>
     </div>
     <div class="mb-3">
-        <label for="ntfy-password" class="form-label">{{ $t("Password") }}</label>
+        <label for="ntfy-password" class="form-label">{{ $t("Password") }} ({{ $t("Optional") }})</label>
         <div class="input-group mb-3">
-            <HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword"></HiddenInput>
+            <HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
         </div>
     </div>
-    <div class="mb-3">
-        <label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
-        <input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
-    </div>
 </template>
 
 <script>
diff --git a/src/languages/en.js b/src/languages/en.js
index 7d980f63..00e9551f 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -582,4 +582,5 @@ export default {
     goAlert: "GoAlert",
     backupOutdatedWarning: "Deprecated: Since a lot of features added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.",
     backupRecommend: "Please backup the volume or the data folder (./data/) directly instead.",
+    "Optional": "Optional",
 };

From c2c3f981bcd1e6c487d2260b51542a816b36e763 Mon Sep 17 00:00:00 2001
From: 5idereal <nelson22768384@gmail.com>
Date: Mon, 3 Oct 2022 18:03:15 +0800
Subject: [PATCH 073/134] update zh-tw translation

---
 src/languages/zh-TW.js | 34 +++++++++++++++++-----------------
 1 file changed, 17 insertions(+), 17 deletions(-)

diff --git a/src/languages/zh-TW.js b/src/languages/zh-TW.js
index afb1fabc..e677d27e 100644
--- a/src/languages/zh-TW.js
+++ b/src/languages/zh-TW.js
@@ -2,8 +2,8 @@ export default {
     languageName: "繁體中文 (台灣)",
     checkEverySecond: "每 {0} 秒檢查一次",
     retryCheckEverySecond: "每 {0} 秒重試一次",
-    resendEveryXTimes: "Resend every {0} times",
-    resendDisabled: "Resend disabled",
+    resendEveryXTimes: "每 {0} 次便重新傳送",
+    resendDisabled: "重新傳送已停用",
     retriesDescription: "在服務被標記為離線並傳送通知前的最大重試次數",
     ignoreTLSError: "忽略 HTTPS 網站的 TLS/SSL 錯誤",
     upsideDownModeDescription: "反轉顯示狀態。若服務可以連線,將顯示離線。",
@@ -486,11 +486,11 @@ export default {
     signedInDisp: "以 {0} 身分登入",
     signedInDispDisabled: "驗證已停用。",
     RadiusSecret: "Radius Secret",
-    RadiusSecretDescription: "Shared Secret between client and server",
-    RadiusCalledStationId: "Called Station Id",
-    RadiusCalledStationIdDescription: "Identifier of the called device",
-    RadiusCallingStationId: "Calling Station Id",
-    RadiusCallingStationIdDescription: "Identifier of the calling device",
+    RadiusSecretDescription: "客戶端與伺服器端的共享機密",
+    RadiusCalledStationId: "被叫站 Id",
+    RadiusCalledStationIdDescription: "被呼叫裝置的識別碼",
+    RadiusCallingStationId: "呼叫站 Id",
+    RadiusCallingStationIdDescription: "呼叫裝置的識別碼",
     "Certificate Expiry Notification": "憑證到期通知",
     "API Username": "API 使用者名稱",
     "API Key": "API 金鑰",
@@ -500,8 +500,8 @@ export default {
     "Octopush API Version": "Octopush API 版本",
     "Legacy Octopush-DM": "舊版 Octopush-DM",
     "endpoint": "端",
-    octopushAPIKey: "\"API key\" from HTTP API credentials in control panel",
-    octopushLogin: "\"Login\" from HTTP API credentials in control panel",
+    octopushAPIKey: "在控制台的 HTTP API 憑證取得的 \"API 金鑰\"",
+    octopushLogin: "在控制台的 HTTP API 憑證取得的 \"Login\"",
     promosmsLogin: "API 登入名稱",
     promosmsPassword: "API 密碼",
     "pushoversounds pushover": "Pushover (預設)",
@@ -560,7 +560,7 @@ export default {
     Domain: "網域",
     Workstation: "工作站",
     disableCloudflaredNoAuthMsg: "您處於無驗證模式。無須輸入密碼。",
-    trustProxyDescription: "Trust 'X-Forwarded-*' headers. If you want to get the correct client IP and your Uptime Kuma is behind such as Nginx or Apache, you should enable this. 如果您的 Uptime Kuma 架設於 Nginx 或 Apache ",
+    trustProxyDescription: "信任 'X-Forwarded-*' 標頭。如果您想要取得正確的客戶端 IP,且您的 Uptime Kuma 架設於 Nginx 或 Apache 後方,您應啟用此選項。",
     wayToGetLineNotifyToken: "您可以從 {0} 取得存取權杖",
     Examples: "範例",
     "Home Assistant URL": "Home Assistant 網址",
@@ -568,18 +568,18 @@ export default {
     "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "若要建立長期有效存取權杖,請點擊您的個人檔案名稱 (左下角),捲動至最下方,然後點擊建立權杖。",
     "Notification Service": "通知服務",
     "default: notify all devices": "預設:通知所有服務",
-    "A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.",
-    "Automations can optionally be triggered in Home Assistant:": " 	自動化程序可以 can optionally be triggered in Home Assistant:",
+    "A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "您可以在 Home Assistant 中查看通知服務的列表,在\"開發者工具 > 服務\"下搜尋\"通知\"來找到您的裝置/手機的名稱。",
+    "Automations can optionally be triggered in Home Assistant:": "可以選擇在 Home Assistant 中觸發自動化程序:",
     "Trigger type:": "觸發器類型:",
     "Event type:": "事件類型:",
     "Event data:": "事件資料:",
-    "Then choose an action, for example switch the scene to where an RGB light is red.": "Then choose an action, for example switch the scene to where an RGB light is red.",
+    "Then choose an action, for example switch the scene to where an RGB light is red.": "然後選擇動作,例如切換至 RGB 燈為紅色的場景。",
     "Frontend Version": "前端版本",
     "Frontend Version do not match backend version!": "前端版本與後端版本不符!",
-    "Base URL": "Base URL",
+    "Base URL": "基底網址",
     goAlertInfo: "GoAlert 是用於待命排程、升級自動化,以及通知 (如簡訊或語音通話) 的開源應用程式。自動在正確的時間、用洽當的方法、聯絡合適的人! {0}",
-    goAlertIntegrationKeyInfo: "Get generic API integration key for the service in this format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" usually the value of token parameter of copied URL.",
+    goAlertIntegrationKeyInfo: "取得服務的通用 API 整合金鑰,格式為 \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\"。通常是已複製的網址的權杖參數值。",
     goAlert: "GoAlert",
-    backupOutdatedWarning: "Deprecated: Since a lot of features added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup. 由於新功能的增加,且此備份功能,故",
-    backupRecommend: "Please backup the volume or the data folder (./data/) directly instead. 請直接備份或 ./data/ 資料夾",
+    backupOutdatedWarning: "過時:由於新功能的增加,且未妥善維護,故此備份功能無法產生或復原完整備份。",
+    backupRecommend: "請直接備份磁碟區或 ./data/ 資料夾。",
 };

From b0d6b5b13d59de628eeb8820cfb3023d8501566a Mon Sep 17 00:00:00 2001
From: George Tsomlektsis <ultrawelfaredev@gmail.com>
Date: Mon, 3 Oct 2022 17:48:34 +0300
Subject: [PATCH 074/134] Fixed entry route not redirecting correctly when the
 status entry page changes slug.

---
 server/server.js | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/server/server.js b/server/server.js
index 0c9a45e6..0f215245 100644
--- a/server/server.js
+++ b/server/server.js
@@ -155,7 +155,9 @@ let needSetup = false;
     Database.init(args);
     await initDatabase(testMode);
 
-    exports.entryPage = await setting("entryPage");
+    const entryPage = (await getSettings("general"))["entryPage"];
+    exports.entryPage = entryPage;
+    UptimeKumaServer.getInstance().entryPage = entryPage;
     await StatusPage.loadDomainMappingList();
 
     log.info("server", "Adding route");
@@ -176,14 +178,15 @@ let needSetup = false;
 
         log.debug("entry", `Request Domain: ${hostname}`);
 
+        const uptimeKumaEntryPage=UptimeKumaServer.getInstance().entryPage;
         if (hostname in StatusPage.domainMappingList) {
             log.debug("entry", "This is a status page domain");
 
             let slug = StatusPage.domainMappingList[hostname];
             await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
 
-        } else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
-            response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
+        } else if (uptimeKumaEntryPage && uptimeKumaEntryPage.startsWith('statusPage-')) {
+            response.redirect("/status/" + uptimeKumaEntryPage.replace("statusPage-", ""));
 
         } else {
             response.redirect("/dashboard");
@@ -200,7 +203,7 @@ let needSetup = false;
     // Robots.txt
     app.get("/robots.txt", async (_request, response) => {
         let txt = "User-agent: *\nDisallow:";
-        if (! await setting("searchEngineIndex")) {
+        if (!await setting("searchEngineIndex")) {
             txt += " /";
         }
         response.setHeader("Content-Type", "text/plain");
@@ -1085,6 +1088,7 @@ let needSetup = false;
 
                 await setSettings("general", data);
                 exports.entryPage = data.entryPage;
+                UptimeKumaServer.getInstance().entryPage = data.entryPage;
 
                 callback({
                     ok: true,

From 3e699f8ac3c544cd0dd7d9459ef9b8da3c7efd65 Mon Sep 17 00:00:00 2001
From: George Tsomlektsis <ultrawelfaredev@gmail.com>
Date: Mon, 3 Oct 2022 18:01:52 +0300
Subject: [PATCH 075/134] Fix linting errors.

---
 server/server.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/server/server.js b/server/server.js
index 0f215245..620e5bb4 100644
--- a/server/server.js
+++ b/server/server.js
@@ -178,14 +178,14 @@ let needSetup = false;
 
         log.debug("entry", `Request Domain: ${hostname}`);
 
-        const uptimeKumaEntryPage=UptimeKumaServer.getInstance().entryPage;
+        const uptimeKumaEntryPage = UptimeKumaServer.getInstance().entryPage;
         if (hostname in StatusPage.domainMappingList) {
             log.debug("entry", "This is a status page domain");
 
             let slug = StatusPage.domainMappingList[hostname];
             await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
 
-        } else if (uptimeKumaEntryPage && uptimeKumaEntryPage.startsWith('statusPage-')) {
+        } else if (uptimeKumaEntryPage && uptimeKumaEntryPage.startsWith("statusPage-")) {
             response.redirect("/status/" + uptimeKumaEntryPage.replace("statusPage-", ""));
 
         } else {

From 068675716086fe8cd24922dddc6c7d2db5c07a95 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 4 Oct 2022 16:19:56 +0800
Subject: [PATCH 076/134] [Docker Monitor] Change `tcp://` to `http://`

---
 server/docker.js                                | 14 +++++++++++++-
 server/model/monitor.js                         |  3 ++-
 server/socket-handlers/docker-socket-handler.js |  2 +-
 src/components/DockerHostDialog.vue             |  3 ++-
 4 files changed, 18 insertions(+), 4 deletions(-)

diff --git a/server/docker.js b/server/docker.js
index 177fa6cb..ff231502 100644
--- a/server/docker.js
+++ b/server/docker.js
@@ -75,7 +75,7 @@ class DockerHost {
         if (dockerHost.dockerType === "socket") {
             options.socketPath = dockerHost.dockerDaemon;
         } else if (dockerHost.dockerType === "tcp") {
-            options.baseURL = dockerHost.dockerDaemon;
+            options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
         }
 
         let res = await axios.request(options);
@@ -99,6 +99,18 @@ class DockerHost {
         }
 
     }
+
+    /**
+     * Since axios 0.27.X, it does not accept `tcp://` protocol.
+     * Change it to `http://` on the fly in order to fix it. (https://github.com/louislam/uptime-kuma/issues/2165)
+     */
+    static patchDockerURL(url) {
+        if (typeof url === "string") {
+            // Replace the first occurrence only with g
+            return url.replace(/tcp:\/\//g, "http://");
+        }
+        return url;
+    }
 }
 
 module.exports = {
diff --git a/server/model/monitor.js b/server/model/monitor.js
index f96b4df0..ac892560 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -17,6 +17,7 @@ const version = require("../../package.json").version;
 const apicache = require("../modules/apicache");
 const { UptimeKumaServer } = require("../uptime-kuma-server");
 const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
+const { DockerHost } = require("../docker");
 
 /**
  * status:
@@ -498,7 +499,7 @@ class Monitor extends BeanModel {
                     if (dockerHost._dockerType === "socket") {
                         options.socketPath = dockerHost._dockerDaemon;
                     } else if (dockerHost._dockerType === "tcp") {
-                        options.baseURL = dockerHost._dockerDaemon;
+                        options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
                     }
 
                     log.debug(`[${this.name}] Axios Request`);
diff --git a/server/socket-handlers/docker-socket-handler.js b/server/socket-handlers/docker-socket-handler.js
index 5a53494d..542f18ce 100644
--- a/server/socket-handlers/docker-socket-handler.js
+++ b/server/socket-handlers/docker-socket-handler.js
@@ -56,7 +56,7 @@ module.exports.dockerSocketHandler = (socket) => {
             let amount = await DockerHost.testDockerHost(dockerHost);
             let msg;
 
-            if (amount > 1) {
+            if (amount >= 1) {
                 msg = "Connected Successfully. Amount of containers: " + amount;
             } else {
                 msg = "Connected Successfully, but there are no containers?";
diff --git a/src/components/DockerHostDialog.vue b/src/components/DockerHostDialog.vue
index 92a8ce45..50ffa49c 100644
--- a/src/components/DockerHostDialog.vue
+++ b/src/components/DockerHostDialog.vue
@@ -30,7 +30,8 @@
                                 {{ $t("Examples") }}:
                                 <ul>
                                     <li>/var/run/docker.sock</li>
-                                    <li>tcp://localhost:2375</li>
+                                    <li>http://localhost:2375</li>
+                                    <li>https://localhost:2376 (TLS)</li>
                                 </ul>
                             </div>
                         </div>

From 16b2cf0e89ea421aa2640e3c1fa330e6d2f837e4 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 4 Oct 2022 17:50:11 +0800
Subject: [PATCH 077/134] Update to 1.18.2

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 219042aa..2357bfe4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "uptime-kuma",
-    "version": "1.18.0",
+    "version": "1.18.2",
     "license": "MIT",
     "repository": {
         "type": "git",
@@ -40,7 +40,7 @@
         "build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
         "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
         "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
-        "setup": "git checkout 1.18.0 && npm ci --production && npm run download-dist",
+        "setup": "git checkout 1.18.2 && npm ci --production && npm run download-dist",
         "download-dist": "node extra/download-dist.js",
         "mark-as-nightly": "node extra/mark-as-nightly.js",
         "reset-password": "node extra/reset-password.js",

From afbc283423c633240c9628308508c9c32ec69d8b Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 4 Oct 2022 22:23:57 +0800
Subject: [PATCH 078/134] Move Cypress directory and convert it to JavaScript
 (#2170)

---
 config/cypress.config.js                      | 26 +++++++++++++++++++
 cypress.config.ts                             | 15 -----------
 cypress/e2e/setup.cy.ts                       | 24 -----------------
 cypress/support/actors/actor.ts               |  8 ------
 cypress/support/e2e.ts                        |  1 -
 cypress/support/tasks/setup-task.ts           | 15 -----------
 package.json                                  |  3 ++-
 src/util.js                                   |  6 ++---
 test/cypress/e2e/setup.cy.js                  | 18 +++++++++++++
 {cypress => test/cypress}/plugins/index.js    |  0
 test/cypress/support/actors/actor.js          |  8 ++++++
 .../cypress/support/commands.js               |  0
 .../cypress/support/const/user-data.js        |  2 +-
 test/cypress/support/e2e.js                   |  1 +
 .../cypress/support/pages/dashboard-page.js   |  2 +-
 .../cypress/support/pages/setup-page.js       |  2 +-
 test/cypress/support/tasks/setup-task.js      | 11 ++++++++
 tsconfig.json                                 |  8 +++---
 18 files changed, 75 insertions(+), 75 deletions(-)
 create mode 100644 config/cypress.config.js
 delete mode 100644 cypress.config.ts
 delete mode 100644 cypress/e2e/setup.cy.ts
 delete mode 100644 cypress/support/actors/actor.ts
 delete mode 100644 cypress/support/e2e.ts
 delete mode 100644 cypress/support/tasks/setup-task.ts
 create mode 100644 test/cypress/e2e/setup.cy.js
 rename {cypress => test/cypress}/plugins/index.js (100%)
 create mode 100644 test/cypress/support/actors/actor.js
 rename cypress/support/commands.ts => test/cypress/support/commands.js (100%)
 rename cypress/support/const/user-data.ts => test/cypress/support/const/user-data.js (62%)
 create mode 100644 test/cypress/support/e2e.js
 rename cypress/support/pages/dasboard-page.ts => test/cypress/support/pages/dashboard-page.js (62%)
 rename cypress/support/pages/setup-page.ts => test/cypress/support/pages/setup-page.js (90%)
 create mode 100644 test/cypress/support/tasks/setup-task.js

diff --git a/config/cypress.config.js b/config/cypress.config.js
new file mode 100644
index 00000000..4eeb6845
--- /dev/null
+++ b/config/cypress.config.js
@@ -0,0 +1,26 @@
+const { defineConfig } = require("cypress");
+
+module.exports = defineConfig({
+    e2e: {
+        setupNodeEvents(on, config) {
+
+        },
+        fixturesFolder: "test/cypress/fixtures",
+        screenshotsFolder: "test/cypress/screenshots",
+        videosFolder: "test/cypress/videos",
+        downloadsFolder: "test/cypress/downloads",
+        supportFile: "test/cypress/support/e2e.js",
+        baseUrl: "http://localhost:3002",
+        defaultCommandTimeout: 10000,
+        pageLoadTimeout: 60000,
+        viewportWidth: 1920,
+        viewportHeight: 1080,
+        specPattern: [
+            "test/cypress/e2e/setup.cy.js",
+            "test/cypress/e2e/**/*.js"
+        ],
+    },
+    env: {
+        baseUrl: "http://localhost:3002",
+    },
+});
diff --git a/cypress.config.ts b/cypress.config.ts
deleted file mode 100644
index d97e0875..00000000
--- a/cypress.config.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { defineConfig } from "cypress";
-
-export default defineConfig({
-    e2e: {
-        baseUrl: "http://localhost:3002",
-        defaultCommandTimeout: 10000,
-        pageLoadTimeout: 60000,
-        viewportWidth: 1920,
-        viewportHeight: 1080,
-        specPattern: ["cypress/e2e/setup.cy.ts", "cypress/e2e/**/*.ts"],
-    },
-    env: {
-        baseUrl: "http://localhost:3002",
-    },
-});
diff --git a/cypress/e2e/setup.cy.ts b/cypress/e2e/setup.cy.ts
deleted file mode 100644
index 94e18ede..00000000
--- a/cypress/e2e/setup.cy.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { actor } from "../support/actors/actor";
-import { DEFAULT_USER_DATA } from "../support/const/user-data";
-import { DashboardPage } from "../support/pages/dasboard-page";
-import { SetupPage } from "../support/pages/setup-page";
-
-describe("user can create a new account on setup page", () => {
-    before(() => {
-        cy.visit("/setup");
-    });
-
-    it("user can create new account", () => {
-        cy.url().should("be.equal", SetupPage.url);
-        actor.setupTask.fillAndSubmitSetupForm(
-            DEFAULT_USER_DATA.username,
-            DEFAULT_USER_DATA.password,
-            DEFAULT_USER_DATA.password
-        );
-
-        cy.url().should("be.equal", DashboardPage.url);
-        cy.get('[role="alert"]')
-            .should("be.visible")
-            .and("contain.text", "Added Successfully.");
-    });
-});
diff --git a/cypress/support/actors/actor.ts b/cypress/support/actors/actor.ts
deleted file mode 100644
index 680c26ce..00000000
--- a/cypress/support/actors/actor.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { SetupTask } from "../tasks/setup-task";
-
-class Actor {
-    setupTask: SetupTask = new SetupTask();
-}
-
-const actor = new Actor();
-export { actor };
diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts
deleted file mode 100644
index f887c29a..00000000
--- a/cypress/support/e2e.ts
+++ /dev/null
@@ -1 +0,0 @@
-import "./commands";
diff --git a/cypress/support/tasks/setup-task.ts b/cypress/support/tasks/setup-task.ts
deleted file mode 100644
index 866e3ca5..00000000
--- a/cypress/support/tasks/setup-task.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { SetupPage } from "../pages/setup-page";
-
-export class SetupTask {
-    fillAndSubmitSetupForm(
-        username: string,
-        password: string,
-        passwordRepeat: string
-    ) {
-        cy.get(SetupPage.usernameInput).type(username);
-        cy.get(SetupPage.passWordInput).type(password);
-        cy.get(SetupPage.passwordRepeatInput).type(passwordRepeat);
-
-        cy.get(SetupPage.submitSetupForm).click();
-    }
-}
diff --git a/package.json b/package.json
index 2357bfe4..479876a6 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,8 @@
         "build-dist-and-restart": "npm run build && npm run start-server-dev",
         "start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
         "cy:test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --e2e",
-        "cy:run": "npx cypress run --browser chrome --headless"
+        "cy:run": "npx cypress run --browser chrome --headless --config-file ./config/cypress.config.js",
+        "cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\""
     },
     "dependencies": {
         "@louislam/sqlite3": "~15.0.6",
diff --git a/src/util.js b/src/util.js
index d5e3617f..d766feb7 100644
--- a/src/util.js
+++ b/src/util.js
@@ -280,9 +280,9 @@ function getCryptoRandomInt(min, max) {
 }
 exports.getCryptoRandomInt = getCryptoRandomInt;
 /**
- * Generate a secret
- * @param length Lenght of secret to generate
- * @returns
+ * Generate a random alphanumeric string of fixed length
+ * @param length Length of string to generate
+ * @returns string
  */
 function genSecret(length = 64) {
     let secret = "";
diff --git a/test/cypress/e2e/setup.cy.js b/test/cypress/e2e/setup.cy.js
new file mode 100644
index 00000000..57960403
--- /dev/null
+++ b/test/cypress/e2e/setup.cy.js
@@ -0,0 +1,18 @@
+const actor = require("../support/actors/actor");
+const userData = require("../support/const/user-data");
+const dashboardPage = require("../support/pages/dashboard-page");
+const setupPage = require("../support/pages/setup-page");
+
+describe("user can create a new account on setup page", () => {
+    before(() => {
+        cy.visit("/setup");
+    });
+    it("user can create new account", () => {
+        cy.url().should("be.equal", setupPage.SetupPage.url);
+        actor.actor.setupTask.fillAndSubmitSetupForm(userData.DEFAULT_USER_DATA.username, userData.DEFAULT_USER_DATA.password, userData.DEFAULT_USER_DATA.password);
+        cy.url().should("be.equal", dashboardPage.DashboardPage.url);
+        cy.get('[role="alert"]')
+            .should("be.visible")
+            .and("contain.text", "Added Successfully.");
+    });
+});
diff --git a/cypress/plugins/index.js b/test/cypress/plugins/index.js
similarity index 100%
rename from cypress/plugins/index.js
rename to test/cypress/plugins/index.js
diff --git a/test/cypress/support/actors/actor.js b/test/cypress/support/actors/actor.js
new file mode 100644
index 00000000..9775880b
--- /dev/null
+++ b/test/cypress/support/actors/actor.js
@@ -0,0 +1,8 @@
+const setupTask = require("../tasks/setup-task");
+class Actor {
+    constructor() {
+        this.setupTask = new setupTask.SetupTask();
+    }
+}
+const actor = new Actor();
+exports.actor = actor;
diff --git a/cypress/support/commands.ts b/test/cypress/support/commands.js
similarity index 100%
rename from cypress/support/commands.ts
rename to test/cypress/support/commands.js
diff --git a/cypress/support/const/user-data.ts b/test/cypress/support/const/user-data.js
similarity index 62%
rename from cypress/support/const/user-data.ts
rename to test/cypress/support/const/user-data.js
index ee2264dd..983597bd 100644
--- a/cypress/support/const/user-data.ts
+++ b/test/cypress/support/const/user-data.js
@@ -1,4 +1,4 @@
-export const DEFAULT_USER_DATA = {
+exports.DEFAULT_USER_DATA = {
     username: "testuser",
     password: "testuser123",
 };
diff --git a/test/cypress/support/e2e.js b/test/cypress/support/e2e.js
new file mode 100644
index 00000000..449ab857
--- /dev/null
+++ b/test/cypress/support/e2e.js
@@ -0,0 +1 @@
+require("./commands");
diff --git a/cypress/support/pages/dasboard-page.ts b/test/cypress/support/pages/dashboard-page.js
similarity index 62%
rename from cypress/support/pages/dasboard-page.ts
rename to test/cypress/support/pages/dashboard-page.js
index 48660dc1..fc2d67e1 100644
--- a/cypress/support/pages/dasboard-page.ts
+++ b/test/cypress/support/pages/dashboard-page.js
@@ -1,3 +1,3 @@
-export const DashboardPage = {
+exports.DashboardPage = {
     url: Cypress.env("baseUrl") + "/dashboard",
 };
diff --git a/cypress/support/pages/setup-page.ts b/test/cypress/support/pages/setup-page.js
similarity index 90%
rename from cypress/support/pages/setup-page.ts
rename to test/cypress/support/pages/setup-page.js
index 8c1b9cfa..44a525a8 100644
--- a/cypress/support/pages/setup-page.ts
+++ b/test/cypress/support/pages/setup-page.js
@@ -1,4 +1,4 @@
-export const SetupPage = {
+exports.SetupPage = {
     url: Cypress.env("baseUrl") + "/setup",
     usernameInput: '[data-cy="username-input"]',
     passWordInput: '[data-cy="password-input"]',
diff --git a/test/cypress/support/tasks/setup-task.js b/test/cypress/support/tasks/setup-task.js
new file mode 100644
index 00000000..205f78c2
--- /dev/null
+++ b/test/cypress/support/tasks/setup-task.js
@@ -0,0 +1,11 @@
+const setupPage = require("../pages/setup-page");
+
+class SetupTask {
+    fillAndSubmitSetupForm(username, password, passwordRepeat) {
+        cy.get(setupPage.SetupPage.usernameInput).type(username);
+        cy.get(setupPage.SetupPage.passWordInput).type(password);
+        cy.get(setupPage.SetupPage.passwordRepeatInput).type(passwordRepeat);
+        cy.get(setupPage.SetupPage.submitSetupForm).click();
+    }
+}
+exports.SetupTask = SetupTask;
diff --git a/tsconfig.json b/tsconfig.json
index cd5f7c5d..c5454642 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -11,11 +11,9 @@
         "removeComments": false,
         "preserveConstEnums": true,
         "sourceMap": false,
-        "strict": true,
-        "types": ["cypress"]
+        "strict": true
     },
     "files": [
-        "./src/util.ts",
-    ],
-    "include": ["cypress/**/*.ts"]
+        "./src/util.ts"
+    ]
 }

From f1a9046193f014148d6fcdaf53ee53c6a2f4608e Mon Sep 17 00:00:00 2001
From: Sympatron GmbH <35803463+Sympatron@users.noreply.github.com>
Date: Tue, 4 Oct 2022 15:30:19 +0000
Subject: [PATCH 079/134] Prevent terminal window from showing when using ping
 on Windows (#2152)

---
 server/ping-lite.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/ping-lite.js b/server/ping-lite.js
index b7d003b8..05dff31d 100644
--- a/server/ping-lite.js
+++ b/server/ping-lite.js
@@ -105,7 +105,7 @@ Ping.prototype.send = function (callback) {
     let _exited;
     let _errored;
 
-    this._ping = spawn(this._bin, this._args); // spawn the binary
+    this._ping = spawn(this._bin, this._args, { windowsHide: true }); // spawn the binary
 
     this._ping.on("error", function (err) { // handle binary errors
         _errored = true;

From d565320f7422fe4493fc2f22962219780848476e Mon Sep 17 00:00:00 2001
From: Muhammed Hussein karimi <info@karimi.dev>
Date: Wed, 5 Oct 2022 08:23:58 +0330
Subject: [PATCH 080/134] New Demo Server (#2172)

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
---
 README.md | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index b81c0be2..2ebf31a0 100644
--- a/README.md
+++ b/README.md
@@ -15,11 +15,12 @@ It is a self-hosted monitoring tool like "Uptime Robot".
 
 Try it!
 
-https://demo.uptime.kuma.pet
+https://demo.uptime.kuma.pet (Tokyo Server)
+https://demo.uptime-kuma.karimi.dev:27000 (Europe server)
 
-It is a temporary live demo, all data will be deleted after 10 minutes. The server is located in Tokyo, so if you live far from there, it may affect your experience. I suggest that you should install and try it out for the best demo experience.
+It is a temporary live demo, all data will be deleted after 10 minutes. Use the one that is closer to you, but I suggest that you should install and try it out for the best demo experience.
 
-VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
+Tokyo VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
 
 ## ⭐ Features
 

From 12696dd53ea1eb5407c46cda05d38ac30603121f Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Wed, 5 Oct 2022 12:54:25 +0800
Subject: [PATCH 081/134] Update README.md

---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 2ebf31a0..cb81f4db 100644
--- a/README.md
+++ b/README.md
@@ -15,8 +15,8 @@ It is a self-hosted monitoring tool like "Uptime Robot".
 
 Try it!
 
-https://demo.uptime.kuma.pet (Tokyo Server)
-https://demo.uptime-kuma.karimi.dev:27000 (Europe server)
+- https://demo.uptime.kuma.pet (Tokyo Server)
+- https://demo.uptime-kuma.karimi.dev:27000 (Europe server)
 
 It is a temporary live demo, all data will be deleted after 10 minutes. Use the one that is closer to you, but I suggest that you should install and try it out for the best demo experience.
 

From e5145a209ac642a36da539dc414f26da75486c52 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Wed, 5 Oct 2022 13:28:13 +0800
Subject: [PATCH 082/134] Update cypress config

---
 config/cypress.config.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/config/cypress.config.js b/config/cypress.config.js
index 4eeb6845..26784e88 100644
--- a/config/cypress.config.js
+++ b/config/cypress.config.js
@@ -1,7 +1,9 @@
 const { defineConfig } = require("cypress");
 
 module.exports = defineConfig({
+    projectId: "vyjuem",
     e2e: {
+        experimentalStudio: true,
         setupNodeEvents(on, config) {
 
         },

From 64ba2dce2483eb413ac2c4c1ca6d3e513d8fcafe Mon Sep 17 00:00:00 2001
From: Cyril59310 <70776486+cyril59310@users.noreply.github.com>
Date: Wed, 5 Oct 2022 07:57:38 +0200
Subject: [PATCH 083/134] Update FR language (#2173)

---
 src/languages/fr-FR.js | 120 +++++++++++++++++++++++++++++++++++++----
 1 file changed, 110 insertions(+), 10 deletions(-)

diff --git a/src/languages/fr-FR.js b/src/languages/fr-FR.js
index 10b8c1ba..22df2728 100644
--- a/src/languages/fr-FR.js
+++ b/src/languages/fr-FR.js
@@ -200,7 +200,7 @@ export default {
     chatIDNotFound: "ID du salon introuvable, envoyez un message via le bot avant",
     webhook: "Webhook",
     "Post URL": "Post URL",
-    "Content Type": "Content Type",
+    "Content Type": "Type de contenu",
     webhookJsonDesc: "{0} est bien/bon pour tous les serveurs HTTP modernes comme express.js",
     webhookFormDataDesc: "{multipart} est bien/bon pour du PHP, vous avez juste besoin de mettre le json via/depuis {decodeFunction}",
     smtp: "Email (SMTP)",
@@ -227,8 +227,8 @@ export default {
     wayToCheckSignalURL: "Vous pouvez regarder l'URL sur comment le mettre en place :",
     signalImportant: "IMPORTANT : Vous ne pouvez pas mixer les groupes et les numéros en destinataires !",
     gotify: "Gotify",
-    "Application Token": "Application Token",
-    "Server URL": "Server URL",
+    "Application Token": "Jeton d'application",
+    "Server URL": "URL du serveur",
     Priority: "Priorité",
     slack: "Slack",
     "Icon Emoji": "Icon Emoji",
@@ -287,7 +287,7 @@ export default {
     promosmsTypeSpeed: "SMS SPEED - La plus haute des priorités dans le système. Très rapide et fiable mais cher (environ le double du prix d'un SMS FULL).",
     promosmsPhoneNumber: "Numéro de téléphone (Poiur les déstinataires Polonais, vous pouvez enlever les codes interna.)",
     promosmsSMSSender: "SMS Expéditeur : Nom pré-enregistré ou l'un de base : InfoSMS, SMS Info, MaxSMS, INFO, SMS",
-    "Primary Base URL": "Primary Base URL",
+    "Primary Base URL": "URL principale",
     emailCustomSubject: "Sujet personalisé",
     clicksendsms: "ClickSend SMS",
     checkPrice: "Vérification {0} tarifs :",
@@ -342,13 +342,13 @@ export default {
     Title: "Titre",
     Content: "Contenu",
     Style: "Style",
-    info: "info",
+    info: "Info",
     warning: "Attention",
-    danger: "danger",
+    danger: "Danger",
     error: "Erreur",
-    critical: "critique",
-    primary: "primaire",
-    light: "blanc",
+    critical: "Critique",
+    primary: "Primaire",
+    light: "Blanc",
     dark: "Noir",
     Post: "Post",
     "Please input title and content": "Veuillez entrer le titre et le contenu",
@@ -390,7 +390,7 @@ export default {
     Installed: "Installé",
     "Not installed": "Pas installé",
     "Remove Token": "Supprimer le jeton",
-    Slug: "chemin",
+    Slug: "Chemin",
     "The slug is already taken. Please choose another slug.": "Le chemin est déjà pris. Veuillez choisir un autre chemin.",
     Authentication: "Authentication",
     "Page Not Found": "Page non trouvée",
@@ -431,4 +431,104 @@ export default {
     "Trigger type:": "Type de déclencheur:",
     "Event type:": "Type d'événement:",
     "Event data:": "Données d'événement:",
+    topic: "Topic",
+    topicExplanation: "MQTT sujet à surveiller",
+    successMessage: "Message de réussite",
+    successMessageExplanation: "MQTT message qui sera considéré comme un succès",
+    "Powered by": "Propulsé par",
+    serwersms: "SerwerSMS.pl",
+    stackfield: "Stackfield",
+    smtpDkimSettings: "Paramètres DKIM",
+    smtpDkimDesc: "Veuillez vous référer au Nodemailer DKIM {0} pour l'utilisation.",
+    documentation: "Documentation",
+    smtpDkimDomain: "Nom de domaine",
+    smtpDkimKeySelector: "Sélecteur de clé",
+    smtpDkimPrivateKey: "Clé privée",
+    smtpDkimHashAlgo: "Algorithme de hachage (facultatif)",
+    smtpDkimheaderFieldNames: "Clés d'en-tête à signer (facultatif)",
+    smtpDkimskipFields: "Clés d'en-tête à ne pas signer (facultatif)",
+    wayToGetPagerDutyKey: "Vous pouvez l'obtenir en allant dans Service -> Annuaire des services -> (Sélectionner un service) -> Intégrations -> Ajouter une intégration. Ici, vous pouvez rechercher \"Events API V2\". Plus d'infos {0}",
+    "Integration Key": "Clé d'intégration",
+    "Integration URL": "URL d'intégration",
+    "Auto resolve or acknowledged": "Résolution automatique ou accusé de réception",
+    "do nothing": "ne fais rien",
+    "auto acknowledged": "accusé de réception automatique",
+    "auto resolve": "résolution automatique",
+    AccessKeyId: "ID de clé d'accès",
+    SecretAccessKey: "Clé secrète d'accès",
+    PhoneNumbers: "Les numéros de téléphone",
+    SignName: "Signature",
+    "Sms template must contain parameters: ": "Le modèle de SMS doit contenir des paramètres : ",
+    SecretKey: "Clé secrète",
+    "For safety, must use secret key": "Pour la sécurité, doit utiliser la clé secrète",
+    "Device Token": "Jeton d'appareil",
+    Platform: "Plateforme",
+    Retry: "Recommencez",
+    Topic: "Topic",
+    "Proxy server has authentication": "Le serveur proxy a une authentification",
+    Running: "Fonctionne",
+    "Not running": "Ne fonctionne pas",
+    Start: "Start",
+    Stop: "Stop",
+    "Uptime Kuma": "Uptime Kuma",
+    "No Proxy": "Pas de Proxy",
+    "HTTP Basic Auth": "Authentification de base HTTP",
+    "Reverse Proxy": "Proxy inverse",
+    wayToGetCloudflaredURL: "(Télécharger cloudflared depuis {0})",
+    cloudflareWebsite: "le site Cloudflare ",
+    "Message:": "Message:",
+    "Don't know how to get the token? Please read the guide:": "Vous ne savez pas comment obtenir le jeton ? Veuillez lire le guide:",
+    "The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "La connexion actuelle peut être perdue si vous vous connectez actuellement via Cloudflare Tunnel. Êtes-vous sûr de vouloir l'arrêter ? Tapez votre mot de passe actuel pour le confirmer.",
+    "HTTP Headers": "En-têtes HTTP",
+    "Trust Proxy": "Proxy de confiance",
+    "Other Software": "Autres logiciels",
+    "For example: nginx, Apache and Traefik.": "Par exemple : nginx, Apache et Traefik.",
+    "Please read": "S'il vous plaît Lisez",
+    "Valid To:": "Valable pour:",
+    "Days Remaining:": "Jours restant:",
+    "Domain Name Expiry Notification": "Notification d'expiration de nom de domaine",
+    "Date Created": "Date de création",
+    HomeAssistant: "Home Assistant",
+    onebotHttpAddress: "Adresse HTTP OneBot",
+    onebotMessageType: "Type de message OneBot",
+    onebotGroupMessage: "Groupe",
+    onebotUserOrGroupId: "ID de groupe/utilisateur",
+    onebotSafetyTips: "Pour des raisons de sécurité, vous devez définir un jeton d'accès",
+    "PushDeer Key": "Clé PushDeer",
+    "Show Powered By": "Afficher \"Propulsé par\"",
+    RadiusSecretDescription: "Secret partagé entre le client et le serveur",
+    RadiusCalledStationId: "Identifiant de la station appelée",
+    RadiusCalledStationIdDescription: "Identifiant de l'appareil appelé",
+    RadiusCallingStationId: "Identifiant de la station appelante",
+    RadiusCallingStationIdDescription: "Identifiant de l'appareil appelant",
+    "Certificate Expiry Notification": "Notification d'expiration du certificat",
+    "API Username": "Nom d'utilisateur de l'API",
+    "API Key": "clé API",
+    "Recipient Number": "Numéro du destinataire",
+    "From Name/Number": "De Nom/Numéro",
+    "Leave blank to use a shared sender number.": "Laisser vide pour utiliser un numéro d'expéditeur partagé.",
+    "Octopush API Version": "Version de l'API Octopush",
+    octopushAPIKey: "\"Clé API\" à partir des informations d'identification de l'API HTTP dans le panneau de configuration",
+    octopushLogin: "\"Connexion\" à partir des informations d'identification de l'API HTTP dans le panneau de configuration",
+    "Using a Reverse Proxy?": "Utiliser un proxy inverse ?",
+    "Check how to config it for WebSocket": "Vérifiez comment le configurer pour WebSocket",
+    wayToGetClickSendSMSToken: "Vous pouvez obtenir le nom d'utilisateur API et la clé API à partir de {0} .",
+    "Connection String": "Chaîne de connexion",
+    Query: "Requête",
+    tcp: "TCP / HTTP",
+    "Docker Container": "Conteneur Docker",
+    Workstation: "Poste de travail",
+    disableCloudflaredNoAuthMsg: "Vous êtes en mode No Auth, un mot de passe n'est pas nécessaire.",
+    "Long-Lived Access Token": "Jeton d'accès de longue durée",
+    "Then choose an action, for example switch the scene to where an RGB light is red.": "Ensuite, choisissez une action, par exemple basculer la scène là où une lumière RVB est rouge.",
+    "Frontend Version": "Frontend Version",
+    "Frontend Version do not match backend version!": "La version frontale ne correspond pas à la version principale !",
+    "Base URL": "URL de base",
+    goAlertInfo: "GoAlert est une application open source pour la planification des appels, les escalades automatisées et les notifications (comme les SMS ou les appels vocaux). Engagez automatiquement la bonne personne, de la bonne manière et au bon moment ! {0}",
+    goAlertIntegrationKeyInfo: "Obtenez la clé d'intégration d'API générique pour le service dans ce format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" généralement la valeur du paramètre de jeton de l'URL copiée.",
+    goAlert: "GoAlert",
+    backupOutdatedWarning: "Obsolète : étant donné que de nombreuses fonctionnalités ont été ajoutées et que cette fonctionnalité de sauvegarde est un peu non maintenue, elle ne peut pas générer ou restaurer une sauvegarde complète.",
+    backupRecommend: "Veuillez sauvegarder le volume ou le dossier de données (./data/) directement à la place.",
+    Optional: "Optionnel",
+    squadcast: "Squadcast",
 };

From a5c102e750f49b510fbf229bb97ce338fadad1f3 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Wed, 5 Oct 2022 14:19:50 +0800
Subject: [PATCH 084/134] Update README.md

---
 README.md | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index cb81f4db..55a6ec50 100644
--- a/README.md
+++ b/README.md
@@ -15,13 +15,11 @@ It is a self-hosted monitoring tool like "Uptime Robot".
 
 Try it!
 
-- https://demo.uptime.kuma.pet (Tokyo Server)
-- https://demo.uptime-kuma.karimi.dev:27000 (Europe server)
+- Tokyo Demo Server: https://demo.uptime.kuma.pet (Sponsored by [Uptime Kuma Sponsors](https://github.com/louislam/uptime-kuma#%EF%B8%8F-sponsors))
+- Europe Demo Server: https://demo.uptime-kuma.karimi.dev:27000 (Provided by [@mhkarimi1383](https://github.com/mhkarimi1383))
 
 It is a temporary live demo, all data will be deleted after 10 minutes. Use the one that is closer to you, but I suggest that you should install and try it out for the best demo experience.
 
-Tokyo VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
-
 ## ⭐ Features
 
 * Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers.

From b993859926566efc31ed962029d857e04a58d493 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Wed, 5 Oct 2022 14:26:30 +0800
Subject: [PATCH 085/134] Drop Jest e2e testing (#2174)

---
 config/jest-debug-env.js        |   33 -
 config/jest-frontend.config.js  |    5 -
 config/jest-puppeteer.config.js |   20 -
 config/jest.config.js           |   12 -
 package-lock.json               | 1367 +------------------------------
 package.json                    |    4 -
 server/util-server.js           |    2 +-
 test/e2e.spec.js                |  329 --------
 test/frontend.spec.js           |   42 -
 test/prepare-jest.js            |   10 -
 10 files changed, 3 insertions(+), 1821 deletions(-)
 delete mode 100644 config/jest-debug-env.js
 delete mode 100644 config/jest-frontend.config.js
 delete mode 100644 config/jest-puppeteer.config.js
 delete mode 100644 config/jest.config.js
 delete mode 100644 test/e2e.spec.js
 delete mode 100644 test/frontend.spec.js
 delete mode 100644 test/prepare-jest.js

diff --git a/config/jest-debug-env.js b/config/jest-debug-env.js
deleted file mode 100644
index 74f6d783..00000000
--- a/config/jest-debug-env.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const PuppeteerEnvironment = require("jest-environment-puppeteer");
-const util = require("util");
-
-class DebugEnv extends PuppeteerEnvironment {
-    async handleTestEvent(event, state) {
-        const ignoredEvents = [
-            "setup",
-            "add_hook",
-            "start_describe_definition",
-            "add_test",
-            "finish_describe_definition",
-            "run_start",
-            "run_describe_start",
-            "test_start",
-            "hook_start",
-            "hook_success",
-            "test_fn_start",
-            "test_fn_success",
-            "test_done",
-            "run_describe_finish",
-            "run_finish",
-            "teardown",
-            "test_fn_failure",
-        ];
-        if (!ignoredEvents.includes(event.name)) {
-            console.log(
-                new Date().toString() + ` Unhandled event [${event.name}] ` + util.inspect(event)
-            );
-        }
-    }
-}
-
-module.exports = DebugEnv;
diff --git a/config/jest-frontend.config.js b/config/jest-frontend.config.js
deleted file mode 100644
index ab6af7f1..00000000
--- a/config/jest-frontend.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
-    "rootDir": "..",
-    "testRegex": "./test/frontend.spec.js",
-};
-
diff --git a/config/jest-puppeteer.config.js b/config/jest-puppeteer.config.js
deleted file mode 100644
index dc4f7b34..00000000
--- a/config/jest-puppeteer.config.js
+++ /dev/null
@@ -1,20 +0,0 @@
-module.exports = {
-    "launch": {
-        "dumpio": true,
-        "slowMo": 500,
-        "headless": process.env.HEADLESS_TEST || false,
-        "userDataDir": "./data/test-chrome-profile",
-        args: [
-            "--disable-setuid-sandbox",
-            "--disable-gpu",
-            "--disable-dev-shm-usage",
-            "--no-default-browser-check",
-            "--no-experiments",
-            "--no-first-run",
-            "--no-pings",
-            "--no-sandbox",
-            "--no-zygote",
-            "--single-process",
-        ],
-    }
-};
diff --git a/config/jest.config.js b/config/jest.config.js
deleted file mode 100644
index 2d3f585e..00000000
--- a/config/jest.config.js
+++ /dev/null
@@ -1,12 +0,0 @@
-module.exports = {
-    "verbose": true,
-    "preset": "jest-puppeteer",
-    "globals": {
-        "__DEV__": true
-    },
-    "testRegex": "./test/e2e.spec.js",
-    "testEnvironment": "./config/jest-debug-env.js",
-    "rootDir": "..",
-    "testTimeout": 30000,
-};
-
diff --git a/package-lock.json b/package-lock.json
index 65e380d1..71827042 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "uptime-kuma",
-    "version": "1.18.0",
+    "version": "1.18.2",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "uptime-kuma",
-            "version": "1.18.0",
+            "version": "1.18.2",
             "license": "MIT",
             "dependencies": {
                 "@louislam/sqlite3": "~15.0.6",
@@ -83,12 +83,10 @@
                 "eslint-plugin-vue": "~8.7.1",
                 "favico.js": "^0.3.10",
                 "jest": "~27.2.5",
-                "jest-puppeteer": "~6.0.3",
                 "postcss-html": "~1.5.0",
                 "postcss-rtlcss": "~3.7.2",
                 "postcss-scss": "~4.0.4",
                 "prismjs": "^1.27.0",
-                "puppeteer": "~13.1.3",
                 "qrcode": "~1.5.0",
                 "rollup-plugin-visualizer": "^5.6.0",
                 "sass": "~1.42.1",
@@ -4231,15 +4229,6 @@
             "resolved": "https://registry.npmjs.org/args-parser/-/args-parser-1.3.0.tgz",
             "integrity": "sha512-If3Zi4BSjlQIJ9fgAhSiKi0oJtgMzSqh0H4wvl7XSeO16FKx7QqaHld8lZeEajPX7y1C5qKKeNgyrfyvmjmjUQ=="
         },
-        "node_modules/arr-union": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
-            "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/array-flatten": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -5317,34 +5306,6 @@
                 "wrap-ansi": "^7.0.0"
             }
         },
-        "node_modules/clone-deep": {
-            "version": "0.2.4",
-            "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz",
-            "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==",
-            "dev": true,
-            "dependencies": {
-                "for-own": "^0.1.3",
-                "is-plain-object": "^2.0.1",
-                "kind-of": "^3.0.2",
-                "lazy-cache": "^1.0.3",
-                "shallow-clone": "^0.1.2"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/clone-deep/node_modules/is-plain-object": {
-            "version": "2.0.4",
-            "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
-            "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
-            "dev": true,
-            "dependencies": {
-                "isobject": "^3.0.1"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/clone-regexp": {
             "version": "2.2.0",
             "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz",
@@ -5935,19 +5896,6 @@
             "resolved": "https://registry.npmjs.org/custom-error-instance/-/custom-error-instance-2.1.1.tgz",
             "integrity": "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg=="
         },
-        "node_modules/cwd": {
-            "version": "0.10.0",
-            "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz",
-            "integrity": "sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==",
-            "dev": true,
-            "dependencies": {
-                "find-pkg": "^0.1.2",
-                "fs-exists-sync": "^0.1.0"
-            },
-            "engines": {
-                "node": ">=0.8"
-            }
-        },
         "node_modules/cypress": {
             "version": "10.7.0",
             "resolved": "https://registry.npmjs.org/cypress/-/cypress-10.7.0.tgz",
@@ -6400,12 +6348,6 @@
             "resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz",
             "integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ=="
         },
-        "node_modules/devtools-protocol": {
-            "version": "0.0.948846",
-            "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.948846.tgz",
-            "integrity": "sha512-5fGyt9xmMqUl2VI7+rnUkKCiAQIpLns8sfQtTENy5L70ktbNw0Z3TFJ1JoFNYdx/jffz4YXU45VF75wKZD7sZQ==",
-            "dev": true
-        },
         "node_modules/diff-sequences": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
@@ -7717,18 +7659,6 @@
                 "node": ">= 0.8.0"
             }
         },
-        "node_modules/expand-tilde": {
-            "version": "1.2.2",
-            "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz",
-            "integrity": "sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q==",
-            "dev": true,
-            "dependencies": {
-                "os-homedir": "^1.0.1"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/expect": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz",
@@ -7744,12 +7674,6 @@
                 "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
             }
         },
-        "node_modules/expect-puppeteer": {
-            "version": "6.1.1",
-            "resolved": "https://registry.npmjs.org/expect-puppeteer/-/expect-puppeteer-6.1.1.tgz",
-            "integrity": "sha512-cnQF96qdoEcOD63j5NQMc0RtW9WRMW/WHKXEKsuDQ2tszhVH3qC7zkXXS4D0LTt9qCB3DEExioqylsQXvqPrUw==",
-            "dev": true
-        },
         "node_modules/express": {
             "version": "4.17.3",
             "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz",
@@ -8073,115 +7997,6 @@
             "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
             "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
         },
-        "node_modules/find-file-up": {
-            "version": "0.1.3",
-            "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz",
-            "integrity": "sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A==",
-            "dev": true,
-            "dependencies": {
-                "fs-exists-sync": "^0.1.0",
-                "resolve-dir": "^0.1.0"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/find-pkg": {
-            "version": "0.1.2",
-            "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz",
-            "integrity": "sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw==",
-            "dev": true,
-            "dependencies": {
-                "find-file-up": "^0.1.2"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/find-process": {
-            "version": "1.4.7",
-            "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.7.tgz",
-            "integrity": "sha512-/U4CYp1214Xrp3u3Fqr9yNynUrr5Le4y0SsJh2lMDDSbpwYSz3M2SMWQC+wqcx79cN8PQtHQIL8KnuY9M66fdg==",
-            "dev": true,
-            "dependencies": {
-                "chalk": "^4.0.0",
-                "commander": "^5.1.0",
-                "debug": "^4.1.1"
-            },
-            "bin": {
-                "find-process": "bin/find-process.js"
-            }
-        },
-        "node_modules/find-process/node_modules/ansi-styles": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-            "dev": true,
-            "dependencies": {
-                "color-convert": "^2.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-            }
-        },
-        "node_modules/find-process/node_modules/chalk": {
-            "version": "4.1.2",
-            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-            "dev": true,
-            "dependencies": {
-                "ansi-styles": "^4.1.0",
-                "supports-color": "^7.1.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/chalk?sponsor=1"
-            }
-        },
-        "node_modules/find-process/node_modules/color-convert": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-            "dev": true,
-            "dependencies": {
-                "color-name": "~1.1.4"
-            },
-            "engines": {
-                "node": ">=7.0.0"
-            }
-        },
-        "node_modules/find-process/node_modules/color-name": {
-            "version": "1.1.4",
-            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-            "dev": true
-        },
-        "node_modules/find-process/node_modules/has-flag": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/find-process/node_modules/supports-color": {
-            "version": "7.2.0",
-            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-            "dev": true,
-            "dependencies": {
-                "has-flag": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/find-up": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -8233,27 +8048,6 @@
                 }
             }
         },
-        "node_modules/for-in": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
-            "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/for-own": {
-            "version": "0.1.5",
-            "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
-            "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==",
-            "dev": true,
-            "dependencies": {
-                "for-in": "^1.0.1"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/forever-agent": {
             "version": "0.6.1",
             "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -8326,21 +8120,6 @@
                 "safe-buffer": "~5.1.0"
             }
         },
-        "node_modules/fs-constants": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
-            "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
-            "dev": true
-        },
-        "node_modules/fs-exists-sync": {
-            "version": "0.1.0",
-            "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz",
-            "integrity": "sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/fs-extra": {
             "version": "10.1.0",
             "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -8606,46 +8385,6 @@
                 "node": ">=10"
             }
         },
-        "node_modules/global-modules": {
-            "version": "0.2.3",
-            "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz",
-            "integrity": "sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA==",
-            "dev": true,
-            "dependencies": {
-                "global-prefix": "^0.1.4",
-                "is-windows": "^0.2.0"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/global-prefix": {
-            "version": "0.1.5",
-            "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz",
-            "integrity": "sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==",
-            "dev": true,
-            "dependencies": {
-                "homedir-polyfill": "^1.0.0",
-                "ini": "^1.3.4",
-                "is-windows": "^0.2.0",
-                "which": "^1.2.12"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/global-prefix/node_modules/which": {
-            "version": "1.3.1",
-            "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
-            "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-            "dev": true,
-            "dependencies": {
-                "isexe": "^2.0.0"
-            },
-            "bin": {
-                "which": "bin/which"
-            }
-        },
         "node_modules/globals": {
             "version": "11.12.0",
             "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -8822,18 +8561,6 @@
             "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==",
             "deprecated": "This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues."
         },
-        "node_modules/homedir-polyfill": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
-            "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
-            "dev": true,
-            "dependencies": {
-                "parse-passwd": "^1.0.0"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/hosted-git-info": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -9186,12 +8913,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/is-buffer": {
-            "version": "1.1.6",
-            "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
-            "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
-            "dev": true
-        },
         "node_modules/is-callable": {
             "version": "1.2.4",
             "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
@@ -9254,15 +8975,6 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/is-extendable": {
-            "version": "0.1.1",
-            "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
-            "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/is-extglob": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -9545,15 +9257,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/is-windows": {
-            "version": "0.2.0",
-            "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz",
-            "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/is-wsl": {
             "version": "2.2.0",
             "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -9588,15 +9291,6 @@
             "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
             "devOptional": true
         },
-        "node_modules/isobject": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-            "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/isstream": {
             "version": "0.1.2",
             "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@@ -10064,91 +9758,6 @@
                 "node": ">=8"
             }
         },
-        "node_modules/jest-dev-server": {
-            "version": "6.1.1",
-            "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-6.1.1.tgz",
-            "integrity": "sha512-z5LnaGDvlIkdMv/rppSO4+rq+GyQKf1xI9oiBxf9/2EBeN2hxRaWiMvaLNDnHPZj2PAhBXsycrKslDDoZO2Xtw==",
-            "dev": true,
-            "dependencies": {
-                "chalk": "^4.1.2",
-                "cwd": "^0.10.0",
-                "find-process": "^1.4.7",
-                "prompts": "^2.4.2",
-                "spawnd": "^6.0.2",
-                "tree-kill": "^1.2.2",
-                "wait-on": "^6.0.1"
-            }
-        },
-        "node_modules/jest-dev-server/node_modules/ansi-styles": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-            "dev": true,
-            "dependencies": {
-                "color-convert": "^2.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-            }
-        },
-        "node_modules/jest-dev-server/node_modules/chalk": {
-            "version": "4.1.2",
-            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-            "dev": true,
-            "dependencies": {
-                "ansi-styles": "^4.1.0",
-                "supports-color": "^7.1.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/chalk?sponsor=1"
-            }
-        },
-        "node_modules/jest-dev-server/node_modules/color-convert": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-            "dev": true,
-            "dependencies": {
-                "color-name": "~1.1.4"
-            },
-            "engines": {
-                "node": ">=7.0.0"
-            }
-        },
-        "node_modules/jest-dev-server/node_modules/color-name": {
-            "version": "1.1.4",
-            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-            "dev": true
-        },
-        "node_modules/jest-dev-server/node_modules/has-flag": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/jest-dev-server/node_modules/supports-color": {
-            "version": "7.2.0",
-            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-            "dev": true,
-            "dependencies": {
-                "has-flag": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-diff": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",
@@ -10367,89 +9976,6 @@
                 "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
             }
         },
-        "node_modules/jest-environment-puppeteer": {
-            "version": "6.1.1",
-            "resolved": "https://registry.npmjs.org/jest-environment-puppeteer/-/jest-environment-puppeteer-6.1.1.tgz",
-            "integrity": "sha512-Ces37g8Gdj7QaVxszeoXlvmsZxcEJN9EPUdJt8fGMLA+6ARVFKyVmFgP9xVeGyjTvzsXdtIiJdeOKMLMeD8r2A==",
-            "dev": true,
-            "dependencies": {
-                "chalk": "^4.1.2",
-                "cwd": "^0.10.0",
-                "jest-dev-server": "^6.1.1",
-                "jest-environment-node": "^27.4.4",
-                "merge-deep": "^3.0.3"
-            }
-        },
-        "node_modules/jest-environment-puppeteer/node_modules/ansi-styles": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-            "dev": true,
-            "dependencies": {
-                "color-convert": "^2.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-            }
-        },
-        "node_modules/jest-environment-puppeteer/node_modules/chalk": {
-            "version": "4.1.2",
-            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-            "dev": true,
-            "dependencies": {
-                "ansi-styles": "^4.1.0",
-                "supports-color": "^7.1.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/chalk?sponsor=1"
-            }
-        },
-        "node_modules/jest-environment-puppeteer/node_modules/color-convert": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-            "dev": true,
-            "dependencies": {
-                "color-name": "~1.1.4"
-            },
-            "engines": {
-                "node": ">=7.0.0"
-            }
-        },
-        "node_modules/jest-environment-puppeteer/node_modules/color-name": {
-            "version": "1.1.4",
-            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-            "dev": true
-        },
-        "node_modules/jest-environment-puppeteer/node_modules/has-flag": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/jest-environment-puppeteer/node_modules/supports-color": {
-            "version": "7.2.0",
-            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-            "dev": true,
-            "dependencies": {
-                "has-flag": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-get-type": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz",
@@ -10801,19 +10327,6 @@
                 }
             }
         },
-        "node_modules/jest-puppeteer": {
-            "version": "6.0.3",
-            "resolved": "https://registry.npmjs.org/jest-puppeteer/-/jest-puppeteer-6.0.3.tgz",
-            "integrity": "sha512-6GRdbkWwNu8dfzo4icpwc50+K5ECYpWyD9sxpRa03PA8Hi3byl0dcAx+NjCivSezWjAl2Iwwhujqb+bczei0Bg==",
-            "dev": true,
-            "dependencies": {
-                "expect-puppeteer": "^6.0.2",
-                "jest-environment-puppeteer": "^6.0.3"
-            },
-            "peerDependencies": {
-                "puppeteer": ">= 1.5.0"
-            }
-        },
         "node_modules/jest-regex-util": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz",
@@ -11863,18 +11376,6 @@
             "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
             "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
         },
-        "node_modules/kind-of": {
-            "version": "3.2.2",
-            "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-            "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-            "dev": true,
-            "dependencies": {
-                "is-buffer": "^1.1.5"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/kleur": {
             "version": "3.0.3",
             "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -11982,15 +11483,6 @@
                 "node": "> 0.8"
             }
         },
-        "node_modules/lazy-cache": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
-            "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/leven": {
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -12454,20 +11946,6 @@
             "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==",
             "dev": true
         },
-        "node_modules/merge-deep": {
-            "version": "3.0.3",
-            "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz",
-            "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==",
-            "dev": true,
-            "dependencies": {
-                "arr-union": "^3.1.0",
-                "clone-deep": "^0.2.4",
-                "kind-of": "^3.0.2"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/merge-descriptors": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@@ -12619,28 +12097,6 @@
                 "node": ">= 8"
             }
         },
-        "node_modules/mixin-object": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz",
-            "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==",
-            "dev": true,
-            "dependencies": {
-                "for-in": "^0.1.3",
-                "is-extendable": "^0.1.1"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/mixin-object/node_modules/for-in": {
-            "version": "0.1.8",
-            "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz",
-            "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/mkdirp": {
             "version": "1.0.4",
             "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -12652,12 +12108,6 @@
                 "node": ">=10"
             }
         },
-        "node_modules/mkdirp-classic": {
-            "version": "0.5.3",
-            "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
-            "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
-            "dev": true
-        },
         "node_modules/mqemitter": {
             "version": "4.5.0",
             "resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-4.5.0.tgz",
@@ -13292,15 +12742,6 @@
                 "node": ">= 0.8.0"
             }
         },
-        "node_modules/os-homedir": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
-            "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/ospath": {
             "version": "1.2.2",
             "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
@@ -13426,15 +12867,6 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/parse-passwd": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
-            "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/parse5": {
             "version": "7.0.0",
             "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.0.0.tgz",
@@ -13910,15 +13342,6 @@
             "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
             "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
         },
-        "node_modules/progress": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
-            "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.4.0"
-            }
-        },
         "node_modules/prom-client": {
             "version": "13.2.0",
             "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-13.2.0.tgz",
@@ -13984,12 +13407,6 @@
                 "node": ">= 0.10"
             }
         },
-        "node_modules/proxy-from-env": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-            "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
-            "dev": true
-        },
         "node_modules/psl": {
             "version": "1.9.0",
             "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -14013,87 +13430,6 @@
                 "node": ">=6"
             }
         },
-        "node_modules/puppeteer": {
-            "version": "13.1.3",
-            "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-13.1.3.tgz",
-            "integrity": "sha512-nqcJNThLUG0Dgo++2mMtGR2FCyg7olJJhj/rm0A65muyN3nrH6lGvnNRzEaNmSnHWvjaDIG9ox5kxQB+nXTg5A==",
-            "dev": true,
-            "hasInstallScript": true,
-            "dependencies": {
-                "debug": "4.3.2",
-                "devtools-protocol": "0.0.948846",
-                "extract-zip": "2.0.1",
-                "https-proxy-agent": "5.0.0",
-                "node-fetch": "2.6.7",
-                "pkg-dir": "4.2.0",
-                "progress": "2.0.3",
-                "proxy-from-env": "1.1.0",
-                "rimraf": "3.0.2",
-                "tar-fs": "2.1.1",
-                "unbzip2-stream": "1.4.3",
-                "ws": "8.2.3"
-            },
-            "engines": {
-                "node": ">=10.18.1"
-            }
-        },
-        "node_modules/puppeteer/node_modules/debug": {
-            "version": "4.3.2",
-            "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
-            "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
-            "dev": true,
-            "dependencies": {
-                "ms": "2.1.2"
-            },
-            "engines": {
-                "node": ">=6.0"
-            },
-            "peerDependenciesMeta": {
-                "supports-color": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/puppeteer/node_modules/https-proxy-agent": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
-            "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
-            "dev": true,
-            "dependencies": {
-                "agent-base": "6",
-                "debug": "4"
-            },
-            "engines": {
-                "node": ">= 6"
-            }
-        },
-        "node_modules/puppeteer/node_modules/ms": {
-            "version": "2.1.2",
-            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-            "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-            "dev": true
-        },
-        "node_modules/puppeteer/node_modules/ws": {
-            "version": "8.2.3",
-            "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
-            "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
-            "dev": true,
-            "engines": {
-                "node": ">=10.0.0"
-            },
-            "peerDependencies": {
-                "bufferutil": "^4.0.1",
-                "utf-8-validate": "^5.0.2"
-            },
-            "peerDependenciesMeta": {
-                "bufferutil": {
-                    "optional": true
-                },
-                "utf-8-validate": {
-                    "optional": true
-                }
-            }
-        },
         "node_modules/qlobber": {
             "version": "5.0.3",
             "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-5.0.3.tgz",
@@ -14728,19 +14064,6 @@
                 "node": ">=8"
             }
         },
-        "node_modules/resolve-dir": {
-            "version": "0.1.1",
-            "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz",
-            "integrity": "sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA==",
-            "dev": true,
-            "dependencies": {
-                "expand-tilde": "^1.2.2",
-                "global-modules": "^0.2.3"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/resolve-from": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -15070,42 +14393,6 @@
             "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
             "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
         },
-        "node_modules/shallow-clone": {
-            "version": "0.1.2",
-            "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz",
-            "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==",
-            "dev": true,
-            "dependencies": {
-                "is-extendable": "^0.1.1",
-                "kind-of": "^2.0.1",
-                "lazy-cache": "^0.2.3",
-                "mixin-object": "^2.0.1"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/shallow-clone/node_modules/kind-of": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz",
-            "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==",
-            "dev": true,
-            "dependencies": {
-                "is-buffer": "^1.0.2"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/shallow-clone/node_modules/lazy-cache": {
-            "version": "0.2.7",
-            "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz",
-            "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/shebang-command": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -15359,17 +14646,6 @@
             "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==",
             "dev": true
         },
-        "node_modules/spawnd": {
-            "version": "6.0.2",
-            "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-6.0.2.tgz",
-            "integrity": "sha512-+YJtx0dvy2wt304MrHD//tASc84zinBUYU1jacPBzrjhZUd7RsDo25krxr4HUHAQzEQFuMAs4/p+yLYU5ciZ1w==",
-            "dev": true,
-            "dependencies": {
-                "exit": "^0.1.2",
-                "signal-exit": "^3.0.6",
-                "tree-kill": "^1.2.2"
-            }
-        },
         "node_modules/spdx-correct": {
             "version": "3.1.1",
             "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
@@ -15914,40 +15190,6 @@
                 "node": ">= 10"
             }
         },
-        "node_modules/tar-fs": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
-            "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
-            "dev": true,
-            "dependencies": {
-                "chownr": "^1.1.1",
-                "mkdirp-classic": "^0.5.2",
-                "pump": "^3.0.0",
-                "tar-stream": "^2.1.4"
-            }
-        },
-        "node_modules/tar-fs/node_modules/chownr": {
-            "version": "1.1.4",
-            "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
-            "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
-            "dev": true
-        },
-        "node_modules/tar-stream": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
-            "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
-            "dev": true,
-            "dependencies": {
-                "bl": "^4.0.3",
-                "end-of-stream": "^1.4.1",
-                "fs-constants": "^1.0.0",
-                "inherits": "^2.0.3",
-                "readable-stream": "^3.1.1"
-            },
-            "engines": {
-                "node": ">=6"
-            }
-        },
         "node_modules/tarn": {
             "version": "3.0.2",
             "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz",
@@ -16349,16 +15591,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/unbzip2-stream": {
-            "version": "1.4.3",
-            "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
-            "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
-            "dev": true,
-            "dependencies": {
-                "buffer": "^5.2.1",
-                "through": "^2.3.8"
-            }
-        },
         "node_modules/unicode-canonical-property-names-ecmascript": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@@ -20628,12 +19860,6 @@
             "resolved": "https://registry.npmjs.org/args-parser/-/args-parser-1.3.0.tgz",
             "integrity": "sha512-If3Zi4BSjlQIJ9fgAhSiKi0oJtgMzSqh0H4wvl7XSeO16FKx7QqaHld8lZeEajPX7y1C5qKKeNgyrfyvmjmjUQ=="
         },
-        "arr-union": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
-            "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==",
-            "dev": true
-        },
         "array-flatten": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -21462,30 +20688,6 @@
                 "wrap-ansi": "^7.0.0"
             }
         },
-        "clone-deep": {
-            "version": "0.2.4",
-            "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz",
-            "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==",
-            "dev": true,
-            "requires": {
-                "for-own": "^0.1.3",
-                "is-plain-object": "^2.0.1",
-                "kind-of": "^3.0.2",
-                "lazy-cache": "^1.0.3",
-                "shallow-clone": "^0.1.2"
-            },
-            "dependencies": {
-                "is-plain-object": {
-                    "version": "2.0.4",
-                    "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
-                    "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
-                    "dev": true,
-                    "requires": {
-                        "isobject": "^3.0.1"
-                    }
-                }
-            }
-        },
         "clone-regexp": {
             "version": "2.2.0",
             "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz",
@@ -21950,16 +21152,6 @@
             "resolved": "https://registry.npmjs.org/custom-error-instance/-/custom-error-instance-2.1.1.tgz",
             "integrity": "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg=="
         },
-        "cwd": {
-            "version": "0.10.0",
-            "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz",
-            "integrity": "sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==",
-            "dev": true,
-            "requires": {
-                "find-pkg": "^0.1.2",
-                "fs-exists-sync": "^0.1.0"
-            }
-        },
         "cypress": {
             "version": "10.7.0",
             "resolved": "https://registry.npmjs.org/cypress/-/cypress-10.7.0.tgz",
@@ -22299,12 +21491,6 @@
             "resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz",
             "integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ=="
         },
-        "devtools-protocol": {
-            "version": "0.0.948846",
-            "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.948846.tgz",
-            "integrity": "sha512-5fGyt9xmMqUl2VI7+rnUkKCiAQIpLns8sfQtTENy5L70ktbNw0Z3TFJ1JoFNYdx/jffz4YXU45VF75wKZD7sZQ==",
-            "dev": true
-        },
         "diff-sequences": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
@@ -23167,15 +22353,6 @@
             "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
             "dev": true
         },
-        "expand-tilde": {
-            "version": "1.2.2",
-            "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz",
-            "integrity": "sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q==",
-            "dev": true,
-            "requires": {
-                "os-homedir": "^1.0.1"
-            }
-        },
         "expect": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz",
@@ -23188,12 +22365,6 @@
                 "jest-message-util": "^27.5.1"
             }
         },
-        "expect-puppeteer": {
-            "version": "6.1.1",
-            "resolved": "https://registry.npmjs.org/expect-puppeteer/-/expect-puppeteer-6.1.1.tgz",
-            "integrity": "sha512-cnQF96qdoEcOD63j5NQMc0RtW9WRMW/WHKXEKsuDQ2tszhVH3qC7zkXXS4D0LTt9qCB3DEExioqylsQXvqPrUw==",
-            "dev": true
-        },
         "express": {
             "version": "4.17.3",
             "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz",
@@ -23464,87 +22635,6 @@
                 }
             }
         },
-        "find-file-up": {
-            "version": "0.1.3",
-            "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz",
-            "integrity": "sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A==",
-            "dev": true,
-            "requires": {
-                "fs-exists-sync": "^0.1.0",
-                "resolve-dir": "^0.1.0"
-            }
-        },
-        "find-pkg": {
-            "version": "0.1.2",
-            "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz",
-            "integrity": "sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw==",
-            "dev": true,
-            "requires": {
-                "find-file-up": "^0.1.2"
-            }
-        },
-        "find-process": {
-            "version": "1.4.7",
-            "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.7.tgz",
-            "integrity": "sha512-/U4CYp1214Xrp3u3Fqr9yNynUrr5Le4y0SsJh2lMDDSbpwYSz3M2SMWQC+wqcx79cN8PQtHQIL8KnuY9M66fdg==",
-            "dev": true,
-            "requires": {
-                "chalk": "^4.0.0",
-                "commander": "^5.1.0",
-                "debug": "^4.1.1"
-            },
-            "dependencies": {
-                "ansi-styles": {
-                    "version": "4.3.0",
-                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-                    "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-                    "dev": true,
-                    "requires": {
-                        "color-convert": "^2.0.1"
-                    }
-                },
-                "chalk": {
-                    "version": "4.1.2",
-                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-                    "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-                    "dev": true,
-                    "requires": {
-                        "ansi-styles": "^4.1.0",
-                        "supports-color": "^7.1.0"
-                    }
-                },
-                "color-convert": {
-                    "version": "2.0.1",
-                    "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-                    "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-                    "dev": true,
-                    "requires": {
-                        "color-name": "~1.1.4"
-                    }
-                },
-                "color-name": {
-                    "version": "1.1.4",
-                    "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-                    "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-                    "dev": true
-                },
-                "has-flag": {
-                    "version": "4.0.0",
-                    "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-                    "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-                    "dev": true
-                },
-                "supports-color": {
-                    "version": "7.2.0",
-                    "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-                    "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-                    "dev": true,
-                    "requires": {
-                        "has-flag": "^4.0.0"
-                    }
-                }
-            }
-        },
         "find-up": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -23576,21 +22666,6 @@
             "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
             "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
         },
-        "for-in": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
-            "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==",
-            "dev": true
-        },
-        "for-own": {
-            "version": "0.1.5",
-            "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
-            "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==",
-            "dev": true,
-            "requires": {
-                "for-in": "^1.0.1"
-            }
-        },
         "forever-agent": {
             "version": "0.6.1",
             "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -23653,18 +22728,6 @@
                 }
             }
         },
-        "fs-constants": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
-            "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
-            "dev": true
-        },
-        "fs-exists-sync": {
-            "version": "0.1.0",
-            "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz",
-            "integrity": "sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==",
-            "dev": true
-        },
         "fs-extra": {
             "version": "10.1.0",
             "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -23858,39 +22921,6 @@
                 }
             }
         },
-        "global-modules": {
-            "version": "0.2.3",
-            "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz",
-            "integrity": "sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA==",
-            "dev": true,
-            "requires": {
-                "global-prefix": "^0.1.4",
-                "is-windows": "^0.2.0"
-            }
-        },
-        "global-prefix": {
-            "version": "0.1.5",
-            "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz",
-            "integrity": "sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==",
-            "dev": true,
-            "requires": {
-                "homedir-polyfill": "^1.0.0",
-                "ini": "^1.3.4",
-                "is-windows": "^0.2.0",
-                "which": "^1.2.12"
-            },
-            "dependencies": {
-                "which": {
-                    "version": "1.3.1",
-                    "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
-                    "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-                    "dev": true,
-                    "requires": {
-                        "isexe": "^2.0.0"
-                    }
-                }
-            }
-        },
         "globals": {
             "version": "11.12.0",
             "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -24017,15 +23047,6 @@
             "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz",
             "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ=="
         },
-        "homedir-polyfill": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
-            "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
-            "dev": true,
-            "requires": {
-                "parse-passwd": "^1.0.0"
-            }
-        },
         "hosted-git-info": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -24275,12 +23296,6 @@
                 "has-tostringtag": "^1.0.0"
             }
         },
-        "is-buffer": {
-            "version": "1.1.6",
-            "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
-            "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
-            "dev": true
-        },
         "is-callable": {
             "version": "1.2.4",
             "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
@@ -24316,12 +23331,6 @@
             "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
             "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="
         },
-        "is-extendable": {
-            "version": "0.1.1",
-            "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
-            "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
-            "dev": true
-        },
         "is-extglob": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -24510,12 +23519,6 @@
                 "call-bind": "^1.0.2"
             }
         },
-        "is-windows": {
-            "version": "0.2.0",
-            "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz",
-            "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==",
-            "dev": true
-        },
         "is-wsl": {
             "version": "2.2.0",
             "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -24544,12 +23547,6 @@
             "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
             "devOptional": true
         },
-        "isobject": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-            "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
-            "dev": true
-        },
         "isstream": {
             "version": "0.1.2",
             "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@@ -24893,72 +23890,6 @@
                 }
             }
         },
-        "jest-dev-server": {
-            "version": "6.1.1",
-            "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-6.1.1.tgz",
-            "integrity": "sha512-z5LnaGDvlIkdMv/rppSO4+rq+GyQKf1xI9oiBxf9/2EBeN2hxRaWiMvaLNDnHPZj2PAhBXsycrKslDDoZO2Xtw==",
-            "dev": true,
-            "requires": {
-                "chalk": "^4.1.2",
-                "cwd": "^0.10.0",
-                "find-process": "^1.4.7",
-                "prompts": "^2.4.2",
-                "spawnd": "^6.0.2",
-                "tree-kill": "^1.2.2",
-                "wait-on": "^6.0.1"
-            },
-            "dependencies": {
-                "ansi-styles": {
-                    "version": "4.3.0",
-                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-                    "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-                    "dev": true,
-                    "requires": {
-                        "color-convert": "^2.0.1"
-                    }
-                },
-                "chalk": {
-                    "version": "4.1.2",
-                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-                    "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-                    "dev": true,
-                    "requires": {
-                        "ansi-styles": "^4.1.0",
-                        "supports-color": "^7.1.0"
-                    }
-                },
-                "color-convert": {
-                    "version": "2.0.1",
-                    "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-                    "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-                    "dev": true,
-                    "requires": {
-                        "color-name": "~1.1.4"
-                    }
-                },
-                "color-name": {
-                    "version": "1.1.4",
-                    "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-                    "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-                    "dev": true
-                },
-                "has-flag": {
-                    "version": "4.0.0",
-                    "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-                    "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-                    "dev": true
-                },
-                "supports-color": {
-                    "version": "7.2.0",
-                    "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-                    "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-                    "dev": true,
-                    "requires": {
-                        "has-flag": "^4.0.0"
-                    }
-                }
-            }
-        },
         "jest-diff": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",
@@ -25124,70 +24055,6 @@
                 "jest-util": "^27.5.1"
             }
         },
-        "jest-environment-puppeteer": {
-            "version": "6.1.1",
-            "resolved": "https://registry.npmjs.org/jest-environment-puppeteer/-/jest-environment-puppeteer-6.1.1.tgz",
-            "integrity": "sha512-Ces37g8Gdj7QaVxszeoXlvmsZxcEJN9EPUdJt8fGMLA+6ARVFKyVmFgP9xVeGyjTvzsXdtIiJdeOKMLMeD8r2A==",
-            "dev": true,
-            "requires": {
-                "chalk": "^4.1.2",
-                "cwd": "^0.10.0",
-                "jest-dev-server": "^6.1.1",
-                "jest-environment-node": "^27.4.4",
-                "merge-deep": "^3.0.3"
-            },
-            "dependencies": {
-                "ansi-styles": {
-                    "version": "4.3.0",
-                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-                    "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-                    "dev": true,
-                    "requires": {
-                        "color-convert": "^2.0.1"
-                    }
-                },
-                "chalk": {
-                    "version": "4.1.2",
-                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-                    "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-                    "dev": true,
-                    "requires": {
-                        "ansi-styles": "^4.1.0",
-                        "supports-color": "^7.1.0"
-                    }
-                },
-                "color-convert": {
-                    "version": "2.0.1",
-                    "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-                    "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-                    "dev": true,
-                    "requires": {
-                        "color-name": "~1.1.4"
-                    }
-                },
-                "color-name": {
-                    "version": "1.1.4",
-                    "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-                    "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-                    "dev": true
-                },
-                "has-flag": {
-                    "version": "4.0.0",
-                    "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-                    "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-                    "dev": true
-                },
-                "supports-color": {
-                    "version": "7.2.0",
-                    "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-                    "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-                    "dev": true,
-                    "requires": {
-                        "has-flag": "^4.0.0"
-                    }
-                }
-            }
-        },
         "jest-get-type": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz",
@@ -25448,16 +24315,6 @@
             "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
             "dev": true
         },
-        "jest-puppeteer": {
-            "version": "6.0.3",
-            "resolved": "https://registry.npmjs.org/jest-puppeteer/-/jest-puppeteer-6.0.3.tgz",
-            "integrity": "sha512-6GRdbkWwNu8dfzo4icpwc50+K5ECYpWyD9sxpRa03PA8Hi3byl0dcAx+NjCivSezWjAl2Iwwhujqb+bczei0Bg==",
-            "dev": true,
-            "requires": {
-                "expect-puppeteer": "^6.0.2",
-                "jest-environment-puppeteer": "^6.0.3"
-            }
-        },
         "jest-regex-util": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz",
@@ -26277,15 +25134,6 @@
             "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
             "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
         },
-        "kind-of": {
-            "version": "3.2.2",
-            "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-            "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-            "dev": true,
-            "requires": {
-                "is-buffer": "^1.1.5"
-            }
-        },
         "kleur": {
             "version": "3.0.3",
             "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -26349,12 +25197,6 @@
             "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==",
             "dev": true
         },
-        "lazy-cache": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
-            "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==",
-            "dev": true
-        },
         "leven": {
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -26722,17 +25564,6 @@
             "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==",
             "dev": true
         },
-        "merge-deep": {
-            "version": "3.0.3",
-            "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz",
-            "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==",
-            "dev": true,
-            "requires": {
-                "arr-union": "^3.1.0",
-                "clone-deep": "^0.2.4",
-                "kind-of": "^3.0.2"
-            }
-        },
         "merge-descriptors": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@@ -26844,35 +25675,11 @@
                 "yallist": "^4.0.0"
             }
         },
-        "mixin-object": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz",
-            "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==",
-            "dev": true,
-            "requires": {
-                "for-in": "^0.1.3",
-                "is-extendable": "^0.1.1"
-            },
-            "dependencies": {
-                "for-in": {
-                    "version": "0.1.8",
-                    "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz",
-                    "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==",
-                    "dev": true
-                }
-            }
-        },
         "mkdirp": {
             "version": "1.0.4",
             "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
             "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
         },
-        "mkdirp-classic": {
-            "version": "0.5.3",
-            "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
-            "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
-            "dev": true
-        },
         "mqemitter": {
             "version": "4.5.0",
             "resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-4.5.0.tgz",
@@ -27381,12 +26188,6 @@
                 "word-wrap": "^1.2.3"
             }
         },
-        "os-homedir": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
-            "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
-            "dev": true
-        },
         "ospath": {
             "version": "1.2.2",
             "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
@@ -27473,12 +26274,6 @@
                 "lines-and-columns": "^1.1.6"
             }
         },
-        "parse-passwd": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
-            "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
-            "dev": true
-        },
         "parse5": {
             "version": "7.0.0",
             "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.0.0.tgz",
@@ -27811,12 +26606,6 @@
             "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
             "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
         },
-        "progress": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
-            "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
-            "dev": true
-        },
         "prom-client": {
             "version": "13.2.0",
             "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-13.2.0.tgz",
@@ -27872,12 +26661,6 @@
                 "ipaddr.js": "1.9.1"
             }
         },
-        "proxy-from-env": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-            "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
-            "dev": true
-        },
         "psl": {
             "version": "1.9.0",
             "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -27898,59 +26681,6 @@
             "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
             "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
         },
-        "puppeteer": {
-            "version": "13.1.3",
-            "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-13.1.3.tgz",
-            "integrity": "sha512-nqcJNThLUG0Dgo++2mMtGR2FCyg7olJJhj/rm0A65muyN3nrH6lGvnNRzEaNmSnHWvjaDIG9ox5kxQB+nXTg5A==",
-            "dev": true,
-            "requires": {
-                "debug": "4.3.2",
-                "devtools-protocol": "0.0.948846",
-                "extract-zip": "2.0.1",
-                "https-proxy-agent": "5.0.0",
-                "node-fetch": "2.6.7",
-                "pkg-dir": "4.2.0",
-                "progress": "2.0.3",
-                "proxy-from-env": "1.1.0",
-                "rimraf": "3.0.2",
-                "tar-fs": "2.1.1",
-                "unbzip2-stream": "1.4.3",
-                "ws": "8.2.3"
-            },
-            "dependencies": {
-                "debug": {
-                    "version": "4.3.2",
-                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
-                    "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
-                    "dev": true,
-                    "requires": {
-                        "ms": "2.1.2"
-                    }
-                },
-                "https-proxy-agent": {
-                    "version": "5.0.0",
-                    "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
-                    "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
-                    "dev": true,
-                    "requires": {
-                        "agent-base": "6",
-                        "debug": "4"
-                    }
-                },
-                "ms": {
-                    "version": "2.1.2",
-                    "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-                    "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-                    "dev": true
-                },
-                "ws": {
-                    "version": "8.2.3",
-                    "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
-                    "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
-                    "dev": true
-                }
-            }
-        },
         "qlobber": {
             "version": "5.0.3",
             "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-5.0.3.tgz",
@@ -28447,16 +27177,6 @@
                 }
             }
         },
-        "resolve-dir": {
-            "version": "0.1.1",
-            "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz",
-            "integrity": "sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA==",
-            "dev": true,
-            "requires": {
-                "expand-tilde": "^1.2.2",
-                "global-modules": "^0.2.3"
-            }
-        },
         "resolve-from": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -28698,35 +27418,6 @@
             "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
             "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
         },
-        "shallow-clone": {
-            "version": "0.1.2",
-            "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz",
-            "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==",
-            "dev": true,
-            "requires": {
-                "is-extendable": "^0.1.1",
-                "kind-of": "^2.0.1",
-                "lazy-cache": "^0.2.3",
-                "mixin-object": "^2.0.1"
-            },
-            "dependencies": {
-                "kind-of": {
-                    "version": "2.0.1",
-                    "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz",
-                    "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==",
-                    "dev": true,
-                    "requires": {
-                        "is-buffer": "^1.0.2"
-                    }
-                },
-                "lazy-cache": {
-                    "version": "0.2.7",
-                    "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz",
-                    "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==",
-                    "dev": true
-                }
-            }
-        },
         "shebang-command": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -28928,17 +27619,6 @@
             "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==",
             "dev": true
         },
-        "spawnd": {
-            "version": "6.0.2",
-            "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-6.0.2.tgz",
-            "integrity": "sha512-+YJtx0dvy2wt304MrHD//tASc84zinBUYU1jacPBzrjhZUd7RsDo25krxr4HUHAQzEQFuMAs4/p+yLYU5ciZ1w==",
-            "dev": true,
-            "requires": {
-                "exit": "^0.1.2",
-                "signal-exit": "^3.0.6",
-                "tree-kill": "^1.2.2"
-            }
-        },
         "spdx-correct": {
             "version": "3.1.1",
             "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
@@ -29363,39 +28043,6 @@
                 "yallist": "^4.0.0"
             }
         },
-        "tar-fs": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
-            "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
-            "dev": true,
-            "requires": {
-                "chownr": "^1.1.1",
-                "mkdirp-classic": "^0.5.2",
-                "pump": "^3.0.0",
-                "tar-stream": "^2.1.4"
-            },
-            "dependencies": {
-                "chownr": {
-                    "version": "1.1.4",
-                    "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
-                    "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
-                    "dev": true
-                }
-            }
-        },
-        "tar-stream": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
-            "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
-            "dev": true,
-            "requires": {
-                "bl": "^4.0.3",
-                "end-of-stream": "^1.4.1",
-                "fs-constants": "^1.0.0",
-                "inherits": "^2.0.3",
-                "readable-stream": "^3.1.1"
-            }
-        },
         "tarn": {
             "version": "3.0.2",
             "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz",
@@ -29704,16 +28351,6 @@
                 "which-boxed-primitive": "^1.0.2"
             }
         },
-        "unbzip2-stream": {
-            "version": "1.4.3",
-            "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
-            "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
-            "dev": true,
-            "requires": {
-                "buffer": "^5.2.1",
-                "through": "^2.3.8"
-            }
-        },
         "unicode-canonical-property-names-ecmascript": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
diff --git a/package.json b/package.json
index 479876a6..4f7da681 100644
--- a/package.json
+++ b/package.json
@@ -25,8 +25,6 @@
         "build": "vite build --config ./config/vite.config.js",
         "test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
         "test-with-build": "npm run build && npm test",
-        "jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend",
-        "jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
         "jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
         "tsc": "tsc",
         "vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
@@ -140,12 +138,10 @@
         "eslint-plugin-vue": "~8.7.1",
         "favico.js": "^0.3.10",
         "jest": "~27.2.5",
-        "jest-puppeteer": "~6.0.3",
         "postcss-html": "~1.5.0",
         "postcss-rtlcss": "~3.7.2",
         "postcss-scss": "~4.0.4",
         "prismjs": "^1.27.0",
-        "puppeteer": "~13.1.3",
         "qrcode": "~1.5.0",
         "rollup-plugin-visualizer": "^5.6.0",
         "sass": "~1.42.1",
diff --git a/server/util-server.js b/server/util-server.js
index 1517bcfe..cf303ba8 100644
--- a/server/util-server.js
+++ b/server/util-server.js
@@ -557,7 +557,7 @@ exports.doubleCheckPassword = async (socket, currentPassword) => {
 exports.startUnitTest = async () => {
     console.log("Starting unit test...");
     const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
-    const child = childProcess.spawn(npm, [ "run", "jest" ]);
+    const child = childProcess.spawn(npm, [ "run", "jest-backend" ]);
 
     child.stdout.on("data", (data) => {
         console.log(data.toString());
diff --git a/test/e2e.spec.js b/test/e2e.spec.js
deleted file mode 100644
index 8de8ceb1..00000000
--- a/test/e2e.spec.js
+++ /dev/null
@@ -1,329 +0,0 @@
-// eslint-disable-next-line no-unused-vars
-const { Page, Browser } = require("puppeteer");
-const { sleep } = require("../src/util");
-
-/**
- * Set back the correct data type for page object
- * @type {Page}
- */
-page;
-
-/**
- * @type {Browser}
- */
-browser;
-
-beforeAll(async () => {
-    await page.setViewport({
-        width: 1280,
-        height: 720,
-        deviceScaleFactor: 1,
-    });
-});
-
-afterAll(() => {
-
-});
-
-const baseURL = "http://127.0.0.1:3002";
-
-describe("Init", () => {
-    const title = "Uptime Kuma";
-
-    beforeAll(async () => {
-        await page.goto(baseURL);
-    });
-
-    it(`should be titled "${title}"`, async () => {
-        await expect(page.title()).resolves.toEqual(title);
-    });
-
-    // Setup Page
-    it("Setup", async () => {
-        // Create an Admin
-        await page.waitForSelector("#floatingInput");
-        await page.waitForSelector("#repeat");
-        await page.click("#floatingInput");
-        await page.type("#floatingInput", "admin");
-        await page.type("#floatingPassword", "admin123");
-        await page.type("#repeat", "admin123");
-        await page.click(".btn-primary[type=submit]");
-        await sleep(3000);
-
-        // Go to /setup again
-        await page.goto(baseURL + "/setup");
-        await sleep(3000);
-        let pathname = await page.evaluate(() => location.pathname);
-        expect(pathname).toEqual("/dashboard");
-
-        // Go to /
-        await page.goto(baseURL);
-        await page.waitForSelector("h1.mb-3");
-        pathname = await page.evaluate(() => location.pathname);
-        expect(pathname).toEqual("/dashboard");
-    });
-
-    it("should create monitor", async () => {
-        // Create monitor
-        await page.goto(baseURL + "/add");
-        await page.waitForSelector("#name");
-
-        await page.type("#name", "Myself");
-        await page.waitForSelector("#url");
-        await page.click("#url", { clickCount: 3 });
-        await page.keyboard.type(baseURL);
-        await page.keyboard.press("Enter");
-
-        await page.waitForFunction(() => {
-            const badge = document.querySelector("span.badge");
-            return badge && badge.innerText == "100%";
-        }, { timeout: 5000 });
-
-    });
-
-    // Settings Page
-    /*
-    describe("Settings", () => {
-        beforeEach(async () => {
-            await page.goto(baseURL + "/settings");
-        });
-
-        it("Change Language", async () => {
-            await page.goto(baseURL + "/settings/appearance");
-            await page.waitForSelector("#language");
-
-            await page.select("#language", "zh-HK");
-            let languageTitle = await page.evaluate(() => document.querySelector("[for=language]").innerText);
-            expect(languageTitle).toEqual("語言");
-
-            await page.select("#language", "en");
-            languageTitle = await page.evaluate(() => document.querySelector("[for=language]").innerText);
-            expect(languageTitle).toEqual("Language");
-        });
-
-        it("Change Theme", async () => {
-            await page.goto(baseURL + "/settings/appearance");
-
-            // Dark
-            await click(page, ".btn[for=btncheck2]");
-            await page.waitForSelector("div.dark");
-
-            await page.waitForSelector(".btn[for=btncheck1]");
-
-            // Light
-            await click(page, ".btn[for=btncheck1]");
-            await page.waitForSelector("div.light");
-        });
-
-        it("Change Heartbeat Bar Style", async () => {
-            await page.goto(baseURL + "/settings/appearance");
-
-            // Bottom
-            await click(page, ".btn[for=btncheck5]");
-            await page.waitForSelector("div.hp-bar-big");
-
-            // None
-            await click(page, ".btn[for=btncheck6]");
-            await page.waitForSelector("div.hp-bar-big", {
-                hidden: true,
-                timeout: 1000
-            });
-        });
-
-        // TODO: Timezone
-
-        it("Search Engine Visibility", async () => {
-            // Default
-            let res = await axios.get(baseURL + "/robots.txt");
-            expect(res.data).toContain("Disallow: /");
-
-            // Yes
-            await click(page, "#searchEngineIndexYes");
-            await click(page, "form > div > .btn[type=submit]");
-            await sleep(1000);
-            res = await axios.get(baseURL + "/robots.txt");
-            expect(res.data).not.toContain("Disallow: /");
-
-            // No
-            await click(page, "#searchEngineIndexNo");
-            await click(page, "form > div > .btn[type=submit]");
-            await sleep(1000);
-            res = await axios.get(baseURL + "/robots.txt");
-            expect(res.data).toContain("Disallow: /");
-        });
-
-        it("Entry Page", async () => {
-            const newPage = await browser.newPage();
-
-            // Default
-            await newPage.goto(baseURL);
-            await newPage.waitForSelector("h1.mb-3", { timeout: 3000 });
-            let pathname = await newPage.evaluate(() => location.pathname);
-            expect(pathname).toEqual("/dashboard");
-
-            // Status Page
-            await click(page, "#entryPageNo");
-            await click(page, "form > div > .btn[type=submit]");
-            await sleep(1000);
-            await newPage.goto(baseURL);
-            await newPage.waitForSelector("img.logo", { timeout: 3000 });
-            pathname = await newPage.evaluate(() => location.pathname);
-            expect(pathname).toEqual("/status");
-
-            // Back to Dashboard
-            await click(page, "#entryPageYes");
-            await click(page, "form > div > .btn[type=submit]");
-            await sleep(1000);
-            await newPage.goto(baseURL);
-            await newPage.waitForSelector("h1.mb-3", { timeout: 3000 });
-            pathname = await newPage.evaluate(() => location.pathname);
-            expect(pathname).toEqual("/dashboard");
-
-            await newPage.close();
-        });
-
-        it("Change Password (wrong current password)", async () => {
-            await page.goto(baseURL + "/settings/security");
-            await page.waitForSelector("#current-password");
-
-            await page.type("#current-password", "wrong_passw$$d");
-            await page.type("#new-password", "new_password123");
-            await page.type("#repeat-new-password", "new_password123");
-
-            // Save
-            await click(page, "form > div > .btn[type=submit]", 0);
-            await sleep(1000);
-
-            await click(page, "#logout-btn");
-            await login("admin", "new_password123");
-            let elementCount = await page.evaluate(() => document.querySelectorAll("#floatingPassword").length);
-            expect(elementCount).toEqual(1);
-
-            await login("admin", "admin123");
-        });
-
-        it("Change Password (wrong repeat)", async () => {
-            await page.goto(baseURL + "/settings/security");
-            await page.waitForSelector("#current-password");
-
-            await page.type("#current-password", "admin123");
-            await page.type("#new-password", "new_password123");
-            await page.type("#repeat-new-password", "new_password1234567898797898");
-
-            await click(page, "form > div > .btn[type=submit]", 0);
-            await sleep(1000);
-
-            await click(page, "#logout-btn");
-            await login("admin", "new_password123");
-
-            let elementCount = await page.evaluate(() => document.querySelectorAll("#floatingPassword").length);
-            expect(elementCount).toEqual(1);
-
-            await login("admin", "admin123");
-            await page.waitForSelector("#current-password");
-            let pathname = await page.evaluate(() => location.pathname);
-            expect(pathname).toEqual("/settings/security");
-        });
-
-        // TODO: 2FA
-
-        // TODO: Export Backup
-
-        // TODO: Import Backup
-
-        it("Should disable & enable auth", async () => {
-            await page.goto(baseURL + "/settings/security");
-            await click(page, "#disableAuth-btn");
-            await click(page, ".btn.btn-danger[data-bs-dismiss='modal']", 2); // Not a good way to do it
-            await page.waitForSelector("#enableAuth-btn", { timeout: 3000 });
-            await page.waitForSelector("#logout-btn", {
-                hidden: true,
-                timeout: 3000
-            });
-
-            const newPage = await browser.newPage();
-            await newPage.goto(baseURL);
-            await newPage.waitForSelector("span.badge", { timeout: 3000 });
-            newPage.close();
-
-            await click(page, "#enableAuth-btn");
-            await login("admin", "admin123");
-            await page.waitForSelector("#disableAuth-btn", { timeout: 3000 });
-        });
-
-        // it("Should clear all statistics", async () => {
-        //     await page.goto(baseURL + "/settings/monitor-history");
-        //     await click(page, "#clearAllStats-btn");
-        //     await click(page, ".btn.btn-danger");
-        //     await page.waitForFunction(() => {
-        //         const badge = document.querySelector("span.badge");
-        //         return badge && badge.innerText == "0%";
-        //     }, { timeout: 3000 });
-        // });
-    });
-     */
-
-    /*
-     * TODO
-     * Create Monitor - All type
-     * Edit Monitor
-     * Delete Monitor
-     *
-     * Create Notification (token problem, maybe hard to test)
-     *
-     */
-
-    describe("Status Page", () => {
-        const title = "Uptime Kuma";
-        beforeAll(async () => {
-            await page.goto(baseURL + "/status");
-        });
-        it(`should be titled "${title}"`, async () => {
-            await expect(page.title()).resolves.toEqual(title);
-        });
-    });
-});
-
-/**
- * Test login
- * @param {string} username
- * @param {string} password
- */
-async function login(username, password) {
-    await input(page, "#floatingInput", username);
-    await input(page, "#floatingPassword", password);
-    await page.click(".btn-primary[type=submit]");
-    await sleep(5000);
-}
-
-/**
- * Click on an element on the page
- * @param {Page} page Puppeteer page instance
- * @param {string} selector
- * @param {number} elementIndex
- * @returns {Promise<any>}
- */
-async function click(page, selector, elementIndex = 0) {
-    await page.waitForSelector(selector, {
-        timeout: 5000,
-    });
-    return await page.evaluate((s, i) => {
-        return document.querySelectorAll(s)[i].click();
-    }, selector, elementIndex);
-}
-
-/**
- * Input text into selected field
- * @param {Page} page Puppeteer page instance
- * @param {string} selector
- * @param {string} text Text to input
- */
-async function input(page, selector, text) {
-    await page.waitForSelector(selector, {
-        timeout: 5000,
-    });
-    const element = await page.$(selector);
-    await element.click({ clickCount: 3 });
-    await page.keyboard.press("Backspace");
-    await page.type(selector, text);
-}
diff --git a/test/frontend.spec.js b/test/frontend.spec.js
deleted file mode 100644
index 63121a6b..00000000
--- a/test/frontend.spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-// eslint-disable-next-line no-global-assign
-global.localStorage = {};
-global.navigator = {
-    language: "en"
-};
-
-const { currentLocale } = require("../src/i18n");
-
-describe("Test i18n.js", () => {
-
-    it("currentLocale()", () => {
-        expect(currentLocale()).toEqual("en");
-
-        navigator.language = "zh-HK";
-        expect(currentLocale()).toEqual("zh-HK");
-
-        // Note that in Safari on iOS prior to 10.2, the country code returned is lowercase: "en-us", "fr-fr" etc.
-        // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language
-        navigator.language = "zh-hk";
-        expect(currentLocale()).toEqual("en");
-
-        navigator.language = "en-US";
-        expect(currentLocale()).toEqual("en");
-
-        navigator.language = "ja-ZZ";
-        expect(currentLocale()).toEqual("ja");
-
-        navigator.language = "zz";
-        expect(currentLocale()).toEqual("en");
-
-        navigator.language = "zz-ZZ";
-        expect(currentLocale()).toEqual("en");
-
-        localStorage.locale = "en";
-        expect(currentLocale()).toEqual("en");
-
-        localStorage.locale = "zh-HK";
-        expect(currentLocale()).toEqual("zh-HK");
-    });
-
-});
-
diff --git a/test/prepare-jest.js b/test/prepare-jest.js
deleted file mode 100644
index 3fd89d10..00000000
--- a/test/prepare-jest.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const fs = require("fs");
-const rmSync = require("../extra/fs-rmSync.js");
-
-const path = "./data/test-chrome-profile";
-
-if (fs.existsSync(path)) {
-    rmSync(path, {
-        recursive: true,
-    });
-}

From 528a615fb2b330c05efc86a41cea678bc5b1dff3 Mon Sep 17 00:00:00 2001
From: CL0Pinette <51855790+CL0Pinette@users.noreply.github.com>
Date: Wed, 5 Oct 2022 11:30:49 +0200
Subject: [PATCH 086/134] Add free.fr SMS notification provider (#2159)

---
 server/notification-providers/freemobile.js | 24 +++++++++++++++++++++
 server/notification.js                      |  2 ++
 src/components/notifications/FreeMobile.vue | 12 +++++++++++
 src/components/notifications/index.js       |  2 ++
 4 files changed, 40 insertions(+)
 create mode 100644 server/notification-providers/freemobile.js
 create mode 100644 src/components/notifications/FreeMobile.vue

diff --git a/server/notification-providers/freemobile.js b/server/notification-providers/freemobile.js
new file mode 100644
index 00000000..919150fa
--- /dev/null
+++ b/server/notification-providers/freemobile.js
@@ -0,0 +1,24 @@
+const NotificationProvider = require("./notification-provider");
+const axios = require("axios");
+
+class FreeMobile extends NotificationProvider {
+
+    name = "FreeMobile";
+
+    async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
+        let okMsg = "Sent Successfully.";
+        try {
+            await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, {
+                "user": notification.freemobileUser,
+                "pass": notification.freemobilePass,
+            });
+
+            return okMsg;
+
+        } catch (error) {
+            this.throwGeneralAxiosError(error);
+        }
+    }
+}
+
+module.exports = FreeMobile;
diff --git a/server/notification.js b/server/notification.js
index 7a4b4f29..aed92e5d 100644
--- a/server/notification.js
+++ b/server/notification.js
@@ -9,6 +9,7 @@ const ClickSendSMS = require("./notification-providers/clicksendsms");
 const DingDing = require("./notification-providers/dingding");
 const Discord = require("./notification-providers/discord");
 const Feishu = require("./notification-providers/feishu");
+const FreeMobile = require("./notification-providers/freemobile");
 const GoogleChat = require("./notification-providers/google-chat");
 const Gorush = require("./notification-providers/gorush");
 const Gotify = require("./notification-providers/gotify");
@@ -63,6 +64,7 @@ class Notification {
             new DingDing(),
             new Discord(),
             new Feishu(),
+            new FreeMobile(),
             new GoogleChat(),
             new Gorush(),
             new Gotify(),
diff --git a/src/components/notifications/FreeMobile.vue b/src/components/notifications/FreeMobile.vue
new file mode 100644
index 00000000..852d9ae2
--- /dev/null
+++ b/src/components/notifications/FreeMobile.vue
@@ -0,0 +1,12 @@
+<template>
+    <div class="mb-3">
+        <label for="freemobileUser" class="form-label">{{ $t("Free Mobile User Identifier") }}<span style="color: red;"><sup>*</sup></span></label>
+        <input id="freemobileUser" v-model="$parent.notification.freemobileUser" type="text" class="form-control" required>
+    </div>
+
+    <div class="mb-3">
+        <label for="freemobilePass" class="form-label">{{ $t("Free Mobile API Key") }}<span style="color: red;"><sup>*</sup></span></label>
+        <input id="freemobilePass" v-model="$parent.notification.freemobilePass" type="text" class="form-control" required>
+    </div>
+</template>
+
diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js
index 319a7922..bca4a510 100644
--- a/src/components/notifications/index.js
+++ b/src/components/notifications/index.js
@@ -7,6 +7,7 @@ import ClickSendSMS from "./ClickSendSMS.vue";
 import DingDing from "./DingDing.vue";
 import Discord from "./Discord.vue";
 import Feishu from "./Feishu.vue";
+import FreeMobile from "./FreeMobile.vue";
 import GoogleChat from "./GoogleChat.vue";
 import Gorush from "./Gorush.vue";
 import Gotify from "./Gotify.vue";
@@ -56,6 +57,7 @@ const NotificationFormList = {
     "DingDing": DingDing,
     "discord": Discord,
     "Feishu": Feishu,
+    "FreeMobile": FreeMobile,
     "GoogleChat": GoogleChat,
     "gorush": Gorush,
     "gotify": Gotify,

From c28d8ddff9342da501e60df23cbb79e3259a4306 Mon Sep 17 00:00:00 2001
From: Ben Scobie <benscobie@users.noreply.github.com>
Date: Wed, 5 Oct 2022 16:45:21 +0100
Subject: [PATCH 087/134] Correctly handle multiple IPs in X-Forwarded-For
 (#2177)

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
---
 package.json                 |  4 +-
 server/uptime-kuma-server.js |  4 +-
 test/backend.spec.js         | 83 +++++++++++++++++++++++++++++++++++-
 3 files changed, 87 insertions(+), 4 deletions(-)

diff --git a/package.json b/package.json
index 4f7da681..1411168d 100644
--- a/package.json
+++ b/package.json
@@ -23,9 +23,9 @@
         "start-server": "node server/server.js",
         "start-server-dev": "cross-env NODE_ENV=development node server/server.js",
         "build": "vite build --config ./config/vite.config.js",
-        "test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
+        "test": "node test/prepare-test-server.js && npm run jest-backend",
         "test-with-build": "npm run build && npm test",
-        "jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
+        "jest-backend": "cross-env TEST_BACKEND=1 jest --runInBand --detectOpenHandles --forceExit --config=./config/jest-backend.config.js",
         "tsc": "tsc",
         "vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
         "build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index 98de65a4..6e77e1fd 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -138,7 +138,9 @@ class UptimeKumaServer {
         }
 
         if (await Settings.get("trustProxy")) {
-            return socket.client.conn.request.headers["x-forwarded-for"]
+            const forwardedFor = socket.client.conn.request.headers["x-forwarded-for"];
+
+            return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
                 || socket.client.conn.request.headers["x-real-ip"]
                 || clientIP.replace(/^.*:/, "");
         } else {
diff --git a/test/backend.spec.js b/test/backend.spec.js
index 6deb2853..5b9fa92c 100644
--- a/test/backend.spec.js
+++ b/test/backend.spec.js
@@ -1,7 +1,11 @@
-const { genSecret, DOWN } = require("../src/util");
+const { genSecret, DOWN, log} = require("../src/util");
 const utilServerRewire = require("../server/util-server");
 const Discord = require("../server/notification-providers/discord");
 const axios = require("axios");
+const { UptimeKumaServer } = require("../server/uptime-kuma-server");
+const Database = require("../server/database");
+const {Settings} = require("../server/settings");
+const fs = require("fs");
 
 jest.mock("axios");
 
@@ -225,3 +229,80 @@ describe("The function filterAndJoin", () => {
         expect(result).toBe("");
     });
 });
+
+describe("Test uptimeKumaServer.getClientIP()", () => {
+    it("should able to get a correct client IP", async () => {
+        Database.init({
+            "data-dir": "./data/test"
+        });
+
+        if (! fs.existsSync(Database.path)) {
+            log.info("server", "Copying Database");
+            fs.copyFileSync(Database.templatePath, Database.path);
+        }
+
+        await Database.connect(true);
+        await Database.patch();
+
+        const fakeSocket = {
+            client: {
+                conn: {
+                    remoteAddress: "192.168.10.10",
+                    request: {
+                        headers: {
+                        }
+                    }
+                }
+            }
+        }
+        const server = Object.create(UptimeKumaServer.prototype);
+        let ip = await server.getClientIP(fakeSocket);
+
+        await Settings.set("trustProxy", false);
+        expect(await Settings.get("trustProxy")).toBe(false);
+        expect(ip).toBe("192.168.10.10");
+
+        fakeSocket.client.conn.request.headers["x-forwarded-for"] = "10.10.10.10";
+        ip = await server.getClientIP(fakeSocket);
+        expect(ip).toBe("192.168.10.10");
+
+        fakeSocket.client.conn.request.headers["x-real-ip"] = "20.20.20.20";
+        ip = await server.getClientIP(fakeSocket);
+        expect(ip).toBe("192.168.10.10");
+
+        await Settings.set("trustProxy", true);
+        expect(await Settings.get("trustProxy")).toBe(true);
+
+        fakeSocket.client.conn.request.headers["x-forwarded-for"] = "10.10.10.10";
+        ip = await server.getClientIP(fakeSocket);
+        expect(ip).toBe("10.10.10.10");
+
+        // x-real-ip
+        delete fakeSocket.client.conn.request.headers["x-forwarded-for"];
+        ip = await server.getClientIP(fakeSocket);
+        expect(ip).toBe("20.20.20.20");
+
+        fakeSocket.client.conn.request.headers["x-forwarded-for"] = "2001:db8:85a3:8d3:1319:8a2e:370:7348";
+        ip = await server.getClientIP(fakeSocket);
+        expect(ip).toBe("2001:db8:85a3:8d3:1319:8a2e:370:7348");
+
+        fakeSocket.client.conn.request.headers["x-forwarded-for"] = "203.0.113.195";
+        ip = await server.getClientIP(fakeSocket);
+        expect(ip).toBe("203.0.113.195");
+
+        fakeSocket.client.conn.request.headers["x-forwarded-for"] = "203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348";
+        ip = await server.getClientIP(fakeSocket);
+        expect(ip).toBe("203.0.113.195");
+
+        fakeSocket.client.conn.request.headers["x-forwarded-for"] = "203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178";
+        ip = await server.getClientIP(fakeSocket);
+        expect(ip).toBe("203.0.113.195");
+
+        // Elements are comma-separated, with optional whitespace surrounding the commas.
+        fakeSocket.client.conn.request.headers["x-forwarded-for"] = "203.0.113.195 , 2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178";
+        ip = await server.getClientIP(fakeSocket);
+        expect(ip).toBe("203.0.113.195");
+
+        await Database.close();
+    }, 120000);
+});

From c24b64921d08b6d5c0cfc060fe35e43ecdbce20b Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Thu, 6 Oct 2022 23:28:06 +0800
Subject: [PATCH 088/134] Fix #2183 ntfy issue

---
 server/notification-providers/ntfy.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/notification-providers/ntfy.js b/server/notification-providers/ntfy.js
index 17d6d812..5381da46 100644
--- a/server/notification-providers/ntfy.js
+++ b/server/notification-providers/ntfy.js
@@ -9,7 +9,7 @@ class Ntfy extends NotificationProvider {
         let okMsg = "Sent Successfully.";
         try {
             let headers = {};
-            if (notification.ntfyusername.length > 0) {
+            if (notification.ntfyusername) {
                 headers = {
                     "Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"),
                 };

From 60460442f869f2cc581690bd7c8930b3eb7777b8 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Fri, 7 Oct 2022 00:25:34 +0800
Subject: [PATCH 089/134] Update to 1.18.3

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 1411168d..df0bdbfb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "uptime-kuma",
-    "version": "1.18.2",
+    "version": "1.18.3",
     "license": "MIT",
     "repository": {
         "type": "git",
@@ -38,7 +38,7 @@
         "build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
         "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
         "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
-        "setup": "git checkout 1.18.2 && npm ci --production && npm run download-dist",
+        "setup": "git checkout 1.18.3 && npm ci --production && npm run download-dist",
         "download-dist": "node extra/download-dist.js",
         "mark-as-nightly": "node extra/mark-as-nightly.js",
         "reset-password": "node extra/reset-password.js",

From 6e07ed20816969bfd1c6c06eb518171938312782 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Fri, 7 Oct 2022 15:02:19 +0800
Subject: [PATCH 090/134] Fix #2186

---
 package-lock.json           | 41 +++++++++++++++++++++++++++----------
 package.json                |  1 +
 server/model/status_page.js | 15 ++++++++++----
 3 files changed, 42 insertions(+), 15 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 71827042..4f222171 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "uptime-kuma",
-    "version": "1.18.2",
+    "version": "1.18.3",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "uptime-kuma",
-            "version": "1.18.2",
+            "version": "1.18.3",
             "license": "MIT",
             "dependencies": {
                 "@louislam/sqlite3": "~15.0.6",
@@ -33,6 +33,7 @@
                 "http-proxy-agent": "^5.0.0",
                 "https-proxy-agent": "^5.0.0",
                 "iconv-lite": "^0.6.3",
+                "jsesc": "^3.0.2",
                 "jsonwebtoken": "~8.5.1",
                 "jwt-decode": "^3.1.2",
                 "limiter": "^2.1.0",
@@ -474,6 +475,18 @@
                 "node": ">=6.9.0"
             }
         },
+        "node_modules/@babel/generator/node_modules/jsesc": {
+            "version": "2.5.2",
+            "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+            "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+            "dev": true,
+            "bin": {
+                "jsesc": "bin/jsesc"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
         "node_modules/@babel/helper-annotate-as-pure": {
             "version": "7.18.6",
             "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz",
@@ -11229,15 +11242,14 @@
             "dev": true
         },
         "node_modules/jsesc": {
-            "version": "2.5.2",
-            "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-            "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
-            "dev": true,
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+            "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
             "bin": {
                 "jsesc": "bin/jsesc"
             },
             "engines": {
-                "node": ">=4"
+                "node": ">=6"
             }
         },
         "node_modules/json-parse-even-better-errors": {
@@ -16957,6 +16969,14 @@
                 "@babel/types": "^7.18.13",
                 "@jridgewell/gen-mapping": "^0.3.2",
                 "jsesc": "^2.5.1"
+            },
+            "dependencies": {
+                "jsesc": {
+                    "version": "2.5.2",
+                    "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+                    "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+                    "dev": true
+                }
             }
         },
         "@babel/helper-annotate-as-pure": {
@@ -25010,10 +25030,9 @@
             }
         },
         "jsesc": {
-            "version": "2.5.2",
-            "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-            "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
-            "dev": true
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+            "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="
         },
         "json-parse-even-better-errors": {
             "version": "2.3.1",
diff --git a/package.json b/package.json
index df0bdbfb..d478a1d2 100644
--- a/package.json
+++ b/package.json
@@ -88,6 +88,7 @@
         "http-proxy-agent": "^5.0.0",
         "https-proxy-agent": "^5.0.0",
         "iconv-lite": "^0.6.3",
+        "jsesc": "^3.0.2",
         "jsonwebtoken": "~8.5.1",
         "jwt-decode": "^3.1.2",
         "limiter": "^2.1.0",
diff --git a/server/model/status_page.js b/server/model/status_page.js
index 82d184bf..7682272c 100644
--- a/server/model/status_page.js
+++ b/server/model/status_page.js
@@ -2,6 +2,7 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
 const { R } = require("redbean-node");
 const cheerio = require("cheerio");
 const { UptimeKumaServer } = require("../uptime-kuma-server");
+const jsesc = require("jsesc");
 
 class StatusPage extends BeanModel {
 
@@ -56,13 +57,19 @@ class StatusPage extends BeanModel {
         head.append(`<meta property="og:description" content="${description155}" />`);
 
         // Preload data
-        const json = JSON.stringify(await StatusPage.getStatusPageData(statusPage));
-        head.append(`
-            <script>
-                window.preloadData = ${json}
+        // Add jsesc, fix https://github.com/louislam/uptime-kuma/issues/2186
+        const escapedJSONObject = jsesc(JSON.stringify(await StatusPage.getStatusPageData(statusPage)), {
+            "isScriptContext": true
+        });
+
+        const script = $(`
+            <script id="preload-data" data-json="{}">
+                window.preloadData = ${escapedJSONObject};
             </script>
         `);
 
+        head.append(script);
+
         // manifest.json
         $("link[rel=manifest]").attr("href", `/api/status-page/${statusPage.slug}/manifest.json`);
 

From 7d3cc002ea8fe95c7ca23885052280110e7f5217 Mon Sep 17 00:00:00 2001
From: Vasilis The Pikachu <vascreeper@yahoo.com>
Date: Fri, 7 Oct 2022 08:55:12 +0000
Subject: [PATCH 091/134] Added greek language

---
 src/i18n.js            |   1 +
 src/languages/el-GR.js | 585 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 586 insertions(+)
 create mode 100644 src/languages/el-GR.js

diff --git a/src/i18n.js b/src/i18n.js
index 8495cd99..4c19eb00 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -34,6 +34,7 @@ const languageList = {
     "zh-TW": "繁體中文 (台灣)",
     "uk-UA": "Український",
     "th-TH": "ไทย",
+    "el-GR": "Ελληνικά",
 };
 
 let messages = {
diff --git a/src/languages/el-GR.js b/src/languages/el-GR.js
new file mode 100644
index 00000000..29af0417
--- /dev/null
+++ b/src/languages/el-GR.js
@@ -0,0 +1,585 @@
+export default {
+    languageName: "Ελληνικά",
+    checkEverySecond: "Έλεγχος κάθε {0} δευτερόλεπτα",
+    retryCheckEverySecond: "Επανάληψη κάθε {0} δευτερόλεπτα",
+    resendEveryXTimes: "Επανάληψη αποστολής ειδοποίησης κάθε {0} φορές",
+    resendDisabled: "Η επανάληψη αποστολής ειδοποίησης είναι απενεργοποιημένη",
+    retriesDescription: "Μέγιστες επαναλήψεις προτού η υπηρεσία επισημανθεί ως κατω και σταλεί μια ειδοποίηση",
+    ignoreTLSError: "Παράβλεψη σφάλματος TLS/SSL για ιστότοπους HTTPS",
+    upsideDownModeDescription: "Αναποδογυρίστε την κατάσταση. Εάν η υπηρεσία είναι προσβάσιμη, είναι ΚΑΤΩ.",
+    maxRedirectDescription: "Μέγιστος αριθμός redirect που θα ακολουθήσουν. Ρυθμίστε το 0 για να απενεργοποιήσετε τα redirect.",
+    acceptedStatusCodesDescription: "Επιλέξτε κωδικούς κατάστασης που θεωρούνται επιτυχή.",
+    passwordNotMatchMsg: "Ο κωδικός δεν ταιριάζει.",
+    notificationDescription: "Οι ειδοποιήσεις πρέπει να εκχωρηθούν σε μια παρακολούθηση για να λειτουργήσουν.",
+    keywordDescription: "Αναζήτηση λέξης-κλειδιού σε απλή απόκριση HTML ή JSON. Η αναζήτηση είναι διάκριση πεζών-κεφαλαίων.",
+    pauseDashboardHome: "Παύση",
+    deleteMonitorMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την παρακολούθηση;",
+    deleteNotificationMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την ειδοποίηση για όλες τις παρακολούθησης?",
+    dnsPortDescription: "Θύρα διακομιστή DNS. Προεπιλογή σε 53. Μπορείτε να αλλάξετε τη θύρα ανά πάσα στιγμή.",
+    resolverserverDescription: "Το Cloudflare είναι ο προεπιλεγμένος διακομιστής. Μπορείτε να αλλάξετε τον διακομιστή επίλυσης ανά πάσα στιγμήhe default server. You can change the resolver server anytime.",
+    rrtypeDescription: "Επιλέξτε τον τύπο RR που θέλετε να παρακολουθήσετε",
+    pauseMonitorMsg: "Είστε βέβαιοι ότι θέλετε να κάνετε παύση;",
+    enableDefaultNotificationDescription: "Αυτή η ειδοποίηση θα είναι ενεργοποιημένη από προεπιλογή για νέες παρακολούθησης. Μπορείτε ακόμα να απενεργοποιήσετε την ειδοποίηση ξεχωριστά για κάθε παρακολούθηση.",
+    clearEventsMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε όλα τα συμβάντα για αυτήν την παρακολούθηση;",
+    clearHeartbeatsMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε όλους τους καρδιακούς παλμούς για αυτήν την παρακολούθηση;",
+    confirmClearStatisticsMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε ΟΛΑ τα στατιστικά στοιχεία;?",
+    importHandleDescription: "Επιλέξτε «Παράλειψη υπάρχοντος» εάν θέλετε να παραλείψετε κάθε παρακολούθηση ή ειδοποίηση με το ίδιο όνομα. Το 'Overwrite' θα διαγράψει κάθε υπάρχουσα παρακολούθηση και ειδοποίηση.",
+    confirmImportMsg: "Είστε βέβαιοι ότι θέλετε να εισαγάγετε το αντίγραφο ασφαλείας; Επαληθεύστε ότι έχετε επιλέξει τη σωστή επιλογή.",
+    twoFAVerifyLabel: "Εισαγάγετε το 2FA κωδικό για να επαληθεύσετε: ",
+    tokenValidSettingsMsg: "Ο κωδικός 2FA είναι έγκυρο! Τώρα μπορείτε να αποθηκεύσετε τις ρυθμίσεις 2FA",
+    confirmEnableTwoFAMsg: "Είστε βέβαιοι ότι θέλετε να ενεργοποιήσετε το 2FA;",
+    confirmDisableTwoFAMsg: "Είστε βέβαιοι ότι θέλετε να απενεργοποιήσετε το 2FA;",
+    Settings: "Ρυθμίσεις",
+    Dashboard: "Πίνακας",
+    "New Update": "Νέα αναβάθμιση",
+    Language: "Γλώσσα",
+    Appearance: "Εμφάνιση",
+    Theme: "Θέμα",
+    General: "Γενικά",
+    "Primary Base URL": "Κύρια βασική διεύθυνση URL",
+    Version: "Εκδοχή",
+    "Check Update On GitHub": "Ελέγξτε για Ενημέρωση στο GitHub",
+    List: "Λίστα",
+    Add: "Προσθήκη",
+    "Add New Monitor": "Προσθήκη νέας παρακολούθησης",
+    "Quick Stats": "Γρήγορα στατιστικά",
+    Up: "Πάνω",
+    Down: "Κάτω",
+    Pending: "Εκκρεμεί",
+    Unknown: "Άγνωστο",
+    Pause: "Παύση",
+    Name: "Ονομα",
+    Status: "Κατάσταση",
+    DateTime: "ΗμερομηνίαΏρα",
+    Message: "Μήνυμα",
+    "No important events": "Δεν υπάρχουν σημαντικά γεγονότα",
+    Resume: "Συνέχιση",
+    Edit: "Επεξεργασία",
+    Delete: "Διαγράφω",
+    Current: "Current",
+    Uptime: "Χρόνος λειτουργίας",
+    "Cert Exp.": "Cert Exp.",
+    day: "ημέρα | ημέρες",
+    "-day": "-ημέρα",
+    hour: "ώρα",
+    "-hour": "-ώρα",
+    Response: "Απάντηση",
+    Ping: "Ping",
+    "Monitor Type": "Τύπος παρακολούθησης",
+    Keyword: "Λέξη-κλειδί",
+    "Friendly Name": "Φιλικό όνομα",
+    URL: "URL",
+    Hostname: "Hostname",
+    Port: "Port",
+    "Heartbeat Interval": "Διάστημα καρδιακών παλμών",
+    Retries: "Επαναλήψεις",
+    "Heartbeat Retry Interval": "Διάστημα επανάληψης παλμών καρδιάς",
+    "Resend Notification if Down X times consequently": "Αποστολή νέας ειδοποίησης εάν κατω X φορές κατά συνέχεια",
+    Advanced: "Προχωρημένα",
+    "Upside Down Mode": "Ανάποδη λειτουργία",
+    "Max. Redirects": "Μέγιστη. Ανακατευθύνσεις",
+    "Accepted Status Codes": "Αποδεκτοί Κωδικοί Κατάστασης",
+    "Push URL": "Push URL",
+    needPushEvery: "Θα πρέπει να καλείτε αυτήν τη διεύθυνση URL κάθε {0} δευτερόλεπτα.",
+    pushOptionalParams: "Προαιρετικές παράμετροι: {0}",
+    Save: "Αποθηκεύση",
+    Notifications: "Ειδοποιήσεις",
+    "Not available, please setup.": "Μη διαθέσιμο, παρακαλώ ρυθμίστε.",
+    "Setup Notification": "Δημιουργία ειδοποίησης",
+    Light: "Φωτεινό",
+    Dark: "Σκοτεινό",
+    Auto: "Αυτόματο",
+    "Theme - Heartbeat Bar": "Θέμα - Μπάρα καρδιακών παλμών",
+    Normal: "Κανονικό",
+    Bottom: "Κάτω μέρος",
+    None: "Τίποτα",
+    Timezone: "Ζώνη ώρας",
+    "Search Engine Visibility": "Ορατότητα μηχανών αναζήτησης",
+    "Allow indexing": "Να επιτρέπεται η ευρετηρίαση",
+    "Discourage search engines from indexing site": "Αποθαρρύνετε τις μηχανές αναζήτησης από την ευρετηρίαση ιστότοπου",
+    "Change Password": "Αλλαγή κωδικού πρόσβασης",
+    "Current Password": "Τρέχων κωδικός πρόσβασης",
+    "New Password": "Νέος κωδικός πρόσβασης",
+    "Repeat New Password": "Επαναλάβετε τον νέο κωδικό πρόσβασης",
+    "Update Password": "Ενημέρωση κωδικού πρόσβασης",
+    "Disable Auth": "Απενεργοποίηση ελέγχου ταυτότητας",
+    "Enable Auth": "Ενεργοποίηση ελέγχου ταυτότητας",
+    "disableauth.message1": "Είστε βέβαιοι ότι θέλετε να <strong>απενεργοποιήσετε τον έλεγχο ταυτότητας</strong>;",
+    "disableauth.message2": "Έχει σχεδιαστεί για σενάρια <strong>όπου σκοπεύετε να εφαρμόσετε έλεγχο ταυτότητας τρίτου μέρους</strong> μπροστά από το Uptime Kuma, όπως το Cloudflare Access, Authelia ή άλλους μηχανισμούς ελέγχου ταυτότητας.",
+    "Please use this option carefully!": "Χρησιμοποιήστε αυτή την επιλογή προσεκτικά!",
+    Logout: "Αποσύνδεση",
+    Leave: "Φύγετε",
+    "I understand, please disable": "Καταλαβαίνω, απενεργοποιήστε",
+    Confirm: "Επιβεβαίωση",
+    Yes: "Ναί",
+    No: "Οχι",
+    Username: "Όνομα χρήστη",
+    Password: "Κωδικός πρόσβασης",
+    "Remember me": "Θυμήσου με",
+    Login: "Σύνδεση",
+    "No Monitors, please": "Δεν υπάρχουν παρακολούθησης παρακαλώ",
+    "add one": "προσθέστε ένα",
+    "Notification Type": "Είδος ειδοποίησης",
+    Email: "Email",
+    Test: "Δοκιμή",
+    "Certificate Info": "Πληροφορίες πιστοποιητικού",
+    "Resolver Server": "Διακομιστής επίλυσης",
+    "Resource Record Type": "Τύπος εγγραφής πόρων",
+    "Last Result": "Τελευταίο Αποτέλεσμα",
+    "Create your admin account": "Δημιουργήστε τον λογαριασμό διαχειριστή σας",
+    "Repeat Password": "Επαναλάβετε τον κωδικό πρόσβασης",
+    "Import Backup": "Εισαγωγή αντιγράφων ασφαλείας",
+    "Export Backup": "Εξαγωγή αντιγράφων ασφαλείας",
+    Export: "Εξαγωγή",
+    Import: "Εισαγωγή",
+    respTime: "Χρόν. Aπό (ms)",
+    notAvailableShort: "N/A",
+    "Default enabled": "Προεπιλογή ενεργοποιημένη",
+    "Apply on all existing monitors": "Εφαρμόστε σε όλες τις υπάρχουσες παρακολούθησης",
+    Create: "Δημιουργία",
+    "Clear Data": "Καθαρισμός δεδομένων",
+    Events: "Γεγονότα",
+    Heartbeats: "Παλμοι καρδιας",
+    "Auto Get": "Αυτόματη λήψη",
+    backupDescription: "Μπορείτε να δημιουργήσετε αντίγραφα ασφαλείας γία ολλες της παρακολούθησης και ειδοποιήσης σε ένα αρχείο JSON..",
+    backupDescription2: "Σημείωση: δεν περιλαμβάνονται δεδομένα ιστορικού και συμβάντων.",
+    backupDescription3: "Στο αρχείο εξαγωγής περιλαμβάνονται ευαίσθητα δεδομένα, όπως token ειδοποιήσεων. Aποθηκεύστε την εξαγωγή με ασφάλεια..",
+    alertNoFile: "Επιλέξτε ένα αρχείο για εισαγωγή.",
+    alertWrongFileType: "Επιλέξτε ένα αρχείο JSON.",
+    "Clear all statistics": "Εκκαθάριση όλων των στατιστικών",
+    "Skip existing": "Παράβλεψη υπάρχοντος",
+    Overwrite: "Αντικατάσταση",
+    Options: "Επιλογές",
+    "Keep both": "Κράτα και τα δύο",
+    "Verify Token": "Επαλήθευση Token",
+    "Setup 2FA": "Ρύθμιση 2FA",
+    "Enable 2FA": "Ενεργοποίηση 2FA",
+    "Disable 2FA": "Απενεργοποίηση 2FA",
+    "2FA Settings": "Ρυθμίσεις 2FA",
+    "Two Factor Authentication": "Έλεγχος ταυτότητας δύο παραγόντων",
+    Active: "Ενεργός",
+    Inactive: "Ανενεργό",
+    Token: "Token",
+    "Show URI": "Εμφάνιση URI",
+    Tags: "Ετικέτες",
+    "Add New below or Select...": "Προσθήκη νέου παρακάτω ή Επιλέξτε...",
+    "Tag with this name already exist.": "Υπάρχει ήδη η ετικέτα με αυτό το όνομα.",
+    "Tag with this value already exist.": "Υπάρχει ήδη ετικέτα με αυτό το value.",
+    color: "χρώμα",
+    "value (optional)": "value (optional)",
+    Gray: "Γκρί",
+    Red: "Κόκκινο",
+    Orange: "Πορτοκάλι",
+    Green: "Πράσινο",
+    Blue: "Μπλε",
+    Indigo: "Indigo",
+    Purple: "Μωβ",
+    Pink: "Ροζ",
+    "Search...": "Αναζήτηση...",
+    "Avg. Ping": "Μέσo.Ping",
+    "Avg. Response": "Μέσo. Aπάντηση",
+    "Entry Page": "Σελίδα εισαγωγής",
+    statusPageNothing: "Δεν υπάρχει τίποτα εδώ, προσθέστε μια ομάδα ή μια παρακολούθηση.",
+    "No Services": "Δεν υπάρχουν υπηρεσίες",
+    "All Systems Operational": "Όλα τα συστήματα λειτουργούν",
+    "Partially Degraded Service": "Μερικώς υποβαθμισμένη υπηρεσία",
+    "Degraded Service": "Υποβαθμισμένη υπηρεσία",
+    "Add Group": "Προσθήκη γρουπ",
+    "Add a monitor": "Προσθήκη παρακολούθησης",
+    "Edit Status Page": "Επεξεργασία σελίδας κατάστασης",
+    "Go to Dashboard": "Μεταβείτε στον Πίνακα ελέγχου",
+    "Status Page": "Σελίδα κατάστασης",
+    "Status Pages": "Σελίδες κατάστασης",
+    defaultNotificationName: "Η ειδοποίηση μου {notification} ({number})",
+    here: "εδώ",
+    Required: "Απαιτείται",
+    telegram: "Telegram",
+    "Bot Token": "Διακριτικό Bot",
+    wayToGetTelegramToken: "Μπορείτε να πάρετε ένα διακριτικό από {0}.",
+    "Chat ID": "Chat ID",
+    supportTelegramChatID: "Support Direct Chat / Group / Channel's Chat ID",
+    wayToGetTelegramChatID: "Μπορείτε να λάβετε το αναγνωριστικό συνομιλίας σας στέλνοντας ένα μήνυμα στο bot και μεταβαίνοντας σε αυτήν τη διεύθυνση URL για να προβάλετε το chat_id:",
+    "YOUR BOT TOKEN HERE": "ΤΟ BOT ΣΑΣ ΔΙΑΚΡΙΤΙΚΌ ΕΔΩ",
+    chatIDNotFound: "Το Chat ID δεν βρέθηκε. Στείλτε πρώτα ένα μήνυμα σε αυτό το bot",
+    webhook: "Webhook",
+    "Post URL": "Post URL",
+    "Content Type": "Τύπος περιεχομένου",
+    webhookJsonDesc: "{0} είναι καλό για οποιονδήποτε σύγχρονο διακομιστή HTTP όπως το Express.js",
+    webhookFormDataDesc: "{multipart} είναι καλό για την PHP. Το JSON θα πρέπει να αναλυθεί με {decodeFunction}",
+    smtp: "Email (SMTP)",
+    secureOptionNone: "None / STARTTLS (25, 587)",
+    secureOptionTLS: "TLS (465)",
+    "Ignore TLS Error": "Παράβλεψη σφάλματος TLS",
+    "From Email": "Από Email",
+    emailCustomSubject: "Προσαρμοσμένο θέμα",
+    "To Email": "Προς Email",
+    smtpCC: "CC",
+    smtpBCC: "BCC",
+    discord: "Discord",
+    "Discord Webhook URL": "Discord Webhook URL",
+    wayToGetDiscordURL: "Μπορείτε να το αποκτήσετε μεταβαίνοντας στις Ρυθμίσεις διακομιστή -> Ενσωματώσεις -> Δημιουργία Webhook",
+    "Bot Display Name": "Εμφανιζόμενο όνομα bot",
+    "Prefix Custom Message": "Προσαρμοσμένο μήνυμα",
+    "Hello @everyone is...": "Γεια {'@'}everyone ειναι...",
+    teams: "Microsoft Teams",
+    "Webhook URL": "Webhook URL",
+    wayToGetTeamsURL: "Μπορείτε να μάθετε πώς να δημιουργείτε μια διεύθυνση URL webhook {0}.",
+    signal: "Signal",
+    Number: "Αριθμός",
+    Recipients: "Αποδέκτες",
+    needSignalAPI: "Πρέπει να έχετε ένα signal client με REST API..",
+    wayToCheckSignalURL: "Μπορείτε να ελέγξετε αυτό το URL για να δείτε πώς να ρυθμίσετε ένα:",
+    signalImportant: "ΣΗΜΑΝΤΙΚΟ: Δεν μπορείτε να συνδυάσετε ομάδες και αριθμούς στους παραλήπτες!",
+    gotify: "Gotify",
+    "Application Token": "Token εφαρμογής",
+    "Server URL": "URL διακομιστή",
+    Priority: "Προτεραιότητα",
+    slack: "Slack",
+    "Icon Emoji": "Εικονίδιο Emoji",
+    "Channel Name": "Όνομα καναλιού",
+    "Uptime Kuma URL": "Uptime Kuma URL",
+    aboutWebhooks: "Περισσότερες πληροφορίες σχετικά με τα Webhooks στο: {0}",
+    aboutChannelName: "Εισαγάγετε το όνομα του καναλιού στο {0} Όνομα καναλιού εάν θέλετε να παρακάμψετε το κανάλι Webhook. Π.χ.: #other-channel",
+    aboutKumaURL: "Εάν αφήσετε κενό το πεδίο URL Uptime Kuma, θα είναι προεπιλεγμένο στη σελίδα Project GitHub..",
+    emojiCheatSheet: "Φύλλο εξαπάτησης emoji: {0}",
+    "rocket.chat": "Rocket.Chat",
+    pushover: "Pushover",
+    pushy: "Pushy",
+    PushByTechulus: "Push by Techulus",
+    octopush: "Octopush",
+    promosms: "PromoSMS",
+    clicksendsms: "ClickSend SMS",
+    lunasea: "LunaSea",
+    apprise: "Apprise (Support 50+ Notification services)",
+    GoogleChat: "Google Chat (Google Workspace only)",
+    pushbullet: "Pushbullet",
+    line: "Line Messenger",
+    mattermost: "Mattermost",
+    "User Key": "Κλειδί χρήστη",
+    Device: "Συσκευή",
+    "Message Title": "Τίτλος μηνύματος",
+    "Notification Sound": "Ήχος ειδοποίησης",
+    "More info on:": "Περισσότερες πληροφορίες στο: {0}",
+    pushoverDesc1: "Η προτεραιότητα έκτακτης ανάγκης (2) έχει προεπιλεγμένο χρονικό όριο 30 δευτερολέπτων μεταξύ των επαναλήψεων και θα λήξει μετά από 1 ώρα.",
+    pushoverDesc2: "Εάν θέλετε να στέλνετε ειδοποιήσεις σε διαφορετικές συσκευές, συμπληρώστε το πεδίο Συσκευή.",
+    "SMS Type": "Τύπος SMS",
+    octopushTypePremium: "Premium (Γρήγορη - συνιστάται για ειδοποίηση)",
+    octopushTypeLowCost: "Χαμηλό κόστος (Αργό - μερικές φορές μπλοκάρεται από τον χειριστή)",
+    checkPrice: "Ελέγξτε τις τιμές {0}:",
+    apiCredentials: "API credentials",
+    octopushLegacyHint: "Χρησιμοποιείτε την παλαιού τύπου έκδοση του Octopush (2011-2020) ή τη νέα έκδοση;",
+    "Check octopush prices": "Ελέγξτε τις τιμές OctoPush {0}.",
+    octopushPhoneNumber: "Αριθμός τηλεφώνου (διεθνής μορφή, π.χ.: +30694345678)",
+    octopushSMSSender: "Όνομα αποστολέα SMS: 3-11 αλφαριθμητικοί χαρακτήρες και διάστημα (a-zA-Z0-9)",
+    "LunaSea Device ID": "LunaSea Device ID",
+    "Apprise URL": "Apprise URL",
+    "Example:": "Παράδειγμα: {0}",
+    "Read more:": "Διαβάστε περισσότερα: {0}",
+    "Status:": "Κατάσταση: {0}",
+    "Read more": "Διαβάστε περισσότερα",
+    appriseInstalled: "Το Apprise έχει εγκατασταθεί.",
+    appriseNotInstalled: "Το Apprise δεν έχει εγκατασταθεί. {0}",
+    "Access Token": "Access Token",
+    "Channel access token": "Channel Access Token",
+    "Line Developers Console": "Line Developers Console",
+    lineDevConsoleTo: "Line Developers Console - {0}",
+    "Basic Settings": "Βασικές ρυθμίσεις",
+    "User ID": "User ID",
+    "Messaging API": "Messaging API",
+    wayToGetLineChannelToken: "Πρώτα αποκτήστε πρόσβαση στο {0}, δημιουργήστε έναν πάροχο και ένα κανάλι (Messanging API) και, στη συνέχεια, μπορείτε να λάβετε το channel access token και το user ID από τα παραπάνω στοιχεία μενού.",
+    "Icon URL": "Διεύθυνση URL εικονιδίου",
+    aboutIconURL: "Μπορείτε να παρέχετε έναν σύνδεσμο προς μια εικόνα στο \"Icon URL\" για να παρακάμψετε την προεπιλεγμένη εικόνα προφίλ. Δεν θα χρησιμοποιηθεί εάν έχει οριστεί το εικονίδιο Emoji.",
+    aboutMattermostChannelName: "Μπορείτε να παρακάμψετε το προεπιλεγμένο κανάλι στο οποίο δημοσιεύει το Webhook εισάγοντας το όνομα του καναλιού στο πεδίο \"Όνομα καναλιού\". Αυτό πρέπει να ενεργοποιηθεί στις ρυθμίσεις του Mattermost Webhook. Π.χ.: #other-channel",
+    matrix: "Matrix",
+    promosmsTypeEco: "SMS ECO - φθηνό αλλά αργό και συχνά υπερφορτωμένο. Περιορίζεται μόνο σε Πολωνούς παραλήπτες.",
+    promosmsTypeFlash: "SMS FLASH - Το μήνυμα θα εμφανίζεται αυτόματα στη συσκευή του παραλήπτη. Περιορίζεται μόνο σε Πολωνούς παραλήπτες.",
+    promosmsTypeFull: "SMS FULL - Premium επίπεδο SMS, Μπορείτε να χρησιμοποιήσετε το Όνομα Αποστολέα σας (Πρέπει πρώτα να καταχωρήσετε το όνομα). Αξιόπιστο για ειδοποιήσεις.",
+    promosmsTypeSpeed: "SMS SPEED - Υψηλότερη προτεραιότητα στο σύστημα. Πολύ γρήγορο και αξιόπιστο αλλά ακριβό (περίπου διπλάσια τιμή SMS FULL).",
+    promosmsPhoneNumber: "Αριθμός τηλεφώνου (για πολωνούς παραλήπτες Μπορείτε να παραλείψετε τους κωδικούς περιοχής)",
+    promosmsSMSSender: "Όνομα αποστολέα SMS: Προεγγεγραμμένο όνομα ή ένα από τα προεπιλεγμένα: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
+    "Feishu WebHookUrl": "Feishu WebHookURL",
+    matrixHomeserverURL: "Homeserver URL (με http(s):// και προαιρετικά θύρα)",
+    "Internal Room Id": "Internal Room ID",
+    matrixDesc1: "Μπορείτε να βρείτε το internal room ID ανατρέχοντας στην ενότητα για προχωρημένους των ρυθμίσεων δωματίου στο πρόγραμμα-πελάτη Matrix. Θα πρέπει να μοιάζει με !QMdRCpUIfLwsfjxye6:home.server.",
+    matrixDesc2: "Συνιστάται ανεπιφύλακτα να δημιουργήσετε έναν νέο χρήστη και να μην χρησιμοποιήσετε το διακριτικό πρόσβασης του χρήστη Matrix, καθώς θα επιτρέψει την πλήρη πρόσβαση στον λογαριασμό σας και σε όλα τα δωμάτια στα οποία συμμετέχετε. Αντίθετα, δημιουργήστε έναν νέο χρήστη και προσκαλέστε τον μόνο στο δωμάτιο στο οποίο θέλετε να λαμβάνετε την ειδοποίηση. Μπορείτε να λάβετε το access token εκτελώντας {0}",
+    Method: "Μέθοδος",
+    Body: "Σώμα",
+    Headers: "Headers",
+    PushUrl: "Push URL",
+    HeadersInvalidFormat: "The request headers are not valid JSON: ",
+    BodyInvalidFormat: "The request body is not valid JSON: ",
+    "Monitor History": "Ιστορικο Παρακολούθησης",
+    clearDataOlderThan: "Διατηρήστε τα δεδομένα ιστορικού παρακολούθησης για {0} ημέρες.",
+    PasswordsDoNotMatch: "Οι κωδικοί πρόσβασης δεν ταιριάζουν.",
+    records: "εγγραφές",
+    "One record": "Μία εγγραφή",
+    steamApiKeyDescription: "Για την παρακολούθηση ενός διακομιστή παιχνιδιών Steam χρειάζεστε ένα κλειδί Steam Web-API. Μπορείτε να καταχωρήσετε το κλειδί API σας εδώ: ",
+    "Current User": "Τρέχων χρήστης",
+    topic: "Θέμα",
+    topicExplanation: "Θέμα MQTT προς παρακολούθηση",
+    successMessage: "Μήνυμα επιτυχίας",
+    successMessageExplanation: "Μήνυμα MQTT που θα θεωρηθεί επιτυχές",
+    recent: "Πρόσφατος",
+    Done: "Ολοκληρώθηκε",
+    Info: "Πληροφορίες",
+    Security: "Ασφάλεια",
+    "Steam API Key": "Steam API Key",
+    "Shrink Database": "Συρρίκνωση βάσης δεδομένων",
+    "Pick a RR-Type...": "Επιλέξτε έναν τύπο RR...",
+    "Pick Accepted Status Codes...": "Επιλέξτε Αποδεκτούς κωδικούς κατάστασης...",
+    Default: "Προκαθορισμένο",
+    "HTTP Options": "Επιλογές HTTP",
+    "Create Incident": "Δημιουργία περιστατικού",
+    Title: "Τίτλος",
+    Content: "Περιεχόμενο",
+    Style: "Στυλ",
+    info: "πληροφορίες",
+    warning: "προειδοποίηση",
+    danger: "κίνδυνος",
+    error: "σφάλμα",
+    critical: "κριτικό",
+    primary: "primary",
+    light: "light",
+    dark: "dark",
+    Post: "Δημοσίευση",
+    "Please input title and content": "Παρακαλούμε εισαγάγετε τίτλο και περιεχόμενο",
+    Created: "Δημιουργήθηκε",
+    "Last Updated": "Τελευταία ενημέρωση",
+    Unpin: "Ξεκαρφιτσώστε",
+    "Switch to Light Theme": "Μετάβαση σε Ανιχτό θέμα",
+    "Switch to Dark Theme": "Μετάβαση σε Σκούρο θέμα",
+    "Show Tags": "Εμφάνιση ετικετών",
+    "Hide Tags": "Απόκρυψη ετικετών",
+    Description: "Περιγραφή",
+    "No monitors available.": "Δεν υπάρχουν διαθέσιμες παρακολουθήσεις.",
+    "Add one": "Προσθέστε ένα",
+    "No Monitors": "Χωρίς παρακολουθήσεις",
+    "Untitled Group": "Ομάδα χωρίς τίτλο",
+    Services: "Υπηρεσίες",
+    Discard: "Απορρίψει",
+    Cancel: "Ακυρο",
+    "Powered by": "Με την υποστήριξη του",
+    shrinkDatabaseDescription: "Ενεργοποίηση βάσης δεδομένων VACUUM για SQLite. Εάν η βάση δεδομένων σας έχει δημιουργηθεί μετά την έκδοση 1.10.0, το AUTO_VACUUM είναι ήδη ενεργοποιημένο και αυτή η ενέργεια δεν χρειάζεται.",
+    serwersms: "SerwerSMS.pl",
+    serwersmsAPIUser: "API Username (incl. webapi_ prefix)",
+    serwersmsAPIPassword: "API κωδικός πρόσβασης",
+    serwersmsPhoneNumber: "Αριθμός τηλεφώνου",
+    serwersmsSenderName: "Όνομα αποστολέα SMS (καταχωρήθηκε μέσω της πύλης πελατών)",
+    stackfield: "Stackfield",
+    Customize: "Προσαρμογή",
+    "Custom Footer": "Προσαρμογή Footer",
+    "Custom CSS": "Προσαρμογή CSS",
+    smtpDkimSettings: "Ρυθμίσεις DKIM",
+    smtpDkimDesc: "Ανατρέξτε στο Nodemailer DKIM {0} για χρήση.",
+    documentation: "documentation",
+    smtpDkimDomain: "Domain Name",
+    smtpDkimKeySelector: "Key Selector",
+    smtpDkimPrivateKey: "Private Key",
+    smtpDkimHashAlgo: "Hash Algorithm (Optional)",
+    smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
+    smtpDkimskipFields: "Header Keys not to sign (Optional)",
+    wayToGetPagerDutyKey: "Μπορείτε να το λάβετε μεταβαίνοντας στο Service -> Service Directory -> (Επιλέξτε μια υπηρεσία) -> Integrations -> Add integration. Εδώ μπορείτε να κάνετε αναζήτηση για \"Events API V2\". Περισσότερες πληροφορίες {0}",
+    "Integration Key": "Integration Key",
+    "Integration URL": "Integration URL",
+    "Auto resolve or acknowledged": "Αυτόματη επίλυση ή αναγνώριση",
+    "do nothing": "μην κάνεις τίποτα",
+    "auto acknowledged": "αυτόματη αναγνώριση",
+    "auto resolve": "αυτόματη επίλυση",
+    gorush: "Gorush",
+    alerta: "Alerta",
+    alertaApiEndpoint: "API Endpoint",
+    alertaEnvironment: "Environment",
+    alertaApiKey: "API Key",
+    alertaAlertState: "Alert State",
+    alertaRecoverState: "Recover State",
+    deleteStatusPageMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν τη σελίδα κατάστασης?",
+    Proxies: "Proxies",
+    default: "Προκαθορισμένο",
+    enabled: "Ενεργοποιημένο",
+    setAsDefault: "Ορίσετε ως προεπιλογή",
+    deleteProxyMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το proxy για όλες τις παρακολουθήσεις;",
+    proxyDescription: "Πρέπει να εκχωρηθούν proxies σε μια οθπαρακολουθή για να λειτουργήσουν..",
+    enableProxyDescription: "Το proxy δεν θα επηρεάσει τα αιτήματα της παρακολουθήσεις μέχρι να ενεργοποιηθεί. Μπορείτε να ελέγξετε την προσωρινή απενεργοποίηση του proxy από όλες τις παρακολουθήσεις βάσει κατάστασης ενεργοποίησης.",
+    setAsDefaultProxyDescription: "Αυτός το proxy θα είναι ενεργοποιημένο από προεπιλογή για νέες παρακολουθήσεις. Μπορείτε ακόμα να απενεργοποιήσετε το proxy ξεχωριστά για κάθε οθόνη.",
+    "Certificate Chain": "Certificate Chain",
+    Valid: "Εγκυρο",
+    Invalid: "Μη έγκυρο",
+    AccessKeyId: "AccessKey ID",
+    SecretAccessKey: "AccessKey Secret",
+    PhoneNumbers: "PhoneNumbers",
+    TemplateCode: "TemplateCode",
+    SignName: "SignName",
+    "Sms template must contain parameters: ": "Το πρότυπο SMS πρέπει να περιέχει παραμέτρους: ",
+    "Bark Endpoint": "Bark Endpoint",
+    "Bark Group": "Bark Ομάδα",
+    "Bark Sound": "Bark Ήχος",
+    WebHookUrl: "WebHookUrl",
+    SecretKey: "SecretKey",
+    "For safety, must use secret key": "Για ασφάλεια, πρέπει να χρησιμοποιήσετε secret key",
+    "Device Token": "Device Token",
+    Platform: "Platform",
+    iOS: "iOS",
+    Android: "Android",
+    Huawei: "Huawei",
+    High: "High",
+    Retry: "Ξαναδοκιμάσετε",
+    Topic: "Θέμα",
+    "WeCom Bot Key": "WeCom Bot Key",
+    "Setup Proxy": "Ρύθμιση Proxy",
+    "Proxy Protocol": "Πρωτόκολλο Proxy",
+    "Proxy Server": "Proxy Server",
+    "Proxy server has authentication": "Το Proxy διαθέτει έλεγχο ταυτότητας",
+    User: "Χρήστης",
+    Installed: "Εγκατεστημένο",
+    "Not installed": "Μη εγκατεστημενο",
+    Running: "Τρέχη",
+    "Not running": "Δεν τρεχη",
+    "Remove Token": "Κατάργηση Token",
+    Start: "Αρχή",
+    Stop: "Στάση",
+    "Uptime Kuma": "Uptime Kuma",
+    "Add New Status Page": "Προσθήκη νέας σελίδας κατάστασης",
+    Slug: "Slug",
+    "Accept characters:": "Αποδοχή χαρακτήρων:",
+    startOrEndWithOnly: "Ξεκινήστε ή τελειώστε μόνο με {0}",
+    "No consecutive dashes": "Χωρίς διαδοχικές παύλες",
+    Next: "Επόμενο",
+    "The slug is already taken. Please choose another slug.": "Ο slug έχει ήδη πιαστεί. Επιλέξτε άλλο slug.",
+    "No Proxy": "Οχι Proxy",
+    Authentication: "Authentication",
+    "HTTP Basic Auth": "HTTP Basic Auth",
+    "New Status Page": "Σελίδα νέας κατάστασης",
+    "Page Not Found": "Η σελίδα δεν βρέθηκε",
+    "Reverse Proxy": "Αντίστροφο Proxy",
+    Backup: "Αντιγράφων ασφαλείας",
+    About: "Σχετικά με",
+    wayToGetCloudflaredURL: "(Λήψη cloudflared από {0})",
+    cloudflareWebsite: "Ιστοσελίδα Cloudflare",
+    "Message:": "Μήνυμα:",
+    "Don't know how to get the token? Please read the guide:": "Δεν ξέρετε πώς να αποκτήσετε το token; Διαβάστε τον οδηγό:",
+    "The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Η τρέχουσα σύνδεση μπορεί να χαθεί εάν αυτή τη στιγμή συνδέεστε μέσω του Cloudflare Tunnel. Θέλετε σίγουρα να το σταματήσετε; Πληκτρολογήστε τον τρέχοντα κωδικό πρόσβασής σας για να τον επιβεβαιώσετε.",
+    "HTTP Headers": "HTTP Headers",
+    "Trust Proxy": "Εμπιστοσύνη του Proxy",
+    "Other Software": "Other Software",
+    "For example: nginx, Apache and Traefik.": "Για παράδειγμα: nginx, Apache και Traefik.",
+    "Please read": "Παρακαλώ διαβάστε",
+    "Subject:": "Θέμα:",
+    "Valid To:": "Εγκυρο για:",
+    "Days Remaining:": "Ημέρες που απομένουν:",
+    "Issuer:": "Εκδότης:",
+    "Fingerprint:": "Δακτυλικό αποτύπωμα:",
+    "No status pages": "Δεν υπάρχουν σελίδες κατάστασης",
+    "Domain Name Expiry Notification": "Ειδοποίηση λήξης ονόματος τομέα",
+    Proxy: "Proxy",
+    "Date Created": "Ημερομηνία Δημιουργίας",
+    HomeAssistant: "Home Assistant",
+    onebotHttpAddress: "OneBot HTTP Address",
+    onebotMessageType: "OneBot Message Type",
+    onebotGroupMessage: "Group",
+    onebotPrivateMessage: "Private",
+    onebotUserOrGroupId: "Group/User ID",
+    onebotSafetyTips: "Για ασφάλεια, πρέπει να ορίσετε το acess token",
+    "PushDeer Key": "PushDeer Key",
+    "Footer Text": "Κείμενο υποσέλιδου",
+    "Show Powered By": "Εμφάνιση Powered By",
+    "Domain Names": "Ονόματα Τομέα",
+    signedInDisp: "Συνδεθήκατε ως {0}",
+    signedInDispDisabled: "Εξουσιοδότηση είναι απενεργοποιημένη.",
+    RadiusSecret: "Radius Secret",
+    RadiusSecretDescription: "Shared Secret μεταξύ client και το server",
+    RadiusCalledStationId: "Called Station Id",
+    RadiusCalledStationIdDescription: "Identifier της καλούμενης συσκευής",
+    RadiusCallingStationId: "Calling Station Id",
+    RadiusCallingStationIdDescription: "Identifier oτης συσκευής κλήσης",
+    "Certificate Expiry Notification": "Ειδοποίηση Λήξης Πιστοποιητικού",
+    "API Username": "API Username",
+    "API Key": "API Key",
+    "Recipient Number": "Αριθμός Παραλήπτη",
+    "From Name/Number": "Από Όνομα/Αριθμός",
+    "Leave blank to use a shared sender number.": "Αφήστε το κενό για να χρησιμοποιήσετε έναν κοινόχρηστο αριθμό αποστολέα.",
+    "Octopush API Version": "Octopush API Version",
+    "Legacy Octopush-DM": "Legacy Octopush-DM",
+    endpoint: "endpoint",
+    octopushAPIKey: "\"API key\" από το HTTP API credentials στον πίνακα ελέγχου",
+    octopushLogin: "\"Login\" από το HTTP API credentials στον πίνακα ελέγχου",
+    promosmsLogin: "API Login Name",
+    promosmsPassword: "API Password",
+    "pushoversounds pushover": "Pushover (default)",
+    "pushoversounds bike": "Bike",
+    "pushoversounds bugle": "Bugle",
+    "pushoversounds cashregister": "Cash Register",
+    "pushoversounds classical": "Classical",
+    "pushoversounds cosmic": "Cosmic",
+    "pushoversounds falling": "Falling",
+    "pushoversounds gamelan": "Gamelan",
+    "pushoversounds incoming": "Incoming",
+    "pushoversounds intermission": "Intermission",
+    "pushoversounds magic": "Magic",
+    "pushoversounds mechanical": "Mechanical",
+    "pushoversounds pianobar": "Piano Bar",
+    "pushoversounds siren": "Siren",
+    "pushoversounds spacealarm": "Space Alarm",
+    "pushoversounds tugboat": "Tug Boat",
+    "pushoversounds alien": "Alien Alarm (long)",
+    "pushoversounds climb": "Climb (long)",
+    "pushoversounds persistent": "Persistent (long)",
+    "pushoversounds echo": "Pushover Echo (long)",
+    "pushoversounds updown": "Up Down (long)",
+    "pushoversounds vibrate": "Vibrate Only",
+    "pushoversounds none": "None (silent)",
+    pushyAPIKey: "Μυστικό API Key",
+    pushyToken: "Τoken Συσκευής",
+    "Show update if available": "Εμφάνιση ενημέρωσης εάν είναι διαθέσιμη",
+    "Also check beta release": "Ελέγξτε επίσης την έκδοση beta",
+    "Using a Reverse Proxy?": "Χρησιμοποιείτε reverse proxy;",
+    "Check how to config it for WebSocket": "Ελέγξτε πώς να το ρυθμίσετε για το WebSocket",
+    "Steam Game Server": "Διακομιστής παιχνιδιών Steam",
+    "Most likely causes:": "Πιο πιθανές αιτίες:",
+    "The resource is no longer available.": "Ο πόρος δεν είναι πλέον διαθέσιμος.",
+    "There might be a typing error in the address.": "Μπορεί να υπάρχει σφάλμα πληκτρολόγησης στη διεύθυνση.",
+    "What you can try:": "Τι μπορείτε να δοκιμάσετε:",
+    "Retype the address.": "Πληκτρολογήστε ξανά τη διεύθυνση.",
+    "Go back to the previous page.": "Επιστρέψτε στην προηγούμενη σελίδα.",
+    "Coming Soon": "Ερχεται σύντομα",
+    wayToGetClickSendSMSToken: "Μπορείτε να πάρετε το API Username και API Key απο {0} .",
+    "Connection String": "Connection String",
+    Query: "Query",
+    settingsCertificateExpiry: "Λήξη πιστοποιητικού TLS",
+    certificationExpiryDescription: "Οι παρακολουθήσεις HTTPS ενεργοποιούν ειδοποίηση όταν λήξει το πιστοποιητικό TLS σε:",
+    "Setup Docker Host": "Ρύθμιση Docker Host",
+    "Connection Type": "Τύπος σύνδεσης",
+    "Docker Daemon": "Docker Daemon",
+    deleteDockerHostMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον κεντρικό υπολογιστή βάσης για όλες τις παρακολουθήσεις;",
+    socket: "Socket",
+    tcp: "TCP / HTTP",
+    "Docker Container": "Docker Container",
+    "Container Name / ID": "Container Name / ID",
+    "Docker Host": "Docker Host",
+    "Docker Hosts": "Docker Hosts",
+    "ntfy Topic": "ntfy Topic",
+    Domain: "Domain",
+    Workstation: "Workstation",
+    disableCloudflaredNoAuthMsg: "Βρίσκεστε σε λειτουργία No Auth, δεν απαιτείται κωδικός πρόσβασης.",
+    trustProxyDescription: "Εμπιστευτείτε τις κεφαλίδες 'X-Forwarded-*'. Εάν θέλετε να λάβετε τη σωστή IP πελάτη και το Uptime Kuma σας βρίσκεται πίσω το Nginx ή το Apache, θα πρέπει να το ενεργοποιήσετε.",
+    wayToGetLineNotifyToken: "Μπορείτε να λάβετε ένα access token από το {0}",
+    Examples: "Παραδείγματα",
+    "Home Assistant URL": "Home Assistant URL",
+    "Long-Lived Access Token": "Long-Lived Access Token",
+    "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Long-Lived Access Token μπορεί να δημιουργηθεί κάνοντας κλικ στο όνομα του προφίλ σας (κάτω αριστερά) και κάνοντας κύλιση προς τα κάτω και, στη συνέχεια, κάντε κλικ στο Create Token. ",
+    "Notification Service": "Υπηρεσία ειδοποιήσεων",
+    "default: notify all devices": "προεπιλογή: ειδοποίηση όλων των συσκευών",
+    "A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Μπορείτε να βρείτε μια λίστα με τις Υπηρεσίες ειδοποιήσεων στον Home assistant στην περιοχή \"Developer Tools > Services\" αναζήτηση για \"notification\" για να βρείτε το όνομα της συσκευής/τηλεφώνου σας.",
+    "Automations can optionally be triggered in Home Assistant:": "Οι αυτοματισμοί μπορούν προαιρετικά να ενεργοποιηθούν στο Home Assistant:",
+    "Trigger type:": "Τύπος ενεργοποίησης:",
+    "Event type:": "Τύπος συμβάντος:",
+    "Event data:": "Δεδομένα συμβάντος:",
+    "Then choose an action, for example switch the scene to where an RGB light is red.": "Στη συνέχεια, επιλέξτε μια ενέργεια, για παράδειγμα αλλάξτε τη σκηνή στο σημείο όπου ένα φως RGB είναι κόκκινο.",
+    "Frontend Version": "Έκδοση Frontend",
+    "Frontend Version do not match backend version!": "Η Frontend έκδοση δεν ταιριάζει με την έκδοση backend!",
+    "Base URL": "Βασική διεύθυνση URL",
+    goAlertInfo: "Το GoAlert είναι μια εφαρμογή ανοιχτού κώδικα για προγραμματισμό κλήσεων, αυτοματοποιημένες κλιμακώσεις και ειδοποιήσεις (όπως SMS ή φωνητικές κλήσεις). Αλληλεπιδράστε αυτόματα με το σωστό άτομο, με τον σωστό τρόπο και τη σωστή στιγμή! {0}",
+    goAlertIntegrationKeyInfo: "Λάβετε το generic API integration key για την υπηρεσία σε αυτήν τη μορφή \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" συνήθως την τιμή της παραμέτρου διακριτικού της αντιγραμμένης διεύθυνσης URL.",
+    goAlert: "GoAlert",
+    backupOutdatedWarning: "Καταργήθηκε: Επειδή προστέθηκαν πολλές δυνατότητες και αυτή η δυνατότητα δημιουργίας αντιγράφων ασφαλείας δεν διατηρείται πολη, δεν μπορεί να δημιουργήσει ή να επαναφέρει ένα πλήρες αντίγραφο ασφαλείας.",
+    backupRecommend: "Παρακαλούμε δημιουργήστε αντίγραφα ασφαλείας του τόμου ή του φακέλου δεδομένων (./data/) απευθείας.",
+};

From e29527e22fad7f9691b3fed4b29466f9bd1ced9c Mon Sep 17 00:00:00 2001
From: Vasilis The Pikachu <vasilis@pikachu.systems>
Date: Fri, 7 Oct 2022 11:33:29 +0200
Subject: [PATCH 092/134] Update Greek

---
 src/languages/el-GR.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/languages/el-GR.js b/src/languages/el-GR.js
index 29af0417..92a2d775 100644
--- a/src/languages/el-GR.js
+++ b/src/languages/el-GR.js
@@ -55,7 +55,7 @@ export default {
     "No important events": "Δεν υπάρχουν σημαντικά γεγονότα",
     Resume: "Συνέχιση",
     Edit: "Επεξεργασία",
-    Delete: "Διαγράφω",
+    Delete: "Διαγράφη",
     Current: "Current",
     Uptime: "Χρόνος λειτουργίας",
     "Cert Exp.": "Cert Exp.",
@@ -448,11 +448,11 @@ export default {
     "No Proxy": "Οχι Proxy",
     Authentication: "Authentication",
     "HTTP Basic Auth": "HTTP Basic Auth",
-    "New Status Page": "Σελίδα νέας κατάστασης",
+    "New Status Page": "Νέας Σελίδα κατάστασης",
     "Page Not Found": "Η σελίδα δεν βρέθηκε",
     "Reverse Proxy": "Αντίστροφο Proxy",
     Backup: "Αντιγράφων ασφαλείας",
-    About: "Σχετικά με",
+    About: "Σχετικά με το Uptime Kuma",
     wayToGetCloudflaredURL: "(Λήψη cloudflared από {0})",
     cloudflareWebsite: "Ιστοσελίδα Cloudflare",
     "Message:": "Μήνυμα:",

From 4f6dec41c617267860ec9c686477e03c633e7c4b Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Fri, 7 Oct 2022 20:46:43 +0800
Subject: [PATCH 093/134] Fix ntfy username should not be required

---
 src/components/notifications/Ntfy.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/notifications/Ntfy.vue b/src/components/notifications/Ntfy.vue
index ddcc3917..b0f7888a 100644
--- a/src/components/notifications/Ntfy.vue
+++ b/src/components/notifications/Ntfy.vue
@@ -18,7 +18,7 @@
     <div class="mb-3">
         <label for="ntfy-username" class="form-label">{{ $t("Username") }} ({{ $t("Optional") }})</label>
         <div class="input-group mb-3">
-            <input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control" required>
+            <input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control">
         </div>
     </div>
     <div class="mb-3">

From 43c1ec640c41e83b0cb7783b1760f7781fab8f4a Mon Sep 17 00:00:00 2001
From: AnnAngela <naganjue@vip.qq.com>
Date: Sat, 8 Oct 2022 01:45:00 +0800
Subject: [PATCH 094/134] feat: :globe_with_meridians: Update zh-cn and en
 translation (#2167)

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
---
 src/components/notifications/HomeAssistant.vue |  2 +-
 src/components/notifications/SMSManager.vue    |  2 +-
 src/languages/en.js                            |  6 ++++++
 src/languages/zh-CN.js                         | 14 ++++++++++++++
 4 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/src/components/notifications/HomeAssistant.vue b/src/components/notifications/HomeAssistant.vue
index 67e370a1..de368095 100644
--- a/src/components/notifications/HomeAssistant.vue
+++ b/src/components/notifications/HomeAssistant.vue
@@ -18,7 +18,7 @@
         <input id="notificationService" v-model="$parent.notification.notificationService" type="text" :placeholder="$t('default: notify all devices')" class="form-control">
 
         <div class="form-text">
-            <p>{{ $t("A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.") }}</p>
+            <p>{{ $t('A list of Notification Services can be found in Home Assistant under "Developer Tools > Services" search for "notification" to find your device/phone name.') }}</p>
             <p>{{ $t("Automations can optionally be triggered in Home Assistant:") }}</p>
             <p>
                 {{ $t("Trigger type:") }} <code>Event</code><br />
diff --git a/src/components/notifications/SMSManager.vue b/src/components/notifications/SMSManager.vue
index 25db624f..1be952ae 100644
--- a/src/components/notifications/SMSManager.vue
+++ b/src/components/notifications/SMSManager.vue
@@ -2,7 +2,7 @@
     <div class="mb-3">
         <label for="smsmanager-key" class="form-label">API Key</label>
         <div class="form-text">
-            {{ $t("SMSManager API Docs ") }}
+            {{ $t("SMSManager API Docs") }}
             <a href="https://smsmanager.cz/api/http#send" target="_blank">{{ $t("here") }}</a>
         </div>
         <input id="smsmanager-key" v-model="$parent.notification.smsmanagerApiKey" type="text" class="form-control">
diff --git a/src/languages/en.js b/src/languages/en.js
index 4bf92e92..e277958b 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -584,4 +584,10 @@ export default {
     backupRecommend: "Please backup the volume or the data folder (./data/) directly instead.",
     "Optional": "Optional",
     squadcast: "Squadcast",
+    SendKey: "SendKey",
+    "SMSManager API Docs": "SMSManager API Docs ",
+    "Gateway Type": "Gateway Type",
+    SMSManager: "SMSManager",
+    "You can divide numbers with": "You can divide numbers with",
+    "or": "or",
 };
diff --git a/src/languages/zh-CN.js b/src/languages/zh-CN.js
index a37d4ae4..0d21efda 100644
--- a/src/languages/zh-CN.js
+++ b/src/languages/zh-CN.js
@@ -581,4 +581,18 @@ export default {
     "Then choose an action, for example switch the scene to where an RGB light is red.": "然后您可以选择关联操作,例如切换到 RGB 灯发出红光的场景",
     "Frontend Version": "前端版本",
     "Frontend Version do not match backend version!": "前端版本与后端版本不符!",
+    "Base URL": "API 基础地址",
+    goAlertInfo: "GoAlert 是一个用于呼叫调度、自动汇报和通知(如 SMS 或语音呼叫)的开源应用程序。在正确的时间以正确的方式自动让正确的人参与!{0}",
+    goAlertIntegrationKeyInfo: "使用形如 aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee 的通用 API 集成密钥,通常是复制来的链接中的 token 参数值。",
+    goAlert: "GoAlert",
+    backupOutdatedWarning: "已弃用:由于大量新功能的加入,以及备份功能没有时时维护,现在备份功能已经无法生成完整的备份和恢复完整的设置。",
+    backupRecommend: "请改为直接备份 docker 卷或者数据文件夹(./data/)。",
+    Optional: "可选的",
+    squadcast: "Squadcast",
+    SendKey: "SendKey",
+    "SMSManager API Docs": "SMSManager API 文档在",
+    "Gateway Type": "网关类型",
+    SMSManager: "SMSManager",
+    "You can divide numbers with": "可用的分隔符:",
+    "or": "或",
 };

From 59e7aa74a3a7c542b06d05b963216c4246a65b06 Mon Sep 17 00:00:00 2001
From: Vasilis The Pikachu <vascreeper@yahoo.com>
Date: Fri, 7 Oct 2022 17:51:45 +0000
Subject: [PATCH 095/134] Remove some double dots & fix backuprecommended

---
 src/languages/el-GR.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/languages/el-GR.js b/src/languages/el-GR.js
index 92a2d775..c520a607 100644
--- a/src/languages/el-GR.js
+++ b/src/languages/el-GR.js
@@ -141,9 +141,9 @@ export default {
     Events: "Γεγονότα",
     Heartbeats: "Παλμοι καρδιας",
     "Auto Get": "Αυτόματη λήψη",
-    backupDescription: "Μπορείτε να δημιουργήσετε αντίγραφα ασφαλείας γία ολλες της παρακολούθησης και ειδοποιήσης σε ένα αρχείο JSON..",
+    backupDescription: "Μπορείτε να δημιουργήσετε αντίγραφα ασφαλείας γία ολλες της παρακολούθησης και ειδοποιήσης σε ένα αρχείο JSON.",
     backupDescription2: "Σημείωση: δεν περιλαμβάνονται δεδομένα ιστορικού και συμβάντων.",
-    backupDescription3: "Στο αρχείο εξαγωγής περιλαμβάνονται ευαίσθητα δεδομένα, όπως token ειδοποιήσεων. Aποθηκεύστε την εξαγωγή με ασφάλεια..",
+    backupDescription3: "Στο αρχείο εξαγωγής περιλαμβάνονται ευαίσθητα δεδομένα, όπως token ειδοποιήσεων. Aποθηκεύστε την εξαγωγή με ασφάλεια.",
     alertNoFile: "Επιλέξτε ένα αρχείο για εισαγωγή.",
     alertWrongFileType: "Επιλέξτε ένα αρχείο JSON.",
     "Clear all statistics": "Εκκαθάριση όλων των στατιστικών",
@@ -581,5 +581,5 @@ export default {
     goAlertIntegrationKeyInfo: "Λάβετε το generic API integration key για την υπηρεσία σε αυτήν τη μορφή \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" συνήθως την τιμή της παραμέτρου διακριτικού της αντιγραμμένης διεύθυνσης URL.",
     goAlert: "GoAlert",
     backupOutdatedWarning: "Καταργήθηκε: Επειδή προστέθηκαν πολλές δυνατότητες και αυτή η δυνατότητα δημιουργίας αντιγράφων ασφαλείας δεν διατηρείται πολη, δεν μπορεί να δημιουργήσει ή να επαναφέρει ένα πλήρες αντίγραφο ασφαλείας.",
-    backupRecommend: "Παρακαλούμε δημιουργήστε αντίγραφα ασφαλείας του τόμου ή του φακέλου δεδομένων (./data/) απευθείας.",
+    backupRecommend: "Παρακαλούμε δημιουργήστε αντίγραφα ασφαλείας του volume ή του φακέλου δεδομένων (./data/) απευθείας.",
 };

From df2f5368459cccab1a4713c11aaf3f51273dfc4c Mon Sep 17 00:00:00 2001
From: wellart <wella.design@gmail.com>
Date: Sat, 8 Oct 2022 01:15:03 +0700
Subject: [PATCH 096/134] [id-ID.js] Fix some type and word (#2149)

Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 src/languages/id-ID.js | 36 ++++++++++++++++++------------------
 1 file changed, 18 insertions(+), 18 deletions(-)

diff --git a/src/languages/id-ID.js b/src/languages/id-ID.js
index 3d3c3389..f8ee3ab7 100644
--- a/src/languages/id-ID.js
+++ b/src/languages/id-ID.js
@@ -126,10 +126,10 @@ export default {
     "Resolver Server": "Resolver Server",
     "Resource Record Type": "Resource Record Type",
     "Last Result": "Hasil Terakhir",
-    "Create your admin account": "Buat admin akun Anda",
+    "Create your admin account": "Buat akun admin anda",
     "Repeat Password": "Ulangi Sandi",
     "Import Backup": "Impor Cadangan",
-    "Export Backup": "Expor Cadangan",
+    "Export Backup": "Ekspor Cadangan",
     Export: "Ekspor",
     Import: "Impor",
     respTime: "Tanggapan. Waktu (milidetik)",
@@ -217,7 +217,7 @@ export default {
     smtpBCC: "BCC",
     discord: "Discord",
     "Discord Webhook URL": "Discord Webhook URL",
-    wayToGetDiscordURL: "Anda bisa mendapatkan ini dengan pergi ke Server Settings -> Integrations -> Create Webhook",
+    wayToGetDiscordURL: "Anda bisa mendapatkan ini dengan pergi ke Server Pengaturan -> Integrasi -> Buat Webhook",
     "Bot Display Name": "Nama Bot",
     "Prefix Custom Message": "Awalan Pesan",
     "Hello @everyone is...": "Halo {'@'}everyone is...",
@@ -328,7 +328,7 @@ export default {
     "Pick a RR-Type...": "Pilih RR-Type...",
     "Pick Accepted Status Codes...": "Pilih Kode Status yang Diterima...",
     Default: "Default",
-    "HTTP Options": "HTTP Options",
+    "HTTP Options": "Opsi HTTP",
     "Create Incident": "Buat Incident",
     Title: "Judul",
     Content: "Konten",
@@ -379,8 +379,8 @@ export default {
     smtpDkimheaderFieldNames: "Header Keys untuk ditambahkan (Optional)",
     smtpDkimskipFields: "Header Keys not untuk ditambahkan (Optional)",
     wayToGetPagerDutyKey: "Anda dapat menambahkan melalui Service -> Service Directory -> (Select a service) -> Integrations -> Add integration. Lalu Anda dapat menjadi dengan kata kunci \"Events API V2\". Informasi tambahan {0}",
-    "Integration Key": "Integration Key",
-    "Integration URL": "Integration URL",
+    "Integration Key": "Kunci Integrasi",
+    "Integration URL": "URL Integrasi",
     "Auto resolve or acknowledged": "Penyelesaian otomatis atau diakui",
     "do nothing": "tidak melakukan apapun",
     "auto acknowledged": "otomatis diakui",
@@ -402,14 +402,14 @@ export default {
     enableProxyDescription: "Proxy berikut tidak akan berdampak ke monitor hingga diaktifkan. Anda dapat mengontrol menonaktifkan sementara proxy dari semua monitor dengan status aktivasi.",
     setAsDefaultProxyDescription: "Proxy berikut akan diaktifkan sebagai bawaan untuk monitor baru. Anda masih dapat menonaktifkan proxy secara terpisah untuk setiap monitor.",
     "Certificate Chain": "Certificate Chain",
-    Valid: "Sahih",
+    Valid: "Valid",
     Invalid: "Tidak Valid",
     AccessKeyId: "AccessKey ID",
     SecretAccessKey: "AccessKey Secret",
     PhoneNumbers: "Nomor Telepon",
     TemplateCode: "Kode Template",
     SignName: "Nama Tanda",
-    "Sms template must contain parameters: ": "Template SMS harus memuat parameter: ",
+    "Sms template must contain parameters: ": "Template SMS harus berisi parameter: ",
     "Bark Endpoint": "Bark Endpoint",
     "Bark Group": "Bark Group",
     "Bark Sound": "Bark Sound",
@@ -432,7 +432,7 @@ export default {
     User: "Pengguna",
     Installed: "Terpasang",
     "Not installed": "Tidak terpasang",
-    Running: "Berlari",
+    Running: "Berjalan",
     "Not running": "Tidak berjalan",
     "Remove Token": "Hapus Token",
     Start: "Mulai",
@@ -445,7 +445,7 @@ export default {
     "No consecutive dashes": "Tanda hubung tidak berurutan",
     Next: "Selanjutnya",
     "The slug is already taken. Please choose another slug.": "Slug telah digunakan. Silakan pilih slug lain.",
-    "No Proxy": "TIdak ada Proxy",
+    "No Proxy": "Tidak ada Proxy",
     Authentication: "Autentikasi",
     "HTTP Basic Auth": "HTTP Basic Auth",
     "New Status Page": "Halaman Status Baru",
@@ -457,7 +457,7 @@ export default {
     cloudflareWebsite: "Situs Cloudflare",
     "Message:": "Pesan:",
     "Don't know how to get the token? Please read the guide:": "Tidak tahu cara mendapatkan token? Silakan baca panduannya:",
-    "The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Koneksi saat ini mungkin hilang jika Anda saat ini terhubung melalui CloudflareTunnel. Apakah Anda yakin ingin menghentikannya? Ketik kata sandi Anda saat ini untuk mengonfirmasinya.",
+    "The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Koneksi saat ini mungkin hilang jika Anda saat ini terhubung melalui Cloudflare Tunel. Apakah Anda yakin ingin menghentikannya? Ketik kata sandi Anda saat ini untuk mengonfirmasinya.",
     "HTTP Headers": "HTTP Headers",
     "Trust Proxy": "Proxy Terpercaya",
     "Other Software": "Perangkat Lunak lainnya",
@@ -494,7 +494,7 @@ export default {
     "Certificate Expiry Notification": "Pemberitahuan Kedaluwarsa Sertifikat",
     "API Username": "Nama Pengguna API",
     "API Key": "Kunci API",
-    "Recipient Number": "Nomor Penerima Recipient Number",
+    "Recipient Number": "Nomor Penerima",
     "From Name/Number": "Dari Nama/Nomor",
     "Leave blank to use a shared sender number.": "Biarkan kosong untuk menggunakan nomor pengirim bersama.",
     "Octopush API Version": "Versi API Octopush",
@@ -542,9 +542,9 @@ export default {
     "Go back to the previous page.": "Kembali ke halaman sebelumnya.",
     "Coming Soon": "Segera",
     wayToGetClickSendSMSToken: "Anda bisa mendapatkan Nama Pengguna API dan Kunci API dari {0} .",
-    "Connection String": "Connection String",
+    "Connection String": "String Koneksi",
     Query: "Query",
-    settingsCertificateExpiry: "Kedaluwarsa Sertifikat TLS",
+    settingsCertificateExpiry: "Sertifikat TLS Kadaluarsa",
     certificationExpiryDescription: "Monitor HTTPS memicu pemberitahuan saat sertifikat TLS kedaluwarsa dalam:",
     "Setup Docker Host": "Siapkan Host Docker",
     "Connection Type": "Jenis Koneksi",
@@ -570,9 +570,9 @@ export default {
     "default: notify all devices": "bawaan: notifikasi seluruh perangkat",
     "A listof Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Daftar Layanan Pemberitahuan dapat ditemukan di Home Assistant pada \"Developer Tools > Services\" cari \"notification\" lalu cari nama perangkat Anda.",
     "Automations can optionally be triggered in Home Assistant:": "Otomatisasi dapat dipicu secara opsional di Home Assistant:",
-    "Trigger type:": "Trigger type:",
-    "Event type:": "Event type:",
-    "Event data:": "Event data:",
+    "Trigger type:": "Tipe Trigger/Pemicu:",
+    "Event type:": "Tipe event:",
+    "Event data:": "Data event:",
     "Then choose an action, for example switch the scene to where an RGB light is red.": "Kemudian pilih tindakan, misalnya alihkan ke tempat dimana lampu RGB berwarna merah.",
     "Frontend Version": "Versi Frontend",
     "Frontend Version do not match backend version!": "Versi Frontend tidak sama dengan versi backend!",
@@ -580,6 +580,6 @@ export default {
     goAlertInfo: "GoAlert adalah aplikasi open source untuk penjadwalan panggilan, eskalasi otomatis dan pemberitahuan (seperti SMS atau panggilan suara). Secara otomatis melibatkan orang yang tepat, dengan cara yang benar, dan pada waktu yang tepat! {0}",
     goAlertIntegrationKeyInfo: "Dapatkan kunci integrasi API generik untuk layanan dalam format ini \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" biasanya nilai parameter token dari URL yang disalin.",
     goAlert: "GoAlert",
-    backupOutdatedWarning: "Usang: Karena banyak fitur ditambahkan dan fitur cadangan ini agak tidak terawat, itu tidak dapat menghasilkan atau memulihkan cadangan lengkap.",
+    backupOutdatedWarning: "Tidak digunakan lagi: Karena banyak fitur ditambahkan dan fitur cadangan ini agak tidak terawat, itu tidak dapat menghasilkan atau memulihkan cadangan lengkap.",
     backupRecommend: "Harap cadangkan volume atau folder data (./data/) secara langsung.",
 };

From f67d7cdf3f3edf66415747bde53c944a600df57d Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Sat, 8 Oct 2022 08:01:47 +0100
Subject: [PATCH 097/134] Make update-language-files command more useful
 (#2198)

* [empty commit] pull request for Fix language update script

* Avoid mass changes with update-language-files

This commit updates the update-language-files script to prevent mass
changes as seen on a number of recent PRs where the contributer has
ran the script and comitted the results.
The script has been updated to now require the --language argument to
specify which language file to update. This ensures that only that file
is updated instead of all files. If the provided language code does not
already exist, a new file with that code is created. This should make
it easier to add new languages as you only need to pass the language
code to the script.
The base lang code is now also passed as an optional argument to negate
the need for a seperate script entry in package.json.
The script has been restructures into a couple of functions to make it
easier to understand.
ESlint now only checks the changed file instead of
them all in order to improve performance.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>

* Updated translation docs for new command

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>

* [update-language-files] Add cross-env-shell

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
---
 extra/update-language-files/index.js | 77 ++++++++++++++++------------
 package.json                         |  3 +-
 src/languages/README.md              | 13 +++--
 3 files changed, 52 insertions(+), 41 deletions(-)

diff --git a/extra/update-language-files/index.js b/extra/update-language-files/index.js
index e449fe34..078c4e6f 100644
--- a/extra/update-language-files/index.js
+++ b/extra/update-language-files/index.js
@@ -1,51 +1,45 @@
 // Need to use ES6 to read language files
 
 import fs from "fs";
-import path from "path";
 import util from "util";
 import rmSync from "../fs-rmSync.js";
 
-// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
 /**
- * Look ma, it's cp -R.
- * @param {string} src  The path to the thing to copy.
- * @param {string} dest The path to the new copy.
+ * Copy across the required language files
+ * Creates a local directory (./languages) and copies the required files
+ * into it.
+ * @param {string} langCode Code of language to update. A file will be
+ * created with this code if one does not already exist
+ * @param {string} baseLang The second base language file to copy. This
+ * will be ignored if set to "en" as en.js is copied by default
  */
-const copyRecursiveSync = function (src, dest) {
-    let exists = fs.existsSync(src);
-    let stats = exists && fs.statSync(src);
-    let isDirectory = exists && stats.isDirectory();
+function copyFiles(langCode, baseLang) {
+    if (fs.existsSync("./languages")) {
+        rmSync("./languages", { recursive: true });
+    }
+    fs.mkdirSync("./languages");
 
-    if (isDirectory) {
-        fs.mkdirSync(dest);
-        fs.readdirSync(src).forEach(function (childItemName) {
-            copyRecursiveSync(path.join(src, childItemName),
-                path.join(dest, childItemName));
-        });
+    if (!fs.existsSync(`../../src/languages/${langCode}.js`)) {
+        fs.closeSync(fs.openSync(`./languages/${langCode}.js`, "a"));
     } else {
-        fs.copyFileSync(src, dest);
+        fs.copyFileSync(`../../src/languages/${langCode}.js`, `./languages/${langCode}.js`);
+    }
+    fs.copyFileSync("../../src/languages/en.js", "./languages/en.js");
+    if (baseLang !== "en") {
+        fs.copyFileSync(`../../src/languages/${baseLang}.js`, `./languages/${baseLang}.js`);
     }
-};
-
-console.log("Arguments:", process.argv);
-const baseLangCode = process.argv[2] || "en";
-console.log("Base Lang: " + baseLangCode);
-if (fs.existsSync("./languages")) {
-    rmSync("./languages", { recursive: true });
 }
-copyRecursiveSync("../../src/languages", "./languages");
 
-const en = (await import("./languages/en.js")).default;
-const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
-const files = fs.readdirSync("./languages");
-console.log("Files:", files);
-
-for (const file of files) {
-    if (! file.endsWith(".js")) {
-        console.log("Skipping " + file);
-        continue;
-    }
+/**
+ * Update the specified language file
+ * @param {string} langCode Language code to update
+ * @param {string} baseLang Second language to copy keys from
+ */
+async function updateLanguage(langCode, baseLangCode) {
+    const en = (await import("./languages/en.js")).default;
+    const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
 
+    let file = langCode + ".js";
     console.log("Processing " + file);
     const lang = await import("./languages/" + file);
 
@@ -83,5 +77,20 @@ for (const file of files) {
     fs.writeFileSync(`../../src/languages/${file}`, code);
 }
 
+// Get command line arguments
+const baseLangCode = process.env.npm_config_baselang || "en";
+const langCode = process.env.npm_config_language;
+
+// We need the file to edit
+if (langCode == null) {
+    throw new Error("Argument --language=<code> must be provided");
+}
+
+console.log("Base Lang: " + baseLangCode);
+console.log("Updating: " + langCode);
+
+copyFiles(langCode, baseLangCode);
+await updateLanguage(langCode, baseLangCode);
 rmSync("./languages", { recursive: true });
+
 console.log("Done. Fixing formatting by ESLint...");
diff --git a/package.json b/package.json
index d478a1d2..f5f78f3a 100644
--- a/package.json
+++ b/package.json
@@ -51,8 +51,7 @@
         "test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
         "simple-dns-server": "node extra/simple-dns-server.js",
         "simple-mqtt-server": "node extra/simple-mqtt-server.js",
-        "update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
-        "update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
+        "update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
         "ncu-patch": "npm-check-updates -u -t patch",
         "release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
         "release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta .  --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
diff --git a/src/languages/README.md b/src/languages/README.md
index d505476a..eddd6102 100644
--- a/src/languages/README.md
+++ b/src/languages/README.md
@@ -1,10 +1,13 @@
 # How to translate
 
 1. Fork this repo.
-2. Create a language file (e.g. `zh-TW.js`). The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm
-3. Run `npm run update-language-files`. You can also use this command to check if there are new strings to translate for your language.
-4. Your language file should be filled in. You can translate now.
-5. Add it into `languageList` constant.
-6. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
+2. Run `npm run update-language-files --language=<code>` where `<code>`
+   is a valid ISO language code:
+   http://www.lingoes.net/en/translator/langcode.htm. You can also use
+   this command to check if there are new strings to
+   translate for your language.
+3. Your language file should be filled in. You can translate now.
+4. Add it into `languageList` constant.
+5. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
 
 If you do not have programming skills, let me know in [the issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏

From 230e5110b1db0527c763daa680fa556a06d3bd9b Mon Sep 17 00:00:00 2001
From: Kevin Falentio <f@lentio.my.id>
Date: Sat, 8 Oct 2022 19:59:22 +0700
Subject: [PATCH 098/134] Fix typo in id-ID language file (#2202)

---
 src/languages/id-ID.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/languages/id-ID.js b/src/languages/id-ID.js
index f8ee3ab7..fe5d3594 100644
--- a/src/languages/id-ID.js
+++ b/src/languages/id-ID.js
@@ -565,7 +565,7 @@ export default {
     Examples: "Contoh",
     "Home Assistant URL": "Home Assistant URL",
     "Long-Lived Access Token": "Token Akses Berumur Panjang",
-    "Long-Lived Access Token canbe created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Token Akses Berumur Panjang dapat dibuat dengan mengklik nama profil Anda (kiri bawah) dan menggulir ke bawah lalu klik Buat Token. ",
+    "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Token Akses Berumur Panjang dapat dibuat dengan mengklik nama profil Anda (kiri bawah) dan menggulir ke bawah lalu klik Buat Token. ",
     "Notification Service": "Layanan Pemberitahuan",
     "default: notify all devices": "bawaan: notifikasi seluruh perangkat",
     "A listof Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Daftar Layanan Pemberitahuan dapat ditemukan di Home Assistant pada \"Developer Tools > Services\" cari \"notification\" lalu cari nama perangkat Anda.",

From ad1a7c255f214d2ce0be9d71d0d232d246fb64f9 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 8 Oct 2022 23:56:58 +0800
Subject: [PATCH 099/134] Drop exports.entryPage fully

---
 server/server.js | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/server/server.js b/server/server.js
index 620e5bb4..2efad753 100644
--- a/server/server.js
+++ b/server/server.js
@@ -127,6 +127,7 @@ const StatusPage = require("./model/status_page");
 const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
 const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
 const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
+const { Settings } = require("./settings");
 
 app.use(express.json());
 
@@ -155,9 +156,7 @@ let needSetup = false;
     Database.init(args);
     await initDatabase(testMode);
 
-    const entryPage = (await getSettings("general"))["entryPage"];
-    exports.entryPage = entryPage;
-    UptimeKumaServer.getInstance().entryPage = entryPage;
+    server.entryPage = await Settings.get("entryPage");
     await StatusPage.loadDomainMappingList();
 
     log.info("server", "Adding route");
@@ -178,7 +177,7 @@ let needSetup = false;
 
         log.debug("entry", `Request Domain: ${hostname}`);
 
-        const uptimeKumaEntryPage = UptimeKumaServer.getInstance().entryPage;
+        const uptimeKumaEntryPage = server.entryPage;
         if (hostname in StatusPage.domainMappingList) {
             log.debug("entry", "This is a status page domain");
 
@@ -1087,8 +1086,7 @@ let needSetup = false;
                 }
 
                 await setSettings("general", data);
-                exports.entryPage = data.entryPage;
-                UptimeKumaServer.getInstance().entryPage = data.entryPage;
+                server.entryPage = data.entryPage;
 
                 callback({
                     ok: true,

From 1c8631af8d5b3547f1fff5bea1ee3da75b7ee45f Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sun, 9 Oct 2022 16:02:47 +0800
Subject: [PATCH 100/134] Pin dependencies (#2205)

---
 package-lock.json | 48 +++++++++++++++++++++++------------------------
 package.json      | 48 +++++++++++++++++++++++------------------------
 2 files changed, 48 insertions(+), 48 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 4f222171..6053fa5f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,47 +12,47 @@
                 "@louislam/sqlite3": "~15.0.6",
                 "args-parser": "~1.3.0",
                 "axios": "~0.27.0",
-                "axios-ntlm": "^1.3.0",
-                "badge-maker": "^3.3.1",
+                "axios-ntlm": "~1.3.0",
+                "badge-maker": "~3.3.1",
                 "bcryptjs": "~2.4.3",
                 "bree": "~7.1.5",
                 "cacheable-lookup": "~6.0.4",
-                "chardet": "^1.3.0",
+                "chardet": "~1.4.0",
                 "check-password-strength": "^2.0.5",
-                "cheerio": "^1.0.0-rc.10",
-                "chroma-js": "^2.1.2",
+                "cheerio": "~1.0.0-rc.12",
+                "chroma-js": "~2.4.2",
                 "command-exists": "~1.2.9",
                 "compare-versions": "~3.6.0",
-                "compression": "^1.7.4",
-                "dayjs": "^1.11.0",
+                "compression": "~1.7.4",
+                "dayjs": "~1.11.5",
                 "express": "~4.17.3",
                 "express-basic-auth": "~1.2.1",
-                "express-static-gzip": "^2.1.7",
+                "express-static-gzip": "~2.1.7",
                 "form-data": "~4.0.0",
                 "http-graceful-shutdown": "~3.1.7",
-                "http-proxy-agent": "^5.0.0",
-                "https-proxy-agent": "^5.0.0",
-                "iconv-lite": "^0.6.3",
-                "jsesc": "^3.0.2",
+                "http-proxy-agent": "~5.0.0",
+                "https-proxy-agent": "~5.0.1",
+                "iconv-lite": "~0.6.3",
+                "jsesc": "~3.0.2",
                 "jsonwebtoken": "~8.5.1",
-                "jwt-decode": "^3.1.2",
-                "limiter": "^2.1.0",
-                "mqtt": "^4.2.8",
-                "mssql": "^8.1.0",
+                "jwt-decode": "~3.1.2",
+                "limiter": "~2.1.0",
+                "mqtt": "~4.3.7",
+                "mssql": "~8.1.4",
                 "node-cloudflared-tunnel": "~1.0.9",
-                "node-radius-client": "^1.0.0",
+                "node-radius-client": "~1.0.0",
                 "nodemailer": "~6.6.5",
                 "notp": "~2.0.3",
                 "password-hash": "~1.2.2",
-                "pg": "^8.7.3",
-                "pg-connection-string": "^2.5.0",
+                "pg": "~8.8.0",
+                "pg-connection-string": "~2.5.0",
                 "prom-client": "~13.2.0",
                 "prometheus-api-metrics": "~3.2.1",
                 "redbean-node": "0.1.4",
                 "socket.io": "~4.4.1",
                 "socket.io-client": "~4.4.1",
                 "socks-proxy-agent": "6.1.1",
-                "tar": "^6.1.11",
+                "tar": "~6.1.11",
                 "tcp-ping": "~0.1.1",
                 "thirty-two": "~1.0.2"
             },
@@ -82,18 +82,18 @@
                 "dns2": "~2.0.1",
                 "eslint": "~8.14.0",
                 "eslint-plugin-vue": "~8.7.1",
-                "favico.js": "^0.3.10",
+                "favico.js": "~0.3.10",
                 "jest": "~27.2.5",
                 "postcss-html": "~1.5.0",
                 "postcss-rtlcss": "~3.7.2",
                 "postcss-scss": "~4.0.4",
-                "prismjs": "^1.27.0",
+                "prismjs": "~1.29.0",
                 "qrcode": "~1.5.0",
                 "rollup-plugin-visualizer": "^5.6.0",
                 "sass": "~1.42.1",
                 "stylelint": "~14.7.1",
                 "stylelint-config-standard": "~25.0.0",
-                "terser": "^5.15.0",
+                "terser": "~5.15.0",
                 "timezones-list": "~3.0.1",
                 "typescript": "~4.4.4",
                 "v-pagination-3": "~0.1.7",
@@ -106,7 +106,7 @@
                 "vue-i18n": "~9.1.9",
                 "vue-image-crop-upload": "~3.0.3",
                 "vue-multiselect": "~3.0.0-alpha.2",
-                "vue-prism-editor": "^2.0.0-alpha.2",
+                "vue-prism-editor": "~2.0.0-alpha.2",
                 "vue-qrcode": "~1.0.0",
                 "vue-router": "~4.0.14",
                 "vue-toastification": "~2.0.0-rc.5",
diff --git a/package.json b/package.json
index f5f78f3a..0c72722d 100644
--- a/package.json
+++ b/package.json
@@ -66,47 +66,47 @@
         "@louislam/sqlite3": "~15.0.6",
         "args-parser": "~1.3.0",
         "axios": "~0.27.0",
-        "axios-ntlm": "^1.3.0",
-        "badge-maker": "^3.3.1",
+        "axios-ntlm": "~1.3.0",
+        "badge-maker": "~3.3.1",
         "bcryptjs": "~2.4.3",
         "bree": "~7.1.5",
         "cacheable-lookup": "~6.0.4",
-        "chardet": "^1.3.0",
+        "chardet": "~1.4.0",
         "check-password-strength": "^2.0.5",
-        "cheerio": "^1.0.0-rc.10",
-        "chroma-js": "^2.1.2",
+        "cheerio": "~1.0.0-rc.12",
+        "chroma-js": "~2.4.2",
         "command-exists": "~1.2.9",
         "compare-versions": "~3.6.0",
-        "compression": "^1.7.4",
-        "dayjs": "^1.11.0",
+        "compression": "~1.7.4",
+        "dayjs": "~1.11.5",
         "express": "~4.17.3",
         "express-basic-auth": "~1.2.1",
-        "express-static-gzip": "^2.1.7",
+        "express-static-gzip": "~2.1.7",
         "form-data": "~4.0.0",
         "http-graceful-shutdown": "~3.1.7",
-        "http-proxy-agent": "^5.0.0",
-        "https-proxy-agent": "^5.0.0",
-        "iconv-lite": "^0.6.3",
-        "jsesc": "^3.0.2",
+        "http-proxy-agent": "~5.0.0",
+        "https-proxy-agent": "~5.0.1",
+        "iconv-lite": "~0.6.3",
+        "jsesc": "~3.0.2",
         "jsonwebtoken": "~8.5.1",
-        "jwt-decode": "^3.1.2",
-        "limiter": "^2.1.0",
-        "mqtt": "^4.2.8",
-        "mssql": "^8.1.0",
+        "jwt-decode": "~3.1.2",
+        "limiter": "~2.1.0",
+        "mqtt": "~4.3.7",
+        "mssql": "~8.1.4",
         "node-cloudflared-tunnel": "~1.0.9",
-        "node-radius-client": "^1.0.0",
+        "node-radius-client": "~1.0.0",
         "nodemailer": "~6.6.5",
         "notp": "~2.0.3",
         "password-hash": "~1.2.2",
-        "pg": "^8.7.3",
-        "pg-connection-string": "^2.5.0",
+        "pg": "~8.8.0",
+        "pg-connection-string": "~2.5.0",
         "prom-client": "~13.2.0",
         "prometheus-api-metrics": "~3.2.1",
         "redbean-node": "0.1.4",
         "socket.io": "~4.4.1",
         "socket.io-client": "~4.4.1",
         "socks-proxy-agent": "6.1.1",
-        "tar": "^6.1.11",
+        "tar": "~6.1.11",
         "tcp-ping": "~0.1.1",
         "thirty-two": "~1.0.2"
     },
@@ -136,18 +136,18 @@
         "dns2": "~2.0.1",
         "eslint": "~8.14.0",
         "eslint-plugin-vue": "~8.7.1",
-        "favico.js": "^0.3.10",
+        "favico.js": "~0.3.10",
         "jest": "~27.2.5",
         "postcss-html": "~1.5.0",
         "postcss-rtlcss": "~3.7.2",
         "postcss-scss": "~4.0.4",
-        "prismjs": "^1.27.0",
+        "prismjs": "~1.29.0",
         "qrcode": "~1.5.0",
         "rollup-plugin-visualizer": "^5.6.0",
         "sass": "~1.42.1",
         "stylelint": "~14.7.1",
         "stylelint-config-standard": "~25.0.0",
-        "terser": "^5.15.0",
+        "terser": "~5.15.0",
         "timezones-list": "~3.0.1",
         "typescript": "~4.4.4",
         "v-pagination-3": "~0.1.7",
@@ -160,7 +160,7 @@
         "vue-i18n": "~9.1.9",
         "vue-image-crop-upload": "~3.0.3",
         "vue-multiselect": "~3.0.0-alpha.2",
-        "vue-prism-editor": "^2.0.0-alpha.2",
+        "vue-prism-editor": "~2.0.0-alpha.2",
         "vue-qrcode": "~1.0.0",
         "vue-router": "~4.0.14",
         "vue-toastification": "~2.0.0-rc.5",

From 07f9aafd7bce1b6d5226744654070a2fc7d54745 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sun, 9 Oct 2022 16:50:47 +0800
Subject: [PATCH 101/134] Update to 1.18.4

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 0c72722d..180bea28 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "uptime-kuma",
-    "version": "1.18.3",
+    "version": "1.18.4",
     "license": "MIT",
     "repository": {
         "type": "git",
@@ -38,7 +38,7 @@
         "build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
         "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
         "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
-        "setup": "git checkout 1.18.3 && npm ci --production && npm run download-dist",
+        "setup": "git checkout 1.18.4 && npm ci --production && npm run download-dist",
         "download-dist": "node extra/download-dist.js",
         "mark-as-nightly": "node extra/mark-as-nightly.js",
         "reset-password": "node extra/reset-password.js",

From a36f24d827f2dd37c247cee6a124495bbd0e3d19 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sun, 9 Oct 2022 20:59:58 +0800
Subject: [PATCH 102/134] Add configurable server timezone

---
 package-lock.json                   |  4 ++--
 package.json                        |  2 +-
 server/server.js                    | 24 ++++++++++++++++++------
 server/uptime-kuma-server.js        | 25 +++++++++++++++++++++++++
 src/components/settings/General.vue | 20 ++++++++++++++++++--
 src/util.js                         |  2 +-
 src/util.ts                         |  2 +-
 7 files changed, 66 insertions(+), 13 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 2fdcb272..fb50f275 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "uptime-kuma",
-    "version": "1.18.3",
+    "version": "1.18.4",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "uptime-kuma",
-            "version": "1.18.3",
+            "version": "1.18.4",
             "license": "MIT",
             "dependencies": {
                 "@louislam/sqlite3": "~15.0.6",
diff --git a/package.json b/package.json
index a77ea3d7..c3f7a886 100644
--- a/package.json
+++ b/package.json
@@ -123,7 +123,7 @@
         "@vitejs/plugin-legacy": "~2.1.0",
         "@vitejs/plugin-vue": "~3.1.0",
         "@vue/compiler-sfc": "~3.2.36",
-        "@vuepic/vue-datepicker": "^3.4.8",
+        "@vuepic/vue-datepicker": "~3.4.8",
         "aedes": "^0.46.3",
         "babel-plugin-rewire": "~1.2.0",
         "bootstrap": "5.1.3",
diff --git a/server/server.js b/server/server.js
index 1ad99899..f80d5d57 100644
--- a/server/server.js
+++ b/server/server.js
@@ -5,6 +5,11 @@
  */
 console.log("Welcome to Uptime Kuma");
 
+// As the log function need to use dayjs, it should be very top
+const dayjs = require("dayjs");
+dayjs.extend(require("dayjs/plugin/utc"));
+dayjs.extend(require("dayjs/plugin/timezone"));
+
 // Check Node.js Version
 const nodeVersion = parseInt(process.versions.node.split(".")[0]);
 const requiredVersion = 14;
@@ -34,10 +39,6 @@ const fs = require("fs");
 
 log.info("server", "Importing 3rd-party libraries");
 
-const dayjs = require("dayjs");
-dayjs.extend(require("dayjs/plugin/utc"));
-dayjs.extend(require("dayjs/plugin/timezone"));
-
 log.debug("server", "Importing express");
 const express = require("express");
 const expressStaticGzip = require("express-static-gzip");
@@ -160,6 +161,7 @@ let needSetup = false;
 (async () => {
     Database.init(args);
     await initDatabase(testMode);
+    await server.initAfterDatabaseReady();
 
     exports.entryPage = await setting("entryPage");
     await StatusPage.loadDomainMappingList();
@@ -1061,10 +1063,15 @@ let needSetup = false;
         socket.on("getSettings", async (callback) => {
             try {
                 checkLogin(socket);
+                const data = await getSettings("general");
+
+                if (!data.serverTimezone) {
+                    data.serverTimezone = await server.getTimezone();
+                }
 
                 callback({
                     ok: true,
-                    data: await getSettings("general"),
+                    data: data,
                 });
 
             } catch (e) {
@@ -1092,9 +1099,14 @@ let needSetup = false;
                 await setSettings("general", data);
                 exports.entryPage = data.entryPage;
 
+                // Also need to apply timezone globally
+                if (data.serverTimezone) {
+                    await server.setTimezone(data.serverTimezone);
+                }
+
                 callback({
                     ok: true,
-                    msg: "Saved"
+                    msg: "Saved " + dayjs()
                 });
 
                 sendInfo(socket);
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index 7de53fe6..15583159 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -9,6 +9,7 @@ const Database = require("./database");
 const util = require("util");
 const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
 const { Settings } = require("./settings");
+const dayjs = require("dayjs");
 
 /**
  * `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
@@ -84,6 +85,13 @@ class UptimeKumaServer {
         this.io = new Server(this.httpServer);
     }
 
+    async initAfterDatabaseReady() {
+        process.env.TZ = await this.getTimezone();
+        dayjs.tz.setDefault(process.env.TZ);
+        log.debug("DEBUG", "Timezone: " + process.env.TZ);
+        log.debug("DEBUG", "Current Time: " + dayjs.tz().format());
+    }
+
     async sendMonitorList(socket) {
         let list = await this.getMonitorJSONList(socket.userID);
         this.io.to(socket.userID).emit("monitorList", list);
@@ -184,6 +192,23 @@ class UptimeKumaServer {
             return clientIP.replace(/^.*:/, "");
         }
     }
+
+    async getTimezone() {
+        let timezone = await Settings.get("serverTimezone");
+        if (timezone) {
+            return timezone;
+        } else if (process.env.TZ) {
+            return process.env.TZ;
+        } else {
+            return dayjs.tz.guess();
+        }
+    }
+
+    async setTimezone(timezone) {
+        await Settings.set("serverTimezone", timezone, "general");
+        process.env.TZ = timezone;
+        dayjs.tz.setDefault(timezone);
+    }
 }
 
 module.exports = {
diff --git a/src/components/settings/General.vue b/src/components/settings/General.vue
index 242ad853..57c8e0ca 100644
--- a/src/components/settings/General.vue
+++ b/src/components/settings/General.vue
@@ -1,10 +1,10 @@
 <template>
     <div>
         <form class="my-4" @submit.prevent="saveGeneral">
-            <!-- Timezone -->
+            <!-- Client side Timezone -->
             <div class="mb-4">
                 <label for="timezone" class="form-label">
-                    {{ $t("Timezone") }}
+                    {{ $t("Display Timezone") }}
                 </label>
                 <select id="timezone" v-model="$root.userTimezone" class="form-select">
                     <option value="auto">
@@ -20,6 +20,22 @@
                 </select>
             </div>
 
+            <!-- Server Timezone -->
+            <div class="mb-4">
+                <label for="timezone" class="form-label">
+                    {{ $t("Server Timezone") }}
+                </label>
+                <select id="timezone" v-model="settings.serverTimezone" class="form-select">
+                    <option
+                        v-for="(timezone, index) in timezoneList"
+                        :key="index"
+                        :value="timezone.value"
+                    >
+                        {{ timezone.name }}
+                    </option>
+                </select>
+            </div>
+
             <!-- Search Engine -->
             <div class="mb-4">
                 <label class="form-label">
diff --git a/src/util.js b/src/util.js
index 1fee0eaa..2c914cb6 100644
--- a/src/util.js
+++ b/src/util.js
@@ -101,7 +101,7 @@ class Logger {
         }
         module = module.toUpperCase();
         level = level.toUpperCase();
-        const now = new Date().toISOString();
+        const now = dayjs.tz(new Date()).format();
         const formattedMessage = (typeof msg === "string") ? `${now} [${module}] ${level}: ${msg}` : msg;
         if (level === "INFO") {
             console.info(formattedMessage);
diff --git a/src/util.ts b/src/util.ts
index 16511afa..363bfc06 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -116,7 +116,7 @@ class Logger {
         module = module.toUpperCase();
         level = level.toUpperCase();
 
-        const now = new Date().toISOString();
+        const now = dayjs.tz(new Date()).format();
         const formattedMessage = (typeof msg === "string") ? `${now} [${module}] ${level}: ${msg}` : msg;
 
         if (level === "INFO") {

From a577fba84883937848e1b70e5c3ec348976b531b Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Mon, 10 Oct 2022 02:28:03 +0800
Subject: [PATCH 103/134] Change DateTime Range using serverTimezone

---
 server/client.js              |  6 ++++--
 server/model/maintenance.js   | 10 +++++-----
 src/languages/en.js           |  2 +-
 src/pages/EditMaintenance.vue |  8 ++++----
 src/util.js                   | 18 ++++++++++++++++--
 src/util.ts                   | 17 ++++++++++++++++-
 6 files changed, 46 insertions(+), 15 deletions(-)

diff --git a/server/client.js b/server/client.js
index a0c52e1e..795b3ad4 100644
--- a/server/client.js
+++ b/server/client.js
@@ -4,7 +4,8 @@
 const { TimeLogger } = require("../src/util");
 const { R } = require("redbean-node");
 const { UptimeKumaServer } = require("./uptime-kuma-server");
-const io = UptimeKumaServer.getInstance().io;
+const server = UptimeKumaServer.getInstance();
+const io = server.io;
 const { setting } = require("./util-server");
 const checkVersion = require("./check-version");
 
@@ -121,7 +122,8 @@ async function sendInfo(socket) {
     socket.emit("info", {
         version: checkVersion.version,
         latestVersion: checkVersion.latestVersion,
-        primaryBaseURL: await setting("primaryBaseURL")
+        primaryBaseURL: await setting("primaryBaseURL"),
+        serverTimezone: await server.getTimezone(),
     });
 }
 
diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index 3d0595a7..840267ef 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -1,5 +1,5 @@
 const { BeanModel } = require("redbean-node/dist/bean-model");
-const { parseTimeObject, parseTimeFromTimeObject, isoToUTCDateTime, utcToISODateTime } = require("../../src/util");
+const { parseTimeObject, parseTimeFromTimeObject, isoToUTCDateTime, utcToISODateTime, SQL_DATETIME_FORMAT, utcToLocal, localToUTC } = require("../../src/util");
 const { isArray } = require("chart.js/helpers");
 const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
 
@@ -15,9 +15,9 @@ class Maintenance extends BeanModel {
 
         let dateRange = [];
         if (this.start_date) {
-            dateRange.push(utcToISODateTime(this.start_date));
+            dateRange.push(utcToLocal(this.start_date));
             if (this.end_date) {
-                dateRange.push(utcToISODateTime(this.end_date));
+                dateRange.push(utcToLocal(this.end_date));
             }
         }
 
@@ -92,10 +92,10 @@ class Maintenance extends BeanModel {
         bean.active = obj.active;
 
         if (obj.dateRange[0]) {
-            bean.start_date = isoToUTCDateTime(obj.dateRange[0]);
+            bean.start_date = localToUTC(obj.dateRange[0]);
 
             if (obj.dateRange[1]) {
-                bean.end_date = isoToUTCDateTime(obj.dateRange[1]);
+                bean.end_date = localToUTC(obj.dateRange[1]);
             }
         }
 
diff --git a/src/languages/en.js b/src/languages/en.js
index 5a959f27..d6a65b04 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -616,7 +616,7 @@ export default {
     recurringInterval: "Interval",
     "Recurring": "Recurring",
     strategyManual: "Active/Inactive Manually",
-    warningTimezone: "It is using your current Device/PC's timezone.",
+    warningTimezone: "It is using the server's timezone",
     weekdayShortMon: "Mon",
     weekdayShortTue: "Tue",
     weekdayShortWed: "Wed",
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index be9f7ce4..946059cb 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -82,7 +82,7 @@
 
                             <h2 class="mt-5">{{ $t("Date and Time") }}</h2>
 
-                            <div>⚠️ {{ $t("warningTimezone") }}</div>
+                            <div>⚠️ {{ $t("warningTimezone") }}: {{ $root.info.serverTimezone }}</div>
 
                             <!-- Strategy -->
                             <div class="my-3">
@@ -105,10 +105,11 @@
                                     <Datepicker
                                         v-model="maintenance.dateRange"
                                         :dark="$root.isDark"
-                                        range textInput
+                                        range
                                         :monthChangeOnScroll="false"
                                         :minDate="minDate"
                                         format="yyyy-MM-dd HH:mm"
+                                        modelType="yyyy-MM-dd HH:mm:ss"
                                     />
                                 </div>
                             </template>
@@ -186,7 +187,6 @@
                                         :dark="$root.isDark"
                                         timePicker
                                         disableTimeRangeValidation range
-                                        textInput
                                     />
                                 </div>
 
@@ -196,7 +196,7 @@
                                     <Datepicker
                                         v-model="maintenance.dateRange"
                                         :dark="$root.isDark"
-                                        range textInput datePicker
+                                        range datePicker
                                         :monthChangeOnScroll="false"
                                         :minDate="minDate"
                                         :enableTimePicker="false"
diff --git a/src/util.js b/src/util.js
index 2c914cb6..ec2d2322 100644
--- a/src/util.js
+++ b/src/util.js
@@ -7,7 +7,7 @@
 // Backend uses the compiled file util.js
 // Frontend uses util.ts
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = 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;
+exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = 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");
 exports.isDev = process.env.NODE_ENV === "development";
 exports.appName = "Uptime Kuma";
@@ -19,6 +19,8 @@ exports.STATUS_PAGE_ALL_DOWN = 0;
 exports.STATUS_PAGE_ALL_UP = 1;
 exports.STATUS_PAGE_PARTIAL_DOWN = 2;
 exports.STATUS_PAGE_MAINTENANCE = 3;
+exports.SQL_DATE_FORMAT = "YYYY-MM-DD";
+exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
 /** Flip the status of s */
 function flipStatus(s) {
     if (s === exports.UP) {
@@ -351,7 +353,7 @@ function parseTimeFromTimeObject(obj) {
 }
 exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
 function isoToUTCDateTime(input) {
-    return dayjs(input).utc().format("YYYY-MM-DD HH:mm:ss");
+    return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
 }
 exports.isoToUTCDateTime = isoToUTCDateTime;
 /**
@@ -361,3 +363,15 @@ function utcToISODateTime(input) {
     return dayjs.utc(input).toISOString();
 }
 exports.utcToISODateTime = utcToISODateTime;
+/**
+/**
+ * For SQL_DATETIME_FORMAT
+ */
+function utcToLocal(input) {
+    return dayjs.utc(input).local().format(exports.SQL_DATETIME_FORMAT);
+}
+exports.utcToLocal = utcToLocal;
+function localToUTC(input) {
+    return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
+}
+exports.localToUTC = localToUTC;
diff --git a/src/util.ts b/src/util.ts
index 363bfc06..34e0905d 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -23,6 +23,9 @@ export const STATUS_PAGE_ALL_UP = 1;
 export const STATUS_PAGE_PARTIAL_DOWN = 2;
 export const STATUS_PAGE_MAINTENANCE = 3;
 
+export const SQL_DATE_FORMAT = "YYYY-MM-DD";
+export const SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
+
 /** Flip the status of s */
 export function flipStatus(s: number) {
     if (s === UP) {
@@ -396,7 +399,7 @@ export function parseTimeFromTimeObject(obj : any) {
 
 
 export function isoToUTCDateTime(input : string) {
-    return dayjs(input).utc().format("YYYY-MM-DD HH:mm:ss");
+    return dayjs(input).utc().format(SQL_DATETIME_FORMAT);
 }
 
 /**
@@ -405,3 +408,15 @@ export function isoToUTCDateTime(input : string) {
 export function utcToISODateTime(input : string) {
     return dayjs.utc(input).toISOString();
 }
+
+/**
+/**
+ * For SQL_DATETIME_FORMAT
+ */
+export function utcToLocal(input : string) {
+    return dayjs.utc(input).local().format(SQL_DATETIME_FORMAT);
+}
+
+export function localToUTC(input : string) {
+    return dayjs(input).utc().format(SQL_DATETIME_FORMAT);
+}

From 71af23cf0003c4a5086a9eb44abeabd1ccb8bdce Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Mon, 10 Oct 2022 02:47:24 +0800
Subject: [PATCH 104/134] Fix #2207

---
 server/model/status_page.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/model/status_page.js b/server/model/status_page.js
index 7682272c..68c7f8b0 100644
--- a/server/model/status_page.js
+++ b/server/model/status_page.js
@@ -58,7 +58,7 @@ class StatusPage extends BeanModel {
 
         // Preload data
         // Add jsesc, fix https://github.com/louislam/uptime-kuma/issues/2186
-        const escapedJSONObject = jsesc(JSON.stringify(await StatusPage.getStatusPageData(statusPage)), {
+        const escapedJSONObject = jsesc(await StatusPage.getStatusPageData(statusPage), {
             "isScriptContext": true
         });
 

From bd42450e55dc29bdd1daddb1080aaa6d81b32912 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Mon, 10 Oct 2022 16:23:32 +0800
Subject: [PATCH 105/134] Update vue-i18n from 9.1.9 to 9.2.2, force to use
 production version of vue-i18n in order to improve the performance

---
 package-lock.json | 186 +++++++++++++++++-----------------------------
 package.json      |   2 +-
 src/i18n.js       |   2 +-
 3 files changed, 71 insertions(+), 119 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 6053fa5f..44af1469 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "uptime-kuma",
-    "version": "1.18.3",
+    "version": "1.18.4",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "uptime-kuma",
-            "version": "1.18.3",
+            "version": "1.18.4",
             "license": "MIT",
             "dependencies": {
                 "@louislam/sqlite3": "~15.0.6",
@@ -103,7 +103,7 @@
                 "vue-chart-3": "3.0.9",
                 "vue-confirm-dialog": "~1.0.2",
                 "vue-contenteditable": "~3.0.4",
-                "vue-i18n": "~9.1.9",
+                "vue-i18n": "~9.2.2",
                 "vue-image-crop-upload": "~3.0.3",
                 "vue-multiselect": "~3.0.0-alpha.2",
                 "vue-prism-editor": "~2.0.0-alpha.2",
@@ -2338,92 +2338,65 @@
             "dev": true
         },
         "node_modules/@intlify/core-base": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.10.tgz",
-            "integrity": "sha512-So9CNUavB/IsZ+zBmk2Cv6McQp6vc2wbGi1S0XQmJ8Vz+UFcNn9MFXAe9gY67PreIHrbLsLxDD0cwo1qsxM1Nw==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.2.2.tgz",
+            "integrity": "sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==",
             "dev": true,
             "dependencies": {
-                "@intlify/devtools-if": "9.1.10",
-                "@intlify/message-compiler": "9.1.10",
-                "@intlify/message-resolver": "9.1.10",
-                "@intlify/runtime": "9.1.10",
-                "@intlify/shared": "9.1.10",
-                "@intlify/vue-devtools": "9.1.10"
+                "@intlify/devtools-if": "9.2.2",
+                "@intlify/message-compiler": "9.2.2",
+                "@intlify/shared": "9.2.2",
+                "@intlify/vue-devtools": "9.2.2"
             },
             "engines": {
-                "node": ">= 10"
+                "node": ">= 14"
             }
         },
         "node_modules/@intlify/devtools-if": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.10.tgz",
-            "integrity": "sha512-SHaKoYu6sog3+Q8js1y3oXLywuogbH1sKuc7NSYkN3GElvXSBaMoCzW+we0ZSFqj/6c7vTNLg9nQ6rxhKqYwnQ==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.2.2.tgz",
+            "integrity": "sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==",
             "dev": true,
             "dependencies": {
-                "@intlify/shared": "9.1.10"
+                "@intlify/shared": "9.2.2"
             },
             "engines": {
-                "node": ">= 10"
+                "node": ">= 14"
             }
         },
         "node_modules/@intlify/message-compiler": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.10.tgz",
-            "integrity": "sha512-+JiJpXff/XTb0EadYwdxOyRTB0hXNd4n1HaJ/a4yuV960uRmPXaklJsedW0LNdcptd/hYUZtCkI7Lc9J5C1gxg==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz",
+            "integrity": "sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==",
             "dev": true,
             "dependencies": {
-                "@intlify/message-resolver": "9.1.10",
-                "@intlify/shared": "9.1.10",
+                "@intlify/shared": "9.2.2",
                 "source-map": "0.6.1"
             },
             "engines": {
-                "node": ">= 10"
-            }
-        },
-        "node_modules/@intlify/message-resolver": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.10.tgz",
-            "integrity": "sha512-5YixMG/M05m0cn9+gOzd4EZQTFRUu8RGhzxJbR1DWN21x/Z3bJ8QpDYj6hC4FwBj5uKsRfKpJQ3Xqg98KWoA+w==",
-            "dev": true,
-            "engines": {
-                "node": ">= 10"
-            }
-        },
-        "node_modules/@intlify/runtime": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.10.tgz",
-            "integrity": "sha512-7QsuByNzpe3Gfmhwq6hzgXcMPpxz8Zxb/XFI6s9lQdPLPe5Lgw4U1ovRPZTOs6Y2hwitR3j/HD8BJNGWpJnOFA==",
-            "dev": true,
-            "dependencies": {
-                "@intlify/message-compiler": "9.1.10",
-                "@intlify/message-resolver": "9.1.10",
-                "@intlify/shared": "9.1.10"
-            },
-            "engines": {
-                "node": ">= 10"
+                "node": ">= 14"
             }
         },
         "node_modules/@intlify/shared": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.10.tgz",
-            "integrity": "sha512-Om54xJeo1Vw+K1+wHYyXngE8cAbrxZHpWjYzMR9wCkqbhGtRV5VLhVc214Ze2YatPrWlS2WSMOWXR8JktX/IgA==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
+            "integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==",
             "dev": true,
             "engines": {
-                "node": ">= 10"
+                "node": ">= 14"
             }
         },
         "node_modules/@intlify/vue-devtools": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.10.tgz",
-            "integrity": "sha512-5l3qYARVbkWAkagLu1XbDUWRJSL8br1Dj60wgMaKB0+HswVsrR6LloYZTg7ozyvM621V6+zsmwzbQxbVQyrytQ==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz",
+            "integrity": "sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==",
             "dev": true,
             "dependencies": {
-                "@intlify/message-resolver": "9.1.10",
-                "@intlify/runtime": "9.1.10",
-                "@intlify/shared": "9.1.10"
+                "@intlify/core-base": "9.2.2",
+                "@intlify/shared": "9.2.2"
             },
             "engines": {
-                "node": ">= 10"
+                "node": ">= 14"
             }
         },
         "node_modules/@istanbuljs/load-nyc-config": {
@@ -16107,18 +16080,18 @@
             }
         },
         "node_modules/vue-i18n": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.1.10.tgz",
-            "integrity": "sha512-jpr7gV5KPk4n+sSPdpZT8Qx3XzTcNDWffRlHV/cT2NUyEf+sEgTTmLvnBAibjOFJ0zsUyZlVTAWH5DDnYep+1g==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.2.2.tgz",
+            "integrity": "sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==",
             "dev": true,
             "dependencies": {
-                "@intlify/core-base": "9.1.10",
-                "@intlify/shared": "9.1.10",
-                "@intlify/vue-devtools": "9.1.10",
-                "@vue/devtools-api": "^6.0.0-beta.7"
+                "@intlify/core-base": "9.2.2",
+                "@intlify/shared": "9.2.2",
+                "@intlify/vue-devtools": "9.2.2",
+                "@vue/devtools-api": "^6.2.1"
             },
             "engines": {
-                "node": ">= 10"
+                "node": ">= 14"
             },
             "peerDependencies": {
                 "vue": "^3.0.0"
@@ -18279,71 +18252,50 @@
             "dev": true
         },
         "@intlify/core-base": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.10.tgz",
-            "integrity": "sha512-So9CNUavB/IsZ+zBmk2Cv6McQp6vc2wbGi1S0XQmJ8Vz+UFcNn9MFXAe9gY67PreIHrbLsLxDD0cwo1qsxM1Nw==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.2.2.tgz",
+            "integrity": "sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==",
             "dev": true,
             "requires": {
-                "@intlify/devtools-if": "9.1.10",
-                "@intlify/message-compiler": "9.1.10",
-                "@intlify/message-resolver": "9.1.10",
-                "@intlify/runtime": "9.1.10",
-                "@intlify/shared": "9.1.10",
-                "@intlify/vue-devtools": "9.1.10"
+                "@intlify/devtools-if": "9.2.2",
+                "@intlify/message-compiler": "9.2.2",
+                "@intlify/shared": "9.2.2",
+                "@intlify/vue-devtools": "9.2.2"
             }
         },
         "@intlify/devtools-if": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.10.tgz",
-            "integrity": "sha512-SHaKoYu6sog3+Q8js1y3oXLywuogbH1sKuc7NSYkN3GElvXSBaMoCzW+we0ZSFqj/6c7vTNLg9nQ6rxhKqYwnQ==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.2.2.tgz",
+            "integrity": "sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==",
             "dev": true,
             "requires": {
-                "@intlify/shared": "9.1.10"
+                "@intlify/shared": "9.2.2"
             }
         },
         "@intlify/message-compiler": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.10.tgz",
-            "integrity": "sha512-+JiJpXff/XTb0EadYwdxOyRTB0hXNd4n1HaJ/a4yuV960uRmPXaklJsedW0LNdcptd/hYUZtCkI7Lc9J5C1gxg==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz",
+            "integrity": "sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==",
             "dev": true,
             "requires": {
-                "@intlify/message-resolver": "9.1.10",
-                "@intlify/shared": "9.1.10",
+                "@intlify/shared": "9.2.2",
                 "source-map": "0.6.1"
             }
         },
-        "@intlify/message-resolver": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.10.tgz",
-            "integrity": "sha512-5YixMG/M05m0cn9+gOzd4EZQTFRUu8RGhzxJbR1DWN21x/Z3bJ8QpDYj6hC4FwBj5uKsRfKpJQ3Xqg98KWoA+w==",
-            "dev": true
-        },
-        "@intlify/runtime": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.10.tgz",
-            "integrity": "sha512-7QsuByNzpe3Gfmhwq6hzgXcMPpxz8Zxb/XFI6s9lQdPLPe5Lgw4U1ovRPZTOs6Y2hwitR3j/HD8BJNGWpJnOFA==",
-            "dev": true,
-            "requires": {
-                "@intlify/message-compiler": "9.1.10",
-                "@intlify/message-resolver": "9.1.10",
-                "@intlify/shared": "9.1.10"
-            }
-        },
         "@intlify/shared": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.10.tgz",
-            "integrity": "sha512-Om54xJeo1Vw+K1+wHYyXngE8cAbrxZHpWjYzMR9wCkqbhGtRV5VLhVc214Ze2YatPrWlS2WSMOWXR8JktX/IgA==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
+            "integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==",
             "dev": true
         },
         "@intlify/vue-devtools": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.10.tgz",
-            "integrity": "sha512-5l3qYARVbkWAkagLu1XbDUWRJSL8br1Dj60wgMaKB0+HswVsrR6LloYZTg7ozyvM621V6+zsmwzbQxbVQyrytQ==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz",
+            "integrity": "sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==",
             "dev": true,
             "requires": {
-                "@intlify/message-resolver": "9.1.10",
-                "@intlify/runtime": "9.1.10",
-                "@intlify/shared": "9.1.10"
+                "@intlify/core-base": "9.2.2",
+                "@intlify/shared": "9.2.2"
             }
         },
         "@istanbuljs/load-nyc-config": {
@@ -28795,15 +28747,15 @@
             }
         },
         "vue-i18n": {
-            "version": "9.1.10",
-            "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.1.10.tgz",
-            "integrity": "sha512-jpr7gV5KPk4n+sSPdpZT8Qx3XzTcNDWffRlHV/cT2NUyEf+sEgTTmLvnBAibjOFJ0zsUyZlVTAWH5DDnYep+1g==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.2.2.tgz",
+            "integrity": "sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==",
             "dev": true,
             "requires": {
-                "@intlify/core-base": "9.1.10",
-                "@intlify/shared": "9.1.10",
-                "@intlify/vue-devtools": "9.1.10",
-                "@vue/devtools-api": "^6.0.0-beta.7"
+                "@intlify/core-base": "9.2.2",
+                "@intlify/shared": "9.2.2",
+                "@intlify/vue-devtools": "9.2.2",
+                "@vue/devtools-api": "^6.2.1"
             }
         },
         "vue-image-crop-upload": {
diff --git a/package.json b/package.json
index 180bea28..6f48fedd 100644
--- a/package.json
+++ b/package.json
@@ -157,7 +157,7 @@
         "vue-chart-3": "3.0.9",
         "vue-confirm-dialog": "~1.0.2",
         "vue-contenteditable": "~3.0.4",
-        "vue-i18n": "~9.1.9",
+        "vue-i18n": "~9.2.2",
         "vue-image-crop-upload": "~3.0.3",
         "vue-multiselect": "~3.0.0-alpha.2",
         "vue-prism-editor": "~2.0.0-alpha.2",
diff --git a/src/i18n.js b/src/i18n.js
index 4c19eb00..902177cf 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -1,4 +1,4 @@
-import { createI18n } from "vue-i18n/index";
+import { createI18n } from "vue-i18n/dist/vue-i18n.esm-browser.prod.js";
 import en from "./languages/en";
 
 const languageList = {

From c1ccaa7a9feb3dfc9390039f86f183f077c78283 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Mon, 10 Oct 2022 20:48:11 +0800
Subject: [PATCH 106/134] WIP

---
 package-lock.json                             |  2 +-
 server/model/maintenance.js                   | 32 +++++---------
 server/model/maintenance_timeslot.js          |  6 +++
 .../maintenance-socket-handler.js             | 13 +++---
 server/util-server.js                         | 26 +++++++++--
 src/languages/en.js                           |  1 +
 src/pages/EditMaintenance.vue                 | 15 +++----
 src/pages/ManageMaintenance.vue               | 43 ++++++++++++++++++-
 8 files changed, 93 insertions(+), 45 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index f422d6a8..161208af 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -69,7 +69,7 @@
                 "@vitejs/plugin-legacy": "~2.1.0",
                 "@vitejs/plugin-vue": "~3.1.0",
                 "@vue/compiler-sfc": "~3.2.36",
-                "@vuepic/vue-datepicker": "^3.4.8",
+                "@vuepic/vue-datepicker": "~3.4.8",
                 "aedes": "^0.46.3",
                 "babel-plugin-rewire": "~1.2.0",
                 "bootstrap": "5.1.3",
diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index 840267ef..d46b9d4b 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -1,5 +1,5 @@
 const { BeanModel } = require("redbean-node/dist/bean-model");
-const { parseTimeObject, parseTimeFromTimeObject, isoToUTCDateTime, utcToISODateTime, SQL_DATETIME_FORMAT, utcToLocal, localToUTC } = require("../../src/util");
+const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC } = require("../../src/util");
 const { isArray } = require("chart.js/helpers");
 const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
 
@@ -11,7 +11,7 @@ class Maintenance extends BeanModel {
      * @param {string} timezone If not specified, the timeRange will be in UTC
      * @returns {Object}
      */
-    async toPublicJSON(timezone = null) {
+    async toPublicJSON() {
 
         let dateRange = [];
         if (this.start_date) {
@@ -22,21 +22,11 @@ class Maintenance extends BeanModel {
         }
 
         let timeRange = [];
-        let startTime = parseTimeObject(this.start_time);
+        let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
         timeRange.push(startTime);
-        let endTime = parseTimeObject(this.end_time);
+        let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
         timeRange.push(endTime);
 
-        // Apply timezone offset
-        if (timezone) {
-            if (this.start_time) {
-                timeObjectToLocal(startTime, timezone);
-            }
-            if (this.end_time) {
-                timeObjectToLocal(endTime, timezone);
-            }
-        }
-
         let obj = {
             id: this.id,
             title: this.title,
@@ -70,18 +60,16 @@ class Maintenance extends BeanModel {
         return this.toPublicJSON(timezone);
     }
 
-    static jsonToBean(bean, obj, timezone) {
+    static jsonToBean(bean, obj) {
         if (obj.id) {
             bean.id = obj.id;
         }
 
         // Apply timezone offset to timeRange, as it cannot apply automatically.
-        if (timezone) {
-            if (obj.timeRange[0]) {
-                timeObjectToUTC(obj.timeRange[0], timezone);
-                if (obj.timeRange[1]) {
-                    timeObjectToUTC(obj.timeRange[1], timezone);
-                }
+        if (obj.timeRange[0]) {
+            timeObjectToUTC(obj.timeRange[0]);
+            if (obj.timeRange[1]) {
+                timeObjectToUTC(obj.timeRange[1]);
             }
         }
 
@@ -118,7 +106,7 @@ class Maintenance extends BeanModel {
             (maintenance_timeslot.start_date <= DATETIME('now')
             AND maintenance_timeslot.end_date >= DATETIME('now')
             AND maintenance.active = 1)
-            AND
+            OR
             (maintenance.strategy = 'manual' AND active = 1)
 
         `;
diff --git a/server/model/maintenance_timeslot.js b/server/model/maintenance_timeslot.js
index 0ac5158d..4db3a1db 100644
--- a/server/model/maintenance_timeslot.js
+++ b/server/model/maintenance_timeslot.js
@@ -40,6 +40,12 @@ class MaintenanceTimeslot extends BeanModel {
             bean.end_date = maintenance.end_date;
             bean.generated_next = true;
             await R.store(bean);
+        } else if (maintenance.strategy === "recurring-interval") {
+            // TODO
+        } else if (maintenance.strategy === "recurring-weekday") {
+            // TODO
+        } else if (maintenance.strategy === "recurring-day-of-month") {
+            // TODO
         } else {
             throw new Error("Unknown maintenance strategy");
         }
diff --git a/server/socket-handlers/maintenance-socket-handler.js b/server/socket-handlers/maintenance-socket-handler.js
index 9ae36b5c..49527f23 100644
--- a/server/socket-handlers/maintenance-socket-handler.js
+++ b/server/socket-handlers/maintenance-socket-handler.js
@@ -5,7 +5,6 @@ const apicache = require("../modules/apicache");
 const { UptimeKumaServer } = require("../uptime-kuma-server");
 const Maintenance = require("../model/maintenance");
 const server = UptimeKumaServer.getInstance();
-const dayjs = require("dayjs");
 const MaintenanceTimeslot = require("../model/maintenance_timeslot");
 
 /**
@@ -14,13 +13,13 @@ const MaintenanceTimeslot = require("../model/maintenance_timeslot");
  */
 module.exports.maintenanceSocketHandler = (socket) => {
     // Add a new maintenance
-    socket.on("addMaintenance", async (maintenance, timezone, callback) => {
+    socket.on("addMaintenance", async (maintenance, callback) => {
         try {
             checkLogin(socket);
 
             log.debug("maintenance", maintenance);
 
-            let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance, timezone);
+            let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
             bean.user_id = socket.userID;
             let maintenanceID = await R.store(bean);
             await MaintenanceTimeslot.generateTimeslot(bean);
@@ -42,7 +41,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
     });
 
     // Edit a maintenance
-    socket.on("editMaintenance", async (maintenance, timezone, callback) => {
+    socket.on("editMaintenance", async (maintenance, callback) => {
         try {
             checkLogin(socket);
 
@@ -52,7 +51,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
                 throw new Error("Permission denied.");
             }
 
-            Maintenance.jsonToBean(bean, maintenance, timezone);
+            Maintenance.jsonToBean(bean, maintenance);
 
             await R.store(bean);
             await MaintenanceTimeslot.generateTimeslot(bean, null, true);
@@ -142,7 +141,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
         }
     });
 
-    socket.on("getMaintenance", async (maintenanceID, timezone, callback) => {
+    socket.on("getMaintenance", async (maintenanceID, callback) => {
         try {
             checkLogin(socket);
 
@@ -155,7 +154,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
 
             callback({
                 ok: true,
-                maintenance: await bean.toJSON(timezone),
+                maintenance: await bean.toJSON(),
             });
 
         } catch (e) {
diff --git a/server/util-server.js b/server/util-server.js
index ddb9dab5..7c81cde7 100644
--- a/server/util-server.js
+++ b/server/util-server.js
@@ -648,8 +648,14 @@ module.exports.send403 = (res, msg = "") => {
 };
 
 function timeObjectConvertTimezone(obj, timezone, timeObjectToUTC = true) {
-    // e.g. +08:00
-    let offsetString = dayjs().tz(timezone).format("Z");
+    let offsetString;
+
+    if (timezone) {
+        offsetString = dayjs().tz(timezone).format("Z");
+    } else {
+        offsetString = dayjs().format("Z");
+    }
+
     let hours = parseInt(offsetString.substring(1, 3));
     let minutes = parseInt(offsetString.substring(4, 6));
 
@@ -680,10 +686,22 @@ function timeObjectConvertTimezone(obj, timezone, timeObjectToUTC = true) {
     return obj;
 }
 
-module.exports.timeObjectToUTC = (obj, timezone) => {
+/**
+ *
+ * @param {object} obj
+ * @param {string} timezone
+ * @returns {object}
+ */
+module.exports.timeObjectToUTC = (obj, timezone = undefined) => {
     return timeObjectConvertTimezone(obj, timezone, true);
 };
 
-module.exports.timeObjectToLocal = (obj, timezone) => {
+/**
+ *
+ * @param {object} obj
+ * @param {string} timezone
+ * @returns {object}
+ */
+module.exports.timeObjectToLocal = (obj, timezone = undefined) => {
     return timeObjectConvertTimezone(obj, timezone, false);
 };
diff --git a/src/languages/en.js b/src/languages/en.js
index d6a65b04..835fa248 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -630,4 +630,5 @@ export default {
     lastDay2: "2nd Last Day of Month",
     lastDay3: "3rd Last Day of Month",
     lastDay4: "4th Last Day of Month",
+    "No Maintenance": "No Maintenance",
 };
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 946059cb..790588d8 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -200,7 +200,8 @@
                                         :monthChangeOnScroll="false"
                                         :minDate="minDate"
                                         :enableTimePicker="false"
-                                        :utc="true"
+                                        format="yyyy-MM-dd"
+                                        modelType="yyyy-MM-dd HH:mm:ss"
                                     />
                                 </div>
                             </template>
@@ -356,9 +357,6 @@ export default {
     },
     methods: {
         init() {
-            // Use browser's timezone!
-            let timezone = dayjs.tz.guess();
-
             this.affectedMonitors = [];
             this.selectedStatusPages = [];
 
@@ -381,7 +379,7 @@ export default {
                     daysOfMonth: [],
                 };
             } else if (this.isEdit) {
-                this.$root.getSocket().emit("getMaintenance", this.$route.params.id, timezone, (res) => {
+                this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
                     if (res.ok) {
                         this.maintenance = res.maintenance;
 
@@ -440,11 +438,8 @@ export default {
             this.maintenance.end_date = this.$root.toUTC(this.maintenance.end_date);
             */
 
-            // Use browser's timezone!
-            let timezone = dayjs.tz.guess();
-
             if (this.isAdd) {
-                this.$root.addMaintenance(this.maintenance, timezone, async (res) => {
+                this.$root.addMaintenance(this.maintenance, async (res) => {
                     if (res.ok) {
                         await this.addMonitorMaintenance(res.maintenanceID, async () => {
                             await this.addMaintenanceStatusPage(res.maintenanceID, () => {
@@ -461,7 +456,7 @@ export default {
 
                 });
             } else {
-                this.$root.getSocket().emit("editMaintenance", this.maintenance, timezone, async (res) => {
+                this.$root.getSocket().emit("editMaintenance", this.maintenance, async (res) => {
                     if (res.ok) {
                         await this.addMonitorMaintenance(res.maintenanceID, async () => {
                             await this.addMaintenanceStatusPage(res.maintenanceID, () => {
diff --git a/src/pages/ManageMaintenance.vue b/src/pages/ManageMaintenance.vue
index 4bfa9059..51e3ee28 100644
--- a/src/pages/ManageMaintenance.vue
+++ b/src/pages/ManageMaintenance.vue
@@ -13,7 +13,7 @@
 
             <div class="shadow-box">
                 <span v-if="Object.keys(sortedMaintenanceList).length === 0" class="d-flex align-items-center justify-content-center my-3">
-                    {{ $t("No maintenance") }}
+                    {{ $t("No Maintenance") }}
                 </span>
 
                 <div
@@ -34,9 +34,19 @@
 
                     <div class="buttons">
                         <router-link v-if="false" :to="maintenanceURL(item.id)" class="btn btn-light">{{ $t("Details") }}</router-link>
+
+                        <button v-if="item.active" class="btn btn-light" @click="pauseDialog">
+                            <font-awesome-icon icon="pause" /> {{ $t("Pause") }}
+                        </button>
+
+                        <button v-if="!item.active" class="btn btn-primary" @click="resumeMaintenance">
+                            <font-awesome-icon icon="play" /> {{ $t("Resume") }}
+                        </button>
+
                         <router-link :to="'/maintenance/edit/' + item.id" class="btn btn-secondary">
                             <font-awesome-icon icon="edit" /> {{ $t("Edit") }}
                         </router-link>
+
                         <button class="btn btn-danger" @click="deleteDialog(item.id)">
                             <font-awesome-icon icon="trash" /> {{ $t("Delete") }}
                         </button>
@@ -48,6 +58,10 @@
                 <a href="https://github.com/louislam/uptime-kuma/wiki/Maintenance" target="_blank">Learn More</a>
             </div>
 
+            <Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseMaintenance">
+                {{ $t("pauseMaintenanceMsg") }}
+            </Confirm>
+
             <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMaintenance">
                 {{ $t("deleteMaintenanceMsg") }}
             </Confirm>
@@ -148,6 +162,33 @@ export default {
                 }
             });
         },
+
+        /**
+         * Show dialog to confirm pause
+         */
+        pauseDialog() {
+            this.$refs.confirmPause.show();
+        },
+
+        /**
+         * Pause maintenance
+         */
+        pauseMonitor() {
+            return;
+            this.$root.getSocket().emit("pauseMaintenance", selectedMaintenanceID, (res) => {
+                this.$root.toastRes(res);
+            });
+        },
+
+        /**
+         * Resume maintenance
+         */
+        resumeMaintenance() {
+            return;
+            this.$root.getSocket().emit("resumeMaintenance", selectedMaintenanceID, (res) => {
+                this.$root.toastRes(res);
+            });
+        },
     },
 };
 </script>

From c84de4d2598c363b4a45ab0ddef09ca8c50b11c4 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 11 Oct 2022 01:45:30 +0800
Subject: [PATCH 107/134] WIP: Add maintenance status

---
 server/model/maintenance.js     | 53 ++++++++++++++++++++++-
 server/model/monitor.js         |  6 +--
 src/assets/app.scss             | 14 ++++++
 src/layouts/Layout.vue          |  2 +-
 src/mixins/datetime.js          |  4 --
 src/pages/ManageMaintenance.vue | 77 ++++++++++++++++-----------------
 6 files changed, 108 insertions(+), 48 deletions(-)

diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index d46b9d4b..2ab2a5bb 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -2,13 +2,14 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
 const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC } = require("../../src/util");
 const { isArray } = require("chart.js/helpers");
 const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
+const { R } = require("redbean-node");
+const dayjs = require("dayjs");
 
 class Maintenance extends BeanModel {
 
     /**
      * Return an object that ready to parse to JSON for public
      * Only show necessary data to public
-     * @param {string} timezone If not specified, the timeRange will be in UTC
      * @returns {Object}
      */
     async toPublicJSON() {
@@ -38,6 +39,7 @@ class Maintenance extends BeanModel {
             timeRange: timeRange,
             weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
             daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
+            timeslotList: await this.getTimeslotList(),
         };
 
         if (!isArray(obj.weekdays)) {
@@ -48,9 +50,45 @@ class Maintenance extends BeanModel {
             obj.daysOfMonth = [];
         }
 
+        // Maintenance Status
+        if (!obj.active) {
+            obj.status = "inactive";
+        } else if (obj.strategy === "manual" || obj.timeslotList.length > 0) {
+            for (let timeslot of obj.timeslotList) {
+                if (dayjs.utc(timeslot.start_date) <= dayjs.utc() && dayjs.utc(timeslot.end_date) >= dayjs.utc()) {
+                    obj.status = "under-maintenance";
+                    break;
+                }
+            }
+
+            if (!obj.status) {
+                obj.status = "scheduled";
+            }
+        } else if (obj.timeslotList.length === 0) {
+            obj.status = "ended";
+        } else {
+            obj.status = "unknown";
+        }
+
         return obj;
     }
 
+    /**
+     * Only get future or current timeslots only
+     * @returns {Promise<[]>}
+     */
+    async getTimeslotList() {
+        return await R.getAll(`
+            SELECT maintenance_timeslot.*
+            FROM maintenance_timeslot, maintenance
+            WHERE maintenance_timeslot.maintenance_id = maintenance.id
+            AND maintenance.id = ?
+            AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
+        `, [
+            this.id
+        ]);
+    }
+
     /**
      * Return an object that ready to parse to JSON
      * @param {string} timezone If not specified, the timeRange will be in UTC
@@ -111,6 +149,19 @@ class Maintenance extends BeanModel {
 
         `;
     }
+
+    /**
+     * SQL conditions for active and future maintenance
+     * @returns {string}
+     */
+    static getActiveAndFutureMaintenanceSQLCondition() {
+        return `
+            (maintenance_timeslot.end_date >= DATETIME('now')
+            AND maintenance.active = 1)
+            OR
+            (maintenance.strategy = 'manual' AND active = 1)
+        `;
+    }
 }
 
 module.exports = Maintenance;
diff --git a/server/model/monitor.js b/server/model/monitor.js
index 4c51d220..d77c5529 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -1109,10 +1109,10 @@ class Monitor extends BeanModel {
             FROM monitor_maintenance mm
             JOIN maintenance
                 ON mm.maintenance_id = maintenance.id
-            JOIN maintenance_timeslot
+                AND mm.monitor_id = ?
+            LEFT JOIN maintenance_timeslot
                 ON maintenance_timeslot.maintenance_id = maintenance.id
-            WHERE mm.monitor_id = ?
-              AND ${activeCondition}
+            WHERE ${activeCondition}
             LIMIT 1`, [ monitorID ]);
         return maintenance.count !== 0;
     }
diff --git a/src/assets/app.scss b/src/assets/app.scss
index 81cf7724..be324afd 100644
--- a/src/assets/app.scss
+++ b/src/assets/app.scss
@@ -269,6 +269,20 @@ optgroup {
         color: white;
     }
 
+    .btn-normal {
+        $bg-color: $dark-header-bg;
+
+        color: $dark-font-color;
+        background-color: $bg-color;
+        border-color: $bg-color;
+
+        &:hover {
+            $hover-color: darken($bg-color, 3%);
+            background-color: $hover-color;
+            border-color: $hover-color;
+        }
+    }
+
     .btn-warning {
         color: $dark-font-color2;
 
diff --git a/src/layouts/Layout.vue b/src/layouts/Layout.vue
index 7ece4982..acd9446c 100644
--- a/src/layouts/Layout.vue
+++ b/src/layouts/Layout.vue
@@ -58,7 +58,7 @@
                             </li>
 
                             <li>
-                                <router-link to="/settings" class="dropdown-item" :class="{ active: $route.path.includes('settings') }">
+                                <router-link to="/settings/general" class="dropdown-item" :class="{ active: $route.path.includes('settings') }">
                                     <font-awesome-icon icon="cog" /> {{ $t("Settings") }}
                                 </router-link>
                             </li>
diff --git a/src/mixins/datetime.js b/src/mixins/datetime.js
index 3bbe1130..4fa2fa83 100644
--- a/src/mixins/datetime.js
+++ b/src/mixins/datetime.js
@@ -12,10 +12,6 @@ export default {
     },
 
     methods: {
-        isActiveMaintenance(endDate) {
-            return (dayjs.utc(endDate).unix() >= dayjs.utc().unix());
-        },
-
         toUTC(value) {
             return dayjs.tz(value, this.timezone).utc().format();
         },
diff --git a/src/pages/ManageMaintenance.vue b/src/pages/ManageMaintenance.vue
index 51e3ee28..28bea9b8 100644
--- a/src/pages/ManageMaintenance.vue
+++ b/src/pages/ManageMaintenance.vue
@@ -20,7 +20,7 @@
                     v-for="(item, index) in sortedMaintenanceList"
                     :key="index"
                     class="item"
-                    :class="{ 'ended': !$root.isActiveMaintenance(item.end_date) }"
+                    :class="item.status"
                 >
                     <div class="left-part">
                         <div
@@ -35,7 +35,7 @@
                     <div class="buttons">
                         <router-link v-if="false" :to="maintenanceURL(item.id)" class="btn btn-light">{{ $t("Details") }}</router-link>
 
-                        <button v-if="item.active" class="btn btn-light" @click="pauseDialog">
+                        <button v-if="item.active" class="btn btn-normal" @click="pauseDialog">
                             <font-awesome-icon icon="pause" /> {{ $t("Pause") }}
                         </button>
 
@@ -43,7 +43,7 @@
                             <font-awesome-icon icon="play" /> {{ $t("Resume") }}
                         </button>
 
-                        <router-link :to="'/maintenance/edit/' + item.id" class="btn btn-secondary">
+                        <router-link :to="'/maintenance/edit/' + item.id" class="btn btn-normal">
                             <font-awesome-icon icon="edit" /> {{ $t("Edit") }}
                         </router-link>
 
@@ -90,36 +90,6 @@ export default {
             let result = Object.values(this.$root.maintenanceList);
 
             result.sort((m1, m2) => {
-
-                if (this.$root.isActiveMaintenance(m1.end_date) !== this.$root.isActiveMaintenance(m2.end_date)) {
-                    if (!this.$root.isActiveMaintenance(m2.end_date)) {
-                        return -1;
-                    }
-                    if (!this.$root.isActiveMaintenance(m1.end_date)) {
-                        return 1;
-                    }
-                }
-
-                if (this.$root.isActiveMaintenance(m1.end_date) && this.$root.isActiveMaintenance(m2.end_date)) {
-                    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 (!this.$root.isActiveMaintenance(m1.end_date) && !this.$root.isActiveMaintenance(m2.end_date)) {
-                    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);
             });
 
@@ -173,7 +143,7 @@ export default {
         /**
          * Pause maintenance
          */
-        pauseMonitor() {
+        pauseMaintenance() {
             return;
             this.$root.getSocket().emit("pauseMaintenance", selectedMaintenanceID, (res) => {
                 this.$root.toastRes(res);
@@ -211,13 +181,43 @@ export default {
             background-color: $highlight-white;
         }
 
+        &.under-maintenance {
+            background-color: rgba(23, 71, 245, 0.16);
+
+            &:hover {
+                background-color: rgba(23, 71, 245, 0.3) !important;
+            }
+
+            .circle {
+                background-color: $maintenance;
+            }
+        }
+
+        &.scheduled {
+            .circle {
+                background-color: $primary;
+            }
+        }
+
+        &.inactive {
+            .circle {
+                background-color: $danger;
+            }
+        }
+
         &.ended {
             .left-part {
-                opacity: 0.5;
+                opacity: 0.3;
+            }
 
-                .circle {
-                    background-color: $dark-font-color;
-                }
+            .circle {
+                background-color: $dark-font-color;
+            }
+        }
+
+        &.unknown {
+            .circle {
+                background-color: $dark-font-color;
             }
         }
 
@@ -230,7 +230,6 @@ export default {
                 width: 25px;
                 height: 25px;
                 border-radius: 50rem;
-                background-color: $maintenance;
             }
 
             .info {

From d5c02fc627e87a301eb4d7e9ff6466f05424773d Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 11 Oct 2022 01:59:47 +0800
Subject: [PATCH 108/134] Update Maintenance list order by status

---
 src/pages/ManageMaintenance.vue | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/src/pages/ManageMaintenance.vue b/src/pages/ManageMaintenance.vue
index 28bea9b8..5820aada 100644
--- a/src/pages/ManageMaintenance.vue
+++ b/src/pages/ManageMaintenance.vue
@@ -83,6 +83,13 @@ export default {
     data() {
         return {
             selectedMaintenanceID: undefined,
+            statusOrderList: {
+                "under-maintenance": 1000,
+                "scheduled": 900,
+                "inactive": 800,
+                "ended": 700,
+                "unknown": 0,
+            }
         };
     },
     computed: {
@@ -90,7 +97,11 @@ export default {
             let result = Object.values(this.$root.maintenanceList);
 
             result.sort((m1, m2) => {
-                return m1.title.localeCompare(m2.title);
+                if (this.statusOrderList[m1.status] === this.statusOrderList[m2.status]) {
+                    return m1.title.localeCompare(m2.title);
+                } else {
+                    return this.statusOrderList[m1.status] < this.statusOrderList[m2.status];
+                }
             });
 
             return result;

From 2ee8378814d5cf92dceabcf2d9e4e79505001f16 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 11 Oct 2022 02:32:57 +0800
Subject: [PATCH 109/134] Update to 1.18.5

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 6f48fedd..b1a76d36 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "uptime-kuma",
-    "version": "1.18.4",
+    "version": "1.18.5",
     "license": "MIT",
     "repository": {
         "type": "git",
@@ -38,7 +38,7 @@
         "build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
         "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
         "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
-        "setup": "git checkout 1.18.4 && npm ci --production && npm run download-dist",
+        "setup": "git checkout 1.18.5 && npm ci --production && npm run download-dist",
         "download-dist": "node extra/download-dist.js",
         "mark-as-nightly": "node extra/mark-as-nightly.js",
         "reset-password": "node extra/reset-password.js",

From 2271ac4a5a181bfcb0c8836a3a6f812215e03685 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 11 Oct 2022 14:52:47 +0800
Subject: [PATCH 110/134] Add info.serverTimezoneOffset and improve some styles

---
 package-lock.json               |  4 ++--
 server/client.js                |  2 ++
 src/assets/app.scss             |  5 +++++
 src/pages/EditMaintenance.vue   |  4 ++--
 src/pages/ManageMaintenance.vue | 26 ++++++++++++++------------
 src/util.js                     |  1 -
 src/util.ts                     |  1 -
 7 files changed, 25 insertions(+), 18 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 161208af..0b9454ae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "uptime-kuma",
-    "version": "1.18.4",
+    "version": "1.18.5",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "uptime-kuma",
-            "version": "1.18.4",
+            "version": "1.18.5",
             "license": "MIT",
             "dependencies": {
                 "@louislam/sqlite3": "~15.0.6",
diff --git a/server/client.js b/server/client.js
index 795b3ad4..dcc778df 100644
--- a/server/client.js
+++ b/server/client.js
@@ -8,6 +8,7 @@ const server = UptimeKumaServer.getInstance();
 const io = server.io;
 const { setting } = require("./util-server");
 const checkVersion = require("./check-version");
+const dayjs = require("dayjs");
 
 /**
  * Send list of notification providers to client
@@ -124,6 +125,7 @@ async function sendInfo(socket) {
         latestVersion: checkVersion.latestVersion,
         primaryBaseURL: await setting("primaryBaseURL"),
         serverTimezone: await server.getTimezone(),
+        serverTimezoneOffset: dayjs().format("Z"),
     });
 }
 
diff --git a/src/assets/app.scss b/src/assets/app.scss
index be324afd..7eb95931 100644
--- a/src/assets/app.scss
+++ b/src/assets/app.scss
@@ -101,6 +101,11 @@ optgroup {
     }
 }
 
+// Override Bootstrap
+.btn-group > .btn:hover {
+    z-index: initial;
+}
+
 .btn {
     padding-left: 20px;
     padding-right: 20px;
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 790588d8..24229293 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -5,7 +5,7 @@
             <form @submit.prevent="submit">
                 <div class="shadow-box">
                     <div class="row">
-                        <div class="col-xl-7">
+                        <div class="col-xl-10">
                             <!-- Title -->
                             <div class="mb-3">
                                 <label for="name" class="form-label">{{ $t("Title") }}</label>
@@ -82,7 +82,7 @@
 
                             <h2 class="mt-5">{{ $t("Date and Time") }}</h2>
 
-                            <div>⚠️ {{ $t("warningTimezone") }}: {{ $root.info.serverTimezone }}</div>
+                            <div>⚠️ {{ $t("warningTimezone") }}: <mark>{{ $root.info.serverTimezone }} ({{ $root.info.serverTimezoneOffset }})</mark></div>
 
                             <!-- Strategy -->
                             <div class="my-3">
diff --git a/src/pages/ManageMaintenance.vue b/src/pages/ManageMaintenance.vue
index 5820aada..725e94b0 100644
--- a/src/pages/ManageMaintenance.vue
+++ b/src/pages/ManageMaintenance.vue
@@ -35,21 +35,23 @@
                     <div class="buttons">
                         <router-link v-if="false" :to="maintenanceURL(item.id)" class="btn btn-light">{{ $t("Details") }}</router-link>
 
-                        <button v-if="item.active" class="btn btn-normal" @click="pauseDialog">
-                            <font-awesome-icon icon="pause" /> {{ $t("Pause") }}
-                        </button>
+                        <div class="btn-group" role="group">
+                            <button v-if="item.active" class="btn btn-normal" @click="pauseDialog">
+                                <font-awesome-icon icon="pause" /> {{ $t("Pause") }}
+                            </button>
 
-                        <button v-if="!item.active" class="btn btn-primary" @click="resumeMaintenance">
-                            <font-awesome-icon icon="play" /> {{ $t("Resume") }}
-                        </button>
+                            <button v-if="!item.active" class="btn btn-primary" @click="resumeMaintenance">
+                                <font-awesome-icon icon="play" /> {{ $t("Resume") }}
+                            </button>
 
-                        <router-link :to="'/maintenance/edit/' + item.id" class="btn btn-normal">
-                            <font-awesome-icon icon="edit" /> {{ $t("Edit") }}
-                        </router-link>
+                            <router-link :to="'/maintenance/edit/' + item.id" class="btn btn-normal">
+                                <font-awesome-icon icon="edit" /> {{ $t("Edit") }}
+                            </router-link>
 
-                        <button class="btn btn-danger" @click="deleteDialog(item.id)">
-                            <font-awesome-icon icon="trash" /> {{ $t("Delete") }}
-                        </button>
+                            <button class="btn btn-danger" @click="deleteDialog(item.id)">
+                                <font-awesome-icon icon="trash" /> {{ $t("Delete") }}
+                            </button>
+                        </div>
                     </div>
                 </div>
             </div>
diff --git a/src/util.js b/src/util.js
index ec2d2322..2213d4d0 100644
--- a/src/util.js
+++ b/src/util.js
@@ -363,7 +363,6 @@ function utcToISODateTime(input) {
     return dayjs.utc(input).toISOString();
 }
 exports.utcToISODateTime = utcToISODateTime;
-/**
 /**
  * For SQL_DATETIME_FORMAT
  */
diff --git a/src/util.ts b/src/util.ts
index 34e0905d..966383f8 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -409,7 +409,6 @@ export function utcToISODateTime(input : string) {
     return dayjs.utc(input).toISOString();
 }
 
-/**
 /**
  * For SQL_DATETIME_FORMAT
  */

From 180d881ac122fb8f85805153f2342725a4843851 Mon Sep 17 00:00:00 2001
From: Alexander Borzov <59200516+Borzoff@users.noreply.github.com>
Date: Tue, 11 Oct 2022 10:35:08 +0300
Subject: [PATCH 111/134] Update ru-RU.js (#2217)

---
 src/languages/ru-RU.js | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/languages/ru-RU.js b/src/languages/ru-RU.js
index 0aaf0968..6922f51b 100644
--- a/src/languages/ru-RU.js
+++ b/src/languages/ru-RU.js
@@ -393,6 +393,12 @@ export default {
     alertaAlertState: "Состояние алерта",
     alertaRecoverState: "Состояние восстановления",
     Proxies: "Прокси",
+    "Setup Proxy": "Настройка Прокси",
+    "Proxy Protocol": "Протокол Прокси",
+    "Proxy Server": "Прокси",
+    "Proxy server has authentication": "Прокси имеет аутентификацию",
+    "Reverse Proxy": "Обратный прокси",
+    "No Proxy": "Без прокси",
     default: "По умолчанию",
     enabled: "Включено",
     setAsDefault: "Установлено по умолчанию",

From e07aa982c34c5b72f08b9408aab08b39a8d49088 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 11 Oct 2022 18:23:17 +0800
Subject: [PATCH 112/134] WIP

---
 server/client.js                              |  2 +-
 server/model/maintenance.js                   | 20 +++++---
 server/model/maintenance_timeslot.js          | 16 +++++-
 .../maintenance-socket-handler.js             | 50 +++++++++++++++++++
 server/uptime-kuma-server.js                  |  4 ++
 src/assets/app.scss                           | 18 +++++--
 src/languages/en.js                           |  6 +++
 src/pages/ManageMaintenance.vue               | 49 ++++++++++++++----
 src/util.js                                   | 11 ++--
 src/util.ts                                   |  9 ++--
 10 files changed, 152 insertions(+), 33 deletions(-)

diff --git a/server/client.js b/server/client.js
index dcc778df..bed2ba03 100644
--- a/server/client.js
+++ b/server/client.js
@@ -125,7 +125,7 @@ async function sendInfo(socket) {
         latestVersion: checkVersion.latestVersion,
         primaryBaseURL: await setting("primaryBaseURL"),
         serverTimezone: await server.getTimezone(),
-        serverTimezoneOffset: dayjs().format("Z"),
+        serverTimezoneOffset: server.getTimezoneOffset(),
     });
 }
 
diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index 2ab2a5bb..4910d2a0 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -39,9 +39,15 @@ class Maintenance extends BeanModel {
             timeRange: timeRange,
             weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
             daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
-            timeslotList: await this.getTimeslotList(),
+            timeslotList: [],
         };
 
+        const timeslotList = await this.getTimeslotList();
+
+        for (let timeslot of timeslotList) {
+            obj.timeslotList.push(await timeslot.toPublicJSON());
+        }
+
         if (!isArray(obj.weekdays)) {
             obj.weekdays = [];
         }
@@ -53,7 +59,9 @@ class Maintenance extends BeanModel {
         // Maintenance Status
         if (!obj.active) {
             obj.status = "inactive";
-        } else if (obj.strategy === "manual" || obj.timeslotList.length > 0) {
+        } else if (obj.strategy === "manual") {
+            obj.status = "under-maintenance";
+        } else if (obj.timeslotList.length > 0) {
             for (let timeslot of obj.timeslotList) {
                 if (dayjs.utc(timeslot.start_date) <= dayjs.utc() && dayjs.utc(timeslot.end_date) >= dayjs.utc()) {
                     obj.status = "under-maintenance";
@@ -78,7 +86,7 @@ class Maintenance extends BeanModel {
      * @returns {Promise<[]>}
      */
     async getTimeslotList() {
-        return await R.getAll(`
+        return R.convertToBeans("maintenance_timeslot", await R.getAll(`
             SELECT maintenance_timeslot.*
             FROM maintenance_timeslot, maintenance
             WHERE maintenance_timeslot.maintenance_id = maintenance.id
@@ -86,7 +94,7 @@ class Maintenance extends BeanModel {
             AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
         `, [
             this.id
-        ]);
+        ]));
     }
 
     /**
@@ -156,10 +164,10 @@ class Maintenance extends BeanModel {
      */
     static getActiveAndFutureMaintenanceSQLCondition() {
         return `
-            (maintenance_timeslot.end_date >= DATETIME('now')
+            ((maintenance_timeslot.end_date >= DATETIME('now')
             AND maintenance.active = 1)
             OR
-            (maintenance.strategy = 'manual' AND active = 1)
+            (maintenance.strategy = 'manual' AND active = 1))
         `;
     }
 }
diff --git a/server/model/maintenance_timeslot.js b/server/model/maintenance_timeslot.js
index 4db3a1db..f06806ac 100644
--- a/server/model/maintenance_timeslot.js
+++ b/server/model/maintenance_timeslot.js
@@ -1,16 +1,28 @@
 const { BeanModel } = require("redbean-node/dist/bean-model");
 const { R } = require("redbean-node");
 const dayjs = require("dayjs");
-const { log } = require("../../src/util");
+const { log, utcToLocal, SQL_DATETIME_FORMAT_WITHOUT_SECOND } = require("../../src/util");
+const { UptimeKumaServer } = require("../uptime-kuma-server");
 
 class MaintenanceTimeslot extends BeanModel {
 
     async toPublicJSON() {
+        const serverTimezoneOffset = await UptimeKumaServer.getInstance().getTimezoneOffset();
 
+        const obj = {
+            id: this.id,
+            startDate: this.start_date,
+            endDate: this.end_date,
+            startDateServerTimezone: utcToLocal(this.start_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
+            endDateServerTimezone: utcToLocal(this.end_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
+            serverTimezoneOffset,
+        };
+
+        return obj;
     }
 
     async toJSON() {
-
+        return await this.toPublicJSON();
     }
 
     /**
diff --git a/server/socket-handlers/maintenance-socket-handler.js b/server/socket-handlers/maintenance-socket-handler.js
index 49527f23..5294050c 100644
--- a/server/socket-handlers/maintenance-socket-handler.js
+++ b/server/socket-handlers/maintenance-socket-handler.js
@@ -258,4 +258,54 @@ module.exports.maintenanceSocketHandler = (socket) => {
             });
         }
     });
+
+    socket.on("pauseMaintenance", async (maintenanceID, callback) => {
+        try {
+            checkLogin(socket);
+
+            log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+
+            await R.exec("UPDATE maintenance SET active = 0 WHERE id = ? ", [
+                maintenanceID,
+            ]);
+
+            callback({
+                ok: true,
+                msg: "Paused Successfully.",
+            });
+
+            await server.sendMaintenanceList(socket);
+
+        } catch (e) {
+            callback({
+                ok: false,
+                msg: e.message,
+            });
+        }
+    });
+
+    socket.on("resumeMaintenance", async (maintenanceID, callback) => {
+        try {
+            checkLogin(socket);
+
+            log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+
+            await R.exec("UPDATE maintenance SET active = 1 WHERE id = ? ", [
+                maintenanceID,
+            ]);
+
+            callback({
+                ok: true,
+                msg: "Resume Successfully",
+            });
+
+            await server.sendMaintenanceList(socket);
+
+        } catch (e) {
+            callback({
+                ok: false,
+                msg: e.message,
+            });
+        }
+    });
 };
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index 15583159..667f6b6a 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -204,6 +204,10 @@ class UptimeKumaServer {
         }
     }
 
+    async getTimezoneOffset() {
+        return dayjs().format("Z");
+    }
+
     async setTimezone(timezone) {
         await Settings.set("serverTimezone", timezone, "general");
         process.env.TZ = timezone;
diff --git a/src/assets/app.scss b/src/assets/app.scss
index 7eb95931..7da76fff 100644
--- a/src/assets/app.scss
+++ b/src/assets/app.scss
@@ -101,11 +101,6 @@ optgroup {
     }
 }
 
-// Override Bootstrap
-.btn-group > .btn:hover {
-    z-index: initial;
-}
-
 .btn {
     padding-left: 20px;
     padding-right: 20px;
@@ -125,6 +120,19 @@ optgroup {
     }
 }
 
+.btn-normal {
+    $bg-color: #F5F5F5;
+
+    background-color: $bg-color;
+    border-color: $bg-color;
+
+    &:hover {
+        $hover-color: darken($bg-color, 3%);
+        background-color: $hover-color;
+        border-color: $hover-color;
+    }
+}
+
 .btn-warning {
     color: white;
 
diff --git a/src/languages/en.js b/src/languages/en.js
index 835fa248..a1c9b560 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -631,4 +631,10 @@ export default {
     lastDay3: "3rd Last Day of Month",
     lastDay4: "4th Last Day of Month",
     "No Maintenance": "No Maintenance",
+    pauseMaintenanceMsg: "Are you sure want to pause?",
+    "maintenanceStatus-under-maintenance": "Under Maintenance",
+    "maintenanceStatus-inactive": "Inactive",
+    "maintenanceStatus-scheduled": "Scheduled",
+    "maintenanceStatus-ended": "Ended",
+    "maintenanceStatus-unknown": "Unknown",
 };
diff --git a/src/pages/ManageMaintenance.vue b/src/pages/ManageMaintenance.vue
index 725e94b0..af9c0ba5 100644
--- a/src/pages/ManageMaintenance.vue
+++ b/src/pages/ManageMaintenance.vue
@@ -28,7 +28,18 @@
                         ></div>
                         <div class="info">
                             <div class="title">{{ item.title }}</div>
-                            <div>{{ item.description }}</div>
+                            <div v-if="false">{{ item.description }}</div>
+                            <div class="status">
+                                {{ $t("maintenanceStatus-" + item.status) }}
+                            </div>
+
+                            <div v-if="item.strategy === 'manual'" class="timeslot">
+                                {{ $t("Manual") }}
+                            </div>
+                            <div v-else-if="item.timeslotList.length > 0" class="timeslot">
+                                {{ item.timeslotList[0].startDateServerTimezone }} <span class="to">-</span> {{ item.timeslotList[0].endDateServerTimezone }}
+                                (UTC{{ item.timeslotList[0].serverTimezoneOffset }})
+                            </div>
                         </div>
                     </div>
 
@@ -36,11 +47,11 @@
                         <router-link v-if="false" :to="maintenanceURL(item.id)" class="btn btn-light">{{ $t("Details") }}</router-link>
 
                         <div class="btn-group" role="group">
-                            <button v-if="item.active" class="btn btn-normal" @click="pauseDialog">
+                            <button v-if="item.active" class="btn btn-normal" @click="pauseDialog(item.id)">
                                 <font-awesome-icon icon="pause" /> {{ $t("Pause") }}
                             </button>
 
-                            <button v-if="!item.active" class="btn btn-primary" @click="resumeMaintenance">
+                            <button v-if="!item.active" class="btn btn-primary" @click="resumeMaintenance(item.id)">
                                 <font-awesome-icon icon="play" /> {{ $t("Resume") }}
                             </button>
 
@@ -149,7 +160,8 @@ export default {
         /**
          * Show dialog to confirm pause
          */
-        pauseDialog() {
+        pauseDialog(maintenanceID) {
+            this.selectedMaintenanceID = maintenanceID;
             this.$refs.confirmPause.show();
         },
 
@@ -157,8 +169,7 @@ export default {
          * Pause maintenance
          */
         pauseMaintenance() {
-            return;
-            this.$root.getSocket().emit("pauseMaintenance", selectedMaintenanceID, (res) => {
+            this.$root.getSocket().emit("pauseMaintenance", this.selectedMaintenanceID, (res) => {
                 this.$root.toastRes(res);
             });
         },
@@ -166,9 +177,8 @@ export default {
         /**
          * Resume maintenance
          */
-        resumeMaintenance() {
-            return;
-            this.$root.getSocket().emit("resumeMaintenance", selectedMaintenanceID, (res) => {
+        resumeMaintenance(id) {
+            this.$root.getSocket().emit("resumeMaintenance", id, (res) => {
                 this.$root.toastRes(res);
             });
         },
@@ -189,6 +199,7 @@ export default {
         justify-content: space-between;
         padding: 10px;
         min-height: 90px;
+        margin-bottom: 5px;
 
         &:hover {
             background-color: $highlight-white;
@@ -251,9 +262,27 @@ export default {
                     font-size: 20px;
                 }
 
-                .slug {
+                .status {
                     font-size: 14px;
                 }
+
+                .timeslot {
+                    margin-top: 5px;
+                    display: inline-block;
+                    font-size: 14px;
+                    background-color: rgba(255, 255, 255, 0.5);
+                    border-radius: 20px;
+                    padding: 0 10px;
+
+                    .to {
+                        margin: 0 6px;
+                    }
+
+                    .dark & {
+                        color: white;
+                        background-color: rgba(255, 255, 255, 0.1);
+                    }
+                }
             }
         }
 
diff --git a/src/util.js b/src/util.js
index 2213d4d0..9cdecc17 100644
--- a/src/util.js
+++ b/src/util.js
@@ -7,7 +7,7 @@
 // Backend uses the compiled file util.js
 // Frontend uses util.ts
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = 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;
+exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = 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");
 exports.isDev = process.env.NODE_ENV === "development";
 exports.appName = "Uptime Kuma";
@@ -21,6 +21,7 @@ exports.STATUS_PAGE_PARTIAL_DOWN = 2;
 exports.STATUS_PAGE_MAINTENANCE = 3;
 exports.SQL_DATE_FORMAT = "YYYY-MM-DD";
 exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
+exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
 /** Flip the status of s */
 function flipStatus(s) {
     if (s === exports.UP) {
@@ -366,11 +367,11 @@ exports.utcToISODateTime = utcToISODateTime;
 /**
  * For SQL_DATETIME_FORMAT
  */
-function utcToLocal(input) {
-    return dayjs.utc(input).local().format(exports.SQL_DATETIME_FORMAT);
+function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
+    return dayjs.utc(input).local().format(format);
 }
 exports.utcToLocal = utcToLocal;
-function localToUTC(input) {
-    return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
+function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
+    return dayjs(input).utc().format(format);
 }
 exports.localToUTC = localToUTC;
diff --git a/src/util.ts b/src/util.ts
index 966383f8..5d8acfcd 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -25,6 +25,7 @@ export const STATUS_PAGE_MAINTENANCE = 3;
 
 export const SQL_DATE_FORMAT = "YYYY-MM-DD";
 export const SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
+export const SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
 
 /** Flip the status of s */
 export function flipStatus(s: number) {
@@ -412,10 +413,10 @@ export function utcToISODateTime(input : string) {
 /**
  * For SQL_DATETIME_FORMAT
  */
-export function utcToLocal(input : string) {
-    return dayjs.utc(input).local().format(SQL_DATETIME_FORMAT);
+export function utcToLocal(input : string, format = SQL_DATETIME_FORMAT) {
+    return dayjs.utc(input).local().format(format);
 }
 
-export function localToUTC(input : string) {
-    return dayjs(input).utc().format(SQL_DATETIME_FORMAT);
+export function localToUTC(input : string, format = SQL_DATETIME_FORMAT) {
+    return dayjs(input).utc().format(format);
 }

From dfb75c8afb83aee87051966b87d4bb6312433e80 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 11 Oct 2022 20:56:48 +0800
Subject: [PATCH 113/134] Update status page's maintenance message

---
 server/model/status_page.js        | 14 ++++----
 src/components/MaintenanceTime.vue | 44 +++++++++++++++++++++++++
 src/languages/en.js                |  6 ++--
 src/pages/EditMaintenance.vue      | 16 ---------
 src/pages/ManageMaintenance.vue    | 28 ++--------------
 src/pages/StatusPage.vue           | 53 +++++++++++++-----------------
 6 files changed, 80 insertions(+), 81 deletions(-)
 create mode 100644 src/components/MaintenanceTime.vue

diff --git a/server/model/status_page.js b/server/model/status_page.js
index a5a3a4b7..4e7b38cf 100644
--- a/server/model/status_page.js
+++ b/server/model/status_page.js
@@ -91,7 +91,7 @@ class StatusPage extends BeanModel {
             incident = incident.toPublicJSON();
         }
 
-        let maintenance = await StatusPage.getMaintenanceList(statusPage.id);
+        let maintenanceList = await StatusPage.getMaintenanceList(statusPage.id);
 
         // Public Group List
         const publicGroupList = [];
@@ -111,7 +111,7 @@ class StatusPage extends BeanModel {
             config: await statusPage.toPublicJSON(),
             incident,
             publicGroupList,
-            maintenance,
+            maintenanceList,
         };
     }
 
@@ -281,13 +281,13 @@ class StatusPage extends BeanModel {
 
             let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
             let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
-                SELECT m.*
-                FROM maintenance m, maintenance_status_page msp, maintenance_timeslot
-                WHERE  msp.maintenance_id = m.id
-                    AND maintenance_timeslot.maintenance.id = m.id
+                SELECT maintenance.*
+                FROM maintenance, maintenance_status_page msp, maintenance_timeslot
+                WHERE msp.maintenance_id = maintenance.id
+                    AND maintenance_timeslot.maintenance_id = maintenance.id
                     AND msp.status_page_id = ?
                     AND ${activeCondition}
-                ORDER BY m.end_date
+                ORDER BY maintenance.end_date
             `, [ statusPageId ]));
 
             for (const bean of maintenanceBeanList) {
diff --git a/src/components/MaintenanceTime.vue b/src/components/MaintenanceTime.vue
new file mode 100644
index 00000000..66ee4abf
--- /dev/null
+++ b/src/components/MaintenanceTime.vue
@@ -0,0 +1,44 @@
+<template>
+    <div class="timeslot">
+        <div v-if="maintenance.strategy === 'manual'">
+            {{ $t("Manual") }}
+        </div>
+        <div v-else-if="maintenance.timeslotList.length > 0">
+            {{ maintenance.timeslotList[0].startDateServerTimezone }}
+            <span class="to">-</span>
+            {{ maintenance.timeslotList[0].endDateServerTimezone }}
+            (UTC{{ maintenance.timeslotList[0].serverTimezoneOffset }})
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    props: {
+        maintenance: {
+            type: Object,
+            required: true
+        },
+    },
+};
+</script>
+
+<style lang="scss">
+.timeslot {
+    margin-top: 5px;
+    display: inline-block;
+    font-size: 14px;
+    background-color: rgba(255, 255, 255, 0.5);
+    border-radius: 20px;
+    padding: 0 10px;
+
+    .to {
+        margin: 0 6px;
+    }
+
+    .dark & {
+        color: white;
+        background-color: rgba(255, 255, 255, 0.1);
+    }
+}
+</style>
diff --git a/src/languages/en.js b/src/languages/en.js
index a1c9b560..a4c937e6 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -20,13 +20,10 @@ export default {
     "Selected status pages": "Selected status pages",
     "Select status pages...": "Select status pages...",
     recurringIntervalMessage: "Run once every day | Run once every {0} days",
-    "End": "End",
     affectedMonitorsDescription: "Select monitors that are affected by current maintenance",
     affectedStatusPages: "Show this maintenance message on selected status pages",
     atLeastOneMonitor: "Select at least one affected monitor",
-    maintenanceInvalidDate: "Invalid maintenance end date entered",
     selectedStatusPagesDescription: "Select status pages to display maintenance info on",
-    atLeastOneStatusPage: "Select at least one status page",
     maintenanceTitleExample: "Network infrastructure maintenance",
     maintenanceDescriptionExample: "Example: Network infrastructure maintenance is underway which will affect some of our services.",
     passwordNotMatchMsg: "The repeat password does not match.",
@@ -637,4 +634,7 @@ export default {
     "maintenanceStatus-scheduled": "Scheduled",
     "maintenanceStatus-ended": "Ended",
     "maintenanceStatus-unknown": "Unknown",
+    "Display Timezone": "Display Timezone",
+    "Server Timezone": "Server Timezone",
+    statusPageMaintenanceEndDate: "End",
 };
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 24229293..71df19b1 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -422,22 +422,6 @@ export default {
                 return this.processing = false;
             }
 
-            /*
-            TODO: Temporary disable
-            if (this.maintenance.start_date >= this.maintenance.end_date) {
-                toast.error(this.$t("maintenanceInvalidDate"));
-                return this.processing = false;
-            }
-
-            if (!this.showOnAllPages && this.selectedStatusPages.length === 0) {
-                toast.error(this.$t("atLeastOneStatusPage"));
-                return this.processing = false;
-            }
-
-            this.maintenance.start_date = this.$root.toUTC(this.maintenance.start_date);
-            this.maintenance.end_date = this.$root.toUTC(this.maintenance.end_date);
-            */
-
             if (this.isAdd) {
                 this.$root.addMaintenance(this.maintenance, async (res) => {
                     if (res.ok) {
diff --git a/src/pages/ManageMaintenance.vue b/src/pages/ManageMaintenance.vue
index af9c0ba5..9ded0459 100644
--- a/src/pages/ManageMaintenance.vue
+++ b/src/pages/ManageMaintenance.vue
@@ -33,13 +33,7 @@
                                 {{ $t("maintenanceStatus-" + item.status) }}
                             </div>
 
-                            <div v-if="item.strategy === 'manual'" class="timeslot">
-                                {{ $t("Manual") }}
-                            </div>
-                            <div v-else-if="item.timeslotList.length > 0" class="timeslot">
-                                {{ item.timeslotList[0].startDateServerTimezone }} <span class="to">-</span> {{ item.timeslotList[0].endDateServerTimezone }}
-                                (UTC{{ item.timeslotList[0].serverTimezoneOffset }})
-                            </div>
+                            <MaintenanceTime :maintenance="item" />
                         </div>
                     </div>
 
@@ -86,11 +80,13 @@
 import { getResBaseURL } from "../util-frontend";
 import { getMaintenanceRelativeURL } from "../util.ts";
 import Confirm from "../components/Confirm.vue";
+import MaintenanceTime from "../components/MaintenanceTime.vue";
 import { useToast } from "vue-toastification";
 const toast = useToast();
 
 export default {
     components: {
+        MaintenanceTime,
         Confirm,
     },
     data() {
@@ -265,24 +261,6 @@ export default {
                 .status {
                     font-size: 14px;
                 }
-
-                .timeslot {
-                    margin-top: 5px;
-                    display: inline-block;
-                    font-size: 14px;
-                    background-color: rgba(255, 255, 255, 0.5);
-                    border-radius: 20px;
-                    padding: 0 10px;
-
-                    .to {
-                        margin: 0 6px;
-                    }
-
-                    .dark & {
-                        color: white;
-                        background-color: rgba(255, 255, 255, 0.1);
-                    }
-                }
             }
         }
 
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index 699c236d..6cecf668 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -195,33 +195,6 @@
                 </div>
             </div>
 
-            <!-- Maintenance -->
-            <template v-if="maintenance.length">
-                <div
-                    v-for="maintenanceItem in maintenance" :key="maintenanceItem.id"
-                    class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert"
-                >
-                    <div class="item">
-                        <div class="row">
-                            <div class="col-1 col-md-1 d-flex justify-content-center align-items-center">
-                                <font-awesome-icon
-                                    icon="wrench"
-                                    class="maintenance-icon"
-                                />
-                            </div>
-                            <div class="col-11 col-md-11">
-                                <h4 class="alert-heading">{{ maintenanceItem.title }}</h4>
-                                <div class="content">{{ maintenanceItem.description }}</div>
-
-                                <div class="date mt-3">
-                                    {{ $t("End") }}: {{ $root.datetimeMaintenance(maintenanceItem.end_date) }}
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            </template>
-
             <!-- Overall Status -->
             <div class="shadow-box list  p-4 overall-status mb-4">
                 <div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData">
@@ -247,7 +220,7 @@
 
                     <div v-else-if="isMaintenance">
                         <font-awesome-icon icon="wrench" class="status-maintenance" />
-                        {{ $t("Maintenance") }}
+                        {{ $t("maintenanceStatus-under-maintenance") }}
                     </div>
 
                     <div v-else>
@@ -256,6 +229,18 @@
                 </template>
             </div>
 
+            <!-- Maintenance -->
+            <template v-if="maintenanceList.length > 0">
+                <div
+                    v-for="maintenance in maintenanceList" :key="maintenance.id"
+                    class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert"
+                >
+                    <h4 class="alert-heading">{{ maintenance.title }}</h4>
+                    <div class="content">{{ maintenance.description }}</div>
+                    <MaintenanceTime :maintenance="maintenance" />
+                </div>
+            </template>
+
             <!-- Description -->
             <strong v-if="editMode">{{ $t("Description") }}:</strong>
             <Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" />
@@ -327,6 +312,7 @@ import "vue-prism-editor/dist/prismeditor.min.css"; // import the styles somewhe
 import { useToast } from "vue-toastification";
 import Confirm from "../components/Confirm.vue";
 import PublicGroupList from "../components/PublicGroupList.vue";
+import MaintenanceTime from "../components/MaintenanceTime.vue";
 import { getResBaseURL } from "../util-frontend";
 import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE } from "../util.ts";
 
@@ -348,6 +334,7 @@ export default {
         ImageCropUpload,
         Confirm,
         PrismEditor,
+        MaintenanceTime,
     },
 
     // Leave Page for vue route change
@@ -388,7 +375,7 @@ export default {
             loadedData: false,
             baseURL: "",
             clickedEditButton: false,
-            maintenance: [],
+            maintenanceList: [],
         };
     },
     computed: {
@@ -594,7 +581,7 @@ export default {
             }
 
             this.incident = res.data.incident;
-            this.maintenance = res.data.maintenance;
+            this.maintenanceList = res.data.maintenanceList;
             this.$root.publicGroupList = res.data.publicGroupList;
         }).catch( function (error) {
             if (error.response.status === 404) {
@@ -1069,4 +1056,10 @@ footer {
     }
 }
 
+.bg-maintenance {
+    .alert-heading {
+        font-weight: bold;
+    }
+}
+
 </style>

From 39b67251631176dbaf628ef5731133abd9c313a6 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 11 Oct 2022 21:48:43 +0800
Subject: [PATCH 114/134] Update maintenance tables

---
 db/patch-maintenance-table.sql       | 34 -----------------
 db/patch-maintenance-table2.sql      | 56 ++++++++++++++++++++++++++++
 server/database.js                   |  2 +-
 server/model/maintenance_timeslot.js |  2 +-
 server/uptime-kuma-server.js         |  2 +-
 5 files changed, 59 insertions(+), 37 deletions(-)
 delete mode 100644 db/patch-maintenance-table.sql
 create mode 100644 db/patch-maintenance-table2.sql

diff --git a/db/patch-maintenance-table.sql b/db/patch-maintenance-table.sql
deleted file mode 100644
index ce14f766..00000000
--- a/db/patch-maintenance-table.sql
+++ /dev/null
@@ -1,34 +0,0 @@
--- 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 TABLE maintenance_status_page
-(
-    id             INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-    status_page_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_status_page FOREIGN KEY (status_page_id) REFERENCES status_page (id) ON DELETE CASCADE ON UPDATE CASCADE
-);
-
-create index maintenance_user_id on maintenance (user_id);
-
-COMMIT;
diff --git a/db/patch-maintenance-table2.sql b/db/patch-maintenance-table2.sql
new file mode 100644
index 00000000..76644596
--- /dev/null
+++ b/db/patch-maintenance-table2.sql
@@ -0,0 +1,56 @@
+-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
+BEGIN TRANSACTION;
+
+-- Just for someone who tested maintenance before (patch-maintenance-table.sql)
+DROP TABLE IF EXISTS maintenance_status_page;
+DROP TABLE IF EXISTS monitor_maintenance;
+DROP TABLE IF EXISTS maintenance;
+DROP TABLE IF EXISTS maintenance_timeslot;
+
+-- maintenance
+CREATE TABLE [maintenance] (
+    [id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+    [title] VARCHAR(150) NOT NULL,
+    [description] TEXT NOT NULL,
+    [user_id] INTEGER REFERENCES [user]([id]) ON DELETE SET NULL ON UPDATE CASCADE,
+    [active] BOOLEAN NOT NULL DEFAULT 1,
+    [strategy] VARCHAR(50) NOT NULL DEFAULT 'single',
+    [start_date] DATETIME,
+    [end_date] DATETIME,
+    [start_time] TIME,
+    [end_time] TIME,
+    [weekdays] VARCHAR2(250) DEFAULT '[]',
+    [days_of_month] TEXT DEFAULT '[]',
+    [interval_day] INTEGER
+);
+
+CREATE INDEX [maintenance_user_id] ON [maintenance]([user_id]);
+
+-- maintenance_status_page
+CREATE TABLE maintenance_status_page (
+    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+    status_page_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_status_page FOREIGN KEY (status_page_id) REFERENCES status_page (id) ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- maintenance_timeslot
+CREATE TABLE [maintenance_timeslot] (
+    [id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+    [maintenance_id] INTEGER NOT NULL CONSTRAINT [FK_maintenance] REFERENCES [maintenance]([id]) ON DELETE CASCADE ON UPDATE CASCADE,
+    [start_date] DATETIME NOT NULL,
+    [end_date] DATETIME,
+    [generated_next] BOOLEAN DEFAULT 0
+);
+
+-- monitor_maintenance
+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
+);
+
+COMMIT;
diff --git a/server/database.js b/server/database.js
index 5666d9a6..16f12445 100644
--- a/server/database.js
+++ b/server/database.js
@@ -64,7 +64,7 @@ class Database {
         "patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
         "patch-add-radius-monitor.sql": true,
         "patch-monitor-add-resend-interval.sql": true,
-        "patch-maintenance-table.sql": true,
+        "patch-maintenance-table2.sql": true,
     };
 
     /**
diff --git a/server/model/maintenance_timeslot.js b/server/model/maintenance_timeslot.js
index f06806ac..3ac64b6b 100644
--- a/server/model/maintenance_timeslot.js
+++ b/server/model/maintenance_timeslot.js
@@ -7,7 +7,7 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
 class MaintenanceTimeslot extends BeanModel {
 
     async toPublicJSON() {
-        const serverTimezoneOffset = await UptimeKumaServer.getInstance().getTimezoneOffset();
+        const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset();
 
         const obj = {
             id: this.id,
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index 667f6b6a..ac832f8e 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -204,7 +204,7 @@ class UptimeKumaServer {
         }
     }
 
-    async getTimezoneOffset() {
+    getTimezoneOffset() {
         return dayjs().format("Z");
     }
 

From d95e7226586fb504e08d59666e58e5ac6eb534fe Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 11 Oct 2022 22:09:18 +0800
Subject: [PATCH 115/134] Init dayjs for backend.spec.js

---
 test/backend.spec.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/test/backend.spec.js b/test/backend.spec.js
index 5b9fa92c..644a0fd0 100644
--- a/test/backend.spec.js
+++ b/test/backend.spec.js
@@ -6,6 +6,9 @@ const { UptimeKumaServer } = require("../server/uptime-kuma-server");
 const Database = require("../server/database");
 const {Settings} = require("../server/settings");
 const fs = require("fs");
+const dayjs = require("dayjs");
+dayjs.extend(require("dayjs/plugin/utc"));
+dayjs.extend(require("dayjs/plugin/timezone"));
 
 jest.mock("axios");
 

From 7b9766091e79282cb039e2ace956f264a7029d1c Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 11 Oct 2022 22:18:09 +0800
Subject: [PATCH 116/134] Revert testing

---
 server/server.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/server.js b/server/server.js
index f80d5d57..41cf7c37 100644
--- a/server/server.js
+++ b/server/server.js
@@ -1106,7 +1106,7 @@ let needSetup = false;
 
                 callback({
                     ok: true,
-                    msg: "Saved " + dayjs()
+                    msg: "Saved"
                 });
 
                 sendInfo(socket);

From 8cc3e4b7c1a0e69d419f943862d338e9f480f293 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Tue, 11 Oct 2022 22:28:00 +0800
Subject: [PATCH 117/134] Revert

---
 src/languages/en.js    |  1 -
 src/layouts/Layout.vue | 20 +-------------------
 2 files changed, 1 insertion(+), 20 deletions(-)

diff --git a/src/languages/en.js b/src/languages/en.js
index a4c937e6..7db8147f 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -59,7 +59,6 @@ export default {
     "Check Update On GitHub": "Check Update On GitHub",
     List: "List",
     Add: "Add",
-    "Add Monitor": "Add Monitor",
     "Add New Monitor": "Add New Monitor",
     "Quick Stats": "Quick Stats",
     Up: "Up",
diff --git a/src/layouts/Layout.vue b/src/layouts/Layout.vue
index acd9446c..d13e8a8d 100644
--- a/src/layouts/Layout.vue
+++ b/src/layouts/Layout.vue
@@ -103,7 +103,7 @@
 
             <router-link to="/add" class="nav-link">
                 <div><font-awesome-icon icon="plus" /></div>
-                {{ $t("Add Monitor") }}
+                {{ $t("Add") }}
             </router-link>
 
             <router-link to="/settings" class="nav-link">
@@ -317,22 +317,4 @@ main {
         background-color: $dark-bg;
     }
 }
-
-.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>

From 2faf866e9ed5014452a61c96e3df436677ec39cf Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Wed, 12 Oct 2022 17:02:16 +0800
Subject: [PATCH 118/134] Implement generateTimeslot() for recurring interval
 type

---
 server/model/maintenance.js          | 11 +++-
 server/model/maintenance_timeslot.js | 75 ++++++++++++++++++++++++----
 server/server.js                     |  2 +
 server/util-server.js                | 14 +++---
 src/components/MaintenanceTime.vue   |  6 +--
 src/pages/EditMaintenance.vue        |  3 +-
 6 files changed, 87 insertions(+), 24 deletions(-)

diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index 4910d2a0..d1197009 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -1,5 +1,5 @@
 const { BeanModel } = require("redbean-node/dist/bean-model");
-const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC } = require("../../src/util");
+const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
 const { isArray } = require("chart.js/helpers");
 const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
 const { R } = require("redbean-node");
@@ -62,8 +62,15 @@ class Maintenance extends BeanModel {
         } else if (obj.strategy === "manual") {
             obj.status = "under-maintenance";
         } else if (obj.timeslotList.length > 0) {
+            let currentTimestamp = dayjs().unix();
+
             for (let timeslot of obj.timeslotList) {
-                if (dayjs.utc(timeslot.start_date) <= dayjs.utc() && dayjs.utc(timeslot.end_date) >= dayjs.utc()) {
+                if (dayjs.utc(timeslot.startDate).unix() <= currentTimestamp && dayjs.utc(timeslot.endDate).unix() >= currentTimestamp) {
+                    log.debug("timeslot", "Timeslot ID: " + timeslot.id);
+                    log.debug("timeslot", "currentTimestamp:" + currentTimestamp);
+                    log.debug("timeslot", "timeslot.start_date:" + dayjs.utc(timeslot.startDate).unix());
+                    log.debug("timeslot", "timeslot.end_date:" + dayjs.utc(timeslot.endDate).unix());
+
                     obj.status = "under-maintenance";
                     break;
                 }
diff --git a/server/model/maintenance_timeslot.js b/server/model/maintenance_timeslot.js
index 3ac64b6b..4c13632d 100644
--- a/server/model/maintenance_timeslot.js
+++ b/server/model/maintenance_timeslot.js
@@ -1,7 +1,7 @@
 const { BeanModel } = require("redbean-node/dist/bean-model");
 const { R } = require("redbean-node");
 const dayjs = require("dayjs");
-const { log, utcToLocal, SQL_DATETIME_FORMAT_WITHOUT_SECOND } = require("../../src/util");
+const { log, utcToLocal, SQL_DATETIME_FORMAT_WITHOUT_SECOND, localToUTC } = require("../../src/util");
 const { UptimeKumaServer } = require("../uptime-kuma-server");
 
 class MaintenanceTimeslot extends BeanModel {
@@ -26,17 +26,12 @@ class MaintenanceTimeslot extends BeanModel {
     }
 
     /**
-     *
      * @param {Maintenance} maintenance
-     * @param {dayjs} startFrom (For recurring type only) Generate Timeslot from this date, if it is smaller than the current date, it will use the current date instead. As generating a passed timeslot is meaningless.
+     * @param {dayjs} minDate (For recurring type only) Generate a next timeslot from this date.
      * @param {boolean} removeExist Remove existing timeslot before create
-     * @returns {Promise<void>}
+     * @returns {Promise<MaintenanceTimeslot>}
      */
-    static async generateTimeslot(maintenance, startFrom = null, removeExist = false) {
-        if (!startFrom) {
-            startFrom = dayjs();
-        }
-
+    static async generateTimeslot(maintenance, minDate = null, removeExist = false) {
         if (removeExist) {
             await R.exec("DELETE FROM maintenance_timeslot WHERE maintenance_id = ? ", [
                 maintenance.id
@@ -51,9 +46,67 @@ class MaintenanceTimeslot extends BeanModel {
             bean.start_date = maintenance.start_date;
             bean.end_date = maintenance.end_date;
             bean.generated_next = true;
-            await R.store(bean);
+            return await R.store(bean);
         } else if (maintenance.strategy === "recurring-interval") {
-            // TODO
+            let bean = R.dispense("maintenance_timeslot");
+
+            // Prevent dead loop, in case interval_day is not set
+            if (!maintenance.interval_day || maintenance.interval_day <= 0) {
+                maintenance.interval_day = 1;
+            }
+
+            let startOfTheDay = dayjs.utc(maintenance.start_date).format("HH:mm");
+            log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
+
+            // Start Time
+            let startTimeSecond = dayjs.utc(maintenance.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
+            log.debug("timeslot", "startTime: " + startTimeSecond);
+
+            // Duration
+            let duration = dayjs.utc(maintenance.end_time, "HH:mm").diff(dayjs.utc(maintenance.start_time, "HH:mm"), "second");
+            // Add 24hours if it is across day
+            if (duration < 0) {
+                duration += 24 * 3600;
+            }
+
+            // Bake StartDate + StartTime = Start DateTime
+            let startDateTime = dayjs.utc(maintenance.start_date).add(startTimeSecond, "second");
+            let endDateTime;
+
+            // Keep generating from the first possible date, until it is ok
+            while (true) {
+                log.debug("timeslot", "startDateTime: " + startDateTime.format());
+
+                // Handling out of effective date range
+                if (startDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
+                    log.debug("timeslot", "Out of effective date range");
+                    return null;
+                }
+
+                endDateTime = startDateTime.add(duration, "second");
+
+                // If endDateTime is out of effective date range, use the end datetime from effective date range
+                if (endDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
+                    endDateTime = dayjs.utc(maintenance.end_date);
+                }
+
+                // If minDate is set, the endDateTime must be bigger than it.
+                // And the endDateTime must be bigger current time
+                if (
+                    (!minDate || endDateTime.diff(minDate) > 0) &&
+                    endDateTime.diff(dayjs()) > 0
+                ) {
+                    break;
+                }
+
+                startDateTime = startDateTime.add(maintenance.interval_day, "day");
+            }
+
+            bean.maintenance_id = maintenance.id;
+            bean.start_date = localToUTC(startDateTime);
+            bean.end_date = localToUTC(endDateTime);
+            bean.generated_next = false;
+            return await R.store(bean);
         } else if (maintenance.strategy === "recurring-weekday") {
             // TODO
         } else if (maintenance.strategy === "recurring-day-of-month") {
diff --git a/server/server.js b/server/server.js
index 41cf7c37..03b8b6cd 100644
--- a/server/server.js
+++ b/server/server.js
@@ -9,6 +9,7 @@ console.log("Welcome to Uptime Kuma");
 const dayjs = require("dayjs");
 dayjs.extend(require("dayjs/plugin/utc"));
 dayjs.extend(require("dayjs/plugin/timezone"));
+dayjs.extend(require("dayjs/plugin/customParseFormat"));
 
 // Check Node.js Version
 const nodeVersion = parseInt(process.versions.node.split(".")[0]);
@@ -1110,6 +1111,7 @@ let needSetup = false;
                 });
 
                 sendInfo(socket);
+                server.sendMaintenanceList(socket);
 
             } catch (e) {
                 callback({
diff --git a/server/util-server.js b/server/util-server.js
index 7c81cde7..ac8bc488 100644
--- a/server/util-server.js
+++ b/server/util-server.js
@@ -671,18 +671,20 @@ function timeObjectConvertTimezone(obj, timezone, timeObjectToUTC = true) {
     obj.minutes += minutes;
 
     // Handle out of bound
+    if (obj.minutes < 0) {
+        obj.minutes += 60;
+        obj.hours--;
+    } else if (obj.minutes > 60) {
+        obj.minutes -= 60;
+        obj.hours++;
+    }
+
     if (obj.hours < 0) {
         obj.hours += 24;
     } else if (obj.hours > 24) {
         obj.hours -= 24;
     }
 
-    if (obj.minutes < 0) {
-        obj.minutes += 24;
-    } else if (obj.minutes > 24) {
-        obj.minutes -= 24;
-    }
-
     return obj;
 }
 
diff --git a/src/components/MaintenanceTime.vue b/src/components/MaintenanceTime.vue
index 66ee4abf..07d65740 100644
--- a/src/components/MaintenanceTime.vue
+++ b/src/components/MaintenanceTime.vue
@@ -1,9 +1,9 @@
 <template>
-    <div class="timeslot">
-        <div v-if="maintenance.strategy === 'manual'">
+    <div>
+        <div v-if="maintenance.strategy === 'manual'" class="timeslot">
             {{ $t("Manual") }}
         </div>
-        <div v-else-if="maintenance.timeslotList.length > 0">
+        <div v-else-if="maintenance.timeslotList.length > 0" class="timeslot">
             {{ maintenance.timeslotList[0].startDateServerTimezone }}
             <span class="to">-</span>
             {{ maintenance.timeslotList[0].endDateServerTimezone }}
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 71df19b1..8cf28dfb 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -199,8 +199,7 @@
                                         range datePicker
                                         :monthChangeOnScroll="false"
                                         :minDate="minDate"
-                                        :enableTimePicker="false"
-                                        format="yyyy-MM-dd"
+                                        format="yyyy-MM-dd HH:mm:ss"
                                         modelType="yyyy-MM-dd HH:mm:ss"
                                     />
                                 </div>

From edacff123bcb0706a9c18c0112e23176e837d1d0 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Wed, 12 Oct 2022 22:13:07 +0800
Subject: [PATCH 119/134] Add UTC in the serverTimezone dropdown

---
 server/client.js                    | 1 -
 server/model/maintenance.js         | 5 ++---
 src/components/settings/General.vue | 1 +
 src/util.ts                         | 1 -
 4 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/server/client.js b/server/client.js
index bed2ba03..ef96c7f4 100644
--- a/server/client.js
+++ b/server/client.js
@@ -8,7 +8,6 @@ const server = UptimeKumaServer.getInstance();
 const io = server.io;
 const { setting } = require("./util-server");
 const checkVersion = require("./check-version");
-const dayjs = require("dayjs");
 
 /**
  * Send list of notification providers to client
diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index d1197009..a507f870 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -1,6 +1,5 @@
 const { BeanModel } = require("redbean-node/dist/bean-model");
 const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
-const { isArray } = require("chart.js/helpers");
 const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
 const { R } = require("redbean-node");
 const dayjs = require("dayjs");
@@ -48,11 +47,11 @@ class Maintenance extends BeanModel {
             obj.timeslotList.push(await timeslot.toPublicJSON());
         }
 
-        if (!isArray(obj.weekdays)) {
+        if (!Array.isArray(obj.weekdays)) {
             obj.weekdays = [];
         }
 
-        if (!isArray(obj.daysOfMonth)) {
+        if (!Array.isArray(obj.daysOfMonth)) {
             obj.daysOfMonth = [];
         }
 
diff --git a/src/components/settings/General.vue b/src/components/settings/General.vue
index 57c8e0ca..0b8ce83f 100644
--- a/src/components/settings/General.vue
+++ b/src/components/settings/General.vue
@@ -26,6 +26,7 @@
                     {{ $t("Server Timezone") }}
                 </label>
                 <select id="timezone" v-model="settings.serverTimezone" class="form-select">
+                    <option value="UTC">UTC</option>
                     <option
                         v-for="(timezone, index) in timezoneList"
                         :key="index"
diff --git a/src/util.ts b/src/util.ts
index 5d8acfcd..fd2b466b 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -9,7 +9,6 @@
 import * as dayjs  from "dayjs";
 import * as timezone from "dayjs/plugin/timezone";
 import * as utc from "dayjs/plugin/utc";
-import {R} from "redbean-node";
 
 export const isDev = process.env.NODE_ENV === "development";
 export const appName = "Uptime Kuma";

From cb5f90aa89eae0d84c7eb62c98c956b7055183a3 Mon Sep 17 00:00:00 2001
From: MagicFun1241 <nganzikov@gmail.com>
Date: Wed, 12 Oct 2022 19:27:50 +0300
Subject: [PATCH 120/134] Update Russian locale (#2218)

* Update ru-RU.js

* Remove duplicates

* Remove duplicates x2

* Revert to previous version for one translation

* Removed conflicting lines

* Remove conflicting 'Reverse Proxy' key
---
 src/languages/ru-RU.js | 172 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 172 insertions(+)

diff --git a/src/languages/ru-RU.js b/src/languages/ru-RU.js
index 6922f51b..2e6fdf7b 100644
--- a/src/languages/ru-RU.js
+++ b/src/languages/ru-RU.js
@@ -406,4 +406,176 @@ export default {
     proxyDescription: "Прокси должны быть привязаны к монитору, чтобы работать.",
     enableProxyDescription: "Этот прокси не будет влиять на запросы монитора, пока не будет активирован. Вы можете контролировать временное отключение прокси для всех мониторов через статус активации.",
     setAsDefaultProxyDescription: "Этот прокси будет по умолчанию включен для новых мониторов. Вы всё ещё можете отдельно отключать прокси в каждом мониторе.",
+    Invalid: "Недействительный",
+    AccessKeyId: "AccessKey ID",
+    SecretAccessKey: "AccessKey Secret",
+    PhoneNumbers: "PhoneNumbers",
+    TemplateCode: "TemplateCode",
+    SignName: "SignName",
+    "Sms template must contain parameters: ": "Шаблон СМС должен содержать параметры: ",
+    "Bark Endpoint": "Bark Endpoint",
+    "Bark Group": "Bark Group",
+    "Bark Sound": "Bark Sound",
+    WebHookUrl: "WebHookUrl",
+    SecretKey: "SecretKey",
+    "For safety, must use secret key": "В целях безопасности необходимо использовать секретный ключ",
+    "Device Token": "Токен устройства",
+    Platform: "Платформа",
+    iOS: "iOS",
+    Android: "Android",
+    Huawei: "Huawei",
+    High: "High",
+    Retry: "Повторить",
+    Topic: "Тема",
+    "WeCom Bot Key": "WeCom Bot Key",
+    User: "Пользователь",
+    Installed: "Установлено",
+    "Not installed": "Не установлено",
+    Running: "Запускается",
+    "Not running": "Не запускается",
+    "Remove Token": "Удалить токен",
+    Start: "Запустить",
+    Stop: "Остановить",
+    "Uptime Kuma": "Uptime Kuma",
+    Slug: "Slug",
+    "Accept characters:": "Принимаемые символы:",
+    startOrEndWithOnly: "Начинается или кончается только {0}",
+    "No consecutive dashes": "Без последовательных тире",
+    "The slug is already taken. Please choose another slug.": "The slug is already taken. Please choose another slug.",
+    "Page Not Found": "Страница не найдена",
+    wayToGetCloudflaredURL: "(Скачать cloudflared с {0})",
+    cloudflareWebsite: "Cloudflare Website",
+    "Message:": "Сообщение:",
+    "Don't know how to get the token? Please read the guide:": "Don't know how to get the token? Please read the guide:",
+    "The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.",
+    "HTTP Headers": "HTTP заголовки",
+    "Trust Proxy": "Доверять прокси",
+    "Other Software": "Другое программное обеспечение",
+    "For example: nginx, Apache and Traefik.": "К примеру: nginx, Apache и Traefik.",
+    "Please read": "Пожалуйста, прочитайте",
+    "Subject:": "Тема:",
+    "Valid To:": "Действителен до:",
+    "Days Remaining:": "Дней осталось:",
+    "Issuer:": "Издатель:",
+    "Fingerprint:": "Отпечаток:",
+    "No status pages": "Нет статусных страниц",
+    "Domain Name Expiry Notification": "Уведомление об истечении срока действия доменного имени",
+    Proxy: "Прокси",
+    "Date Created": "Дата создания",
+    HomeAssistant: "Home Assistant",
+    onebotHttpAddress: "OneBot HTTP Address",
+    onebotMessageType: "OneBot Message Type",
+    onebotGroupMessage: "Группа",
+    onebotPrivateMessage: "Private",
+    onebotUserOrGroupId: "Группа/ID пользователя",
+    onebotSafetyTips: "В целях безопасности необходимо установить токен доступа",
+    "PushDeer Key": "PushDeer Key",
+    "Footer Text": "Текст нижнего колонтитула",
+    "Show Powered By": "Показывать на чем создано",
+    "Domain Names": "Доменные имена",
+    signedInDisp: "Вы вошли как {0}",
+    signedInDispDisabled: "Аутентификация отключена.",
+    RadiusSecret: "Секрет Radius",
+    RadiusSecretDescription: "Общий секрет между клиентом и сервером",
+    RadiusCalledStationId: "Идентификатор вызываемой станции",
+    RadiusCalledStationIdDescription: "Идентификатор вызываемого устройства",
+    RadiusCallingStationId: "Идентификатор вызывающей станции",
+    RadiusCallingStationIdDescription: "Идентификатор вызывающего устройства",
+    "Certificate Expiry Notification": "Уведомление об истечении срока действия сертификата",
+    "API Username": "Имя пользователя API",
+    "API Key": "API ключ",
+    "Recipient Number": "Номер получателя",
+    "From Name/Number": "Имя/номер отправителя",
+    "Leave blank to use a shared sender number.": "Оставьте пустым, чтобы использовать общий номер отправителя.",
+    "Octopush API Version": "Версия API Octopush",
+    "Legacy Octopush-DM": "Legacy Octopush-DM",
+    endpoint: "endpoint",
+    octopushAPIKey: "\"API key\" из учетных данных HTTP API в панели управления",
+    octopushLogin: "\"Login\" из учетных данных HTTP API в панели управления",
+    promosmsLogin: "Логин API",
+    promosmsPassword: "Пароль API",
+    "pushoversounds pushover": "Pushover (default)",
+    "pushoversounds bike": "Bike",
+    "pushoversounds bugle": "Bugle",
+    "pushoversounds cashregister": "Cash Register",
+    "pushoversounds classical": "Classical",
+    "pushoversounds cosmic": "Cosmic",
+    "pushoversounds falling": "Falling",
+    "pushoversounds gamelan": "Gamelan",
+    "pushoversounds incoming": "Incoming",
+    "pushoversounds intermission": "Intermission",
+    "pushoversounds magic": "Magic",
+    "pushoversounds mechanical": "Mechanical",
+    "pushoversounds pianobar": "Piano Bar",
+    "pushoversounds siren": "Siren",
+    "pushoversounds spacealarm": "Space Alarm",
+    "pushoversounds tugboat": "Tug Boat",
+    "pushoversounds alien": "Alien Alarm (long)",
+    "pushoversounds climb": "Climb (long)",
+    "pushoversounds persistent": "Persistent (long)",
+    "pushoversounds echo": "Pushover Echo (long)",
+    "pushoversounds updown": "Up Down (long)",
+    "pushoversounds vibrate": "Vibrate Only",
+    "pushoversounds none": "None (silent)",
+    pushyAPIKey: "Secret API Key",
+    pushyToken: "Токен устройства",
+    "Using a Reverse Proxy?": "Используете обратный прокси?",
+    "Check how to config it for WebSocket": "Проверьте, как настроить его для WebSocket",
+    "Steam Game Server": "Steam Game Server",
+    "Most likely causes:": "Наиболее вероятные причины:",
+    "The resource is no longer available.": "Ресурс больше не доступен.",
+    "There might be a typing error in the address.": "В адресе может быть опечатка.",
+    "What you can try:": "Что вы можете попробовать:",
+    "Retype the address.": "Повторите адрес.",
+    "Go back to the previous page.": "Вернуться на предыдущую страницу.",
+    "Coming Soon": "Скоро",
+    wayToGetClickSendSMSToken: "Вы можете получить имя пользователя API и ключ API из {0} .",
+    "Connection String": "Строка подключения",
+    Query: "Запрос",
+    settingsCertificateExpiry: "Истекание TLS сертификата",
+    certificationExpiryDescription: "HTTPS Мониторы инициируют уведомление, когда срок действия сертификата TLS истечет:",
+    "Setup Docker Host": "Настроить Docker Host",
+    "Connection Type": "Тип соединения",
+    "Docker Daemon": "Docker Daemon",
+    deleteDockerHostMsg: "Are you sure want to delete this docker host for all monitors?",
+    socket: "Socket",
+    tcp: "TCP / HTTP",
+    "Docker Container": "Docker контейнер",
+    "Container Name / ID": "Название контейнера / ID",
+    "Docker Host": "Docker Host",
+    "Docker Hosts": "Docker Hosts",
+    "ntfy Topic": "ntfy Topic",
+    Domain: "Домен",
+    Workstation: "Workstation",
+    disableCloudflaredNoAuthMsg: "Вы находитесь в режиме без авторизации, пароль не требуется.",
+    trustProxyDescription: "Доверять заголовкам 'X-Forwarded-*'. Если вы хотите получить правильный IP-адрес клиента, а ваш Uptime Kuma находится под Nginx или Apache, вам следует включить этот параметр.",
+    wayToGetLineNotifyToken: "Вы можете получить токен доступа в {0}",
+    Examples: "Примеры",
+    "Home Assistant URL": "Home Assistant URL",
+    "Long-Lived Access Token": "Токен доступа с длительным сроком службы",
+    "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ",
+    "Notification Service": "Служба уведомлений",
+    "default: notify all devices": "по стандарту: уведомлять все устройства",
+    "A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.",
+    "Automations can optionally be triggered in Home Assistant:": "При желании автоматизацию можно активировать в Home Assistant.:",
+    "Trigger type:": "Тип триггера:",
+    "Event type:": "Тип события:",
+    "Event data:": "Данные события:",
+    "Then choose an action, for example switch the scene to where an RGB light is red.": "Затем выберите действие, например, переключите сцену на красный индикатор RGB..",
+    "Frontend Version": "Версия интерфейса",
+    "Frontend Version do not match backend version!": "Версия интерфейса не соответствует версии серверной части!",
+    "Base URL": "Базовый URL",
+    goAlertInfo: "GoAlert is a An open source application for on-call scheduling, automated escalations and notifications (like SMS or voice calls). Automatically engage the right person, the right way, and at the right time! {0}",
+    goAlertIntegrationKeyInfo: "Получить общий ключ интеграции API для сервиса в этом формате \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" обычно значение параметра токена скопированного URL.",
+    goAlert: "GoAlert",
+    backupOutdatedWarning: "Устарело: поскольку добавлено множество функций, а эта функция резервного копирования немного не поддерживается, она не может создать или восстановить полную резервную копию.",
+    backupRecommend: "Сделайте резервную копию тома или папки с данными (./data/) напрямую.",
+    "Optional": "Необязательно",
+    squadcast: "Squadcast",
+    SendKey: "SendKey",
+    "SMSManager API Docs": "Документация к API SMSManager ",
+    "Gateway Type": "Тип шлюза",
+    SMSManager: "SMSManager",
+    "You can divide numbers with": "Вы можете делить числа с",
+    "or": "или",
 };

From f459ea845c6a1b192fa35efa44f3116aca921dd1 Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Wed, 12 Oct 2022 17:32:05 +0100
Subject: [PATCH 121/134] Added #2182 Add support for custom radius ports
 (#2197)

This commit adds support for the port to be specified when using the
radius monitor type. A check has been implemented to ensure that a null
value is not passed to the radius check function as could occur with
monitors that were created before this change was introduced. The
default port of 1812 is displayed when the user selects the radius
monitor in much the same way as the DNS port is handled. The port was
not included in the hostname in the form hostname:port in order to avoid
issues with IPv6 addresses and monitors that had been created before
this change was implemented.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 server/model/monitor.js   | 14 +++++++++++++-
 server/util-server.js     | 13 +++++++++++++
 src/pages/EditMonitor.vue |  8 +++++---
 3 files changed, 31 insertions(+), 4 deletions(-)

diff --git a/server/model/monitor.js b/server/model/monitor.js
index ac892560..c0a3cce6 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -534,6 +534,17 @@ class Monitor extends BeanModel {
                     bean.ping = dayjs().valueOf() - startTime;
                 } else if (this.type === "radius") {
                     let startTime = dayjs().valueOf();
+
+                    // Handle monitors that were created before the
+                    // update and as such don't have a value for
+                    // this.port.
+                    let port;
+                    if (this.port == null) {
+                        port = 1812;
+                    } else {
+                        port = this.port;
+                    }
+
                     try {
                         const resp = await radius(
                             this.hostname,
@@ -541,7 +552,8 @@ class Monitor extends BeanModel {
                             this.radiusPassword,
                             this.radiusCalledStationId,
                             this.radiusCallingStationId,
-                            this.radiusSecret
+                            this.radiusSecret,
+                            port
                         );
                         if (resp.code) {
                             bean.msg = resp.code;
diff --git a/server/util-server.js b/server/util-server.js
index cf303ba8..b975a43f 100644
--- a/server/util-server.js
+++ b/server/util-server.js
@@ -291,6 +291,17 @@ exports.postgresQuery = function (connectionString, query) {
     });
 };
 
+/**
+ * Query radius server
+ * @param {string} hostname Hostname of radius server
+ * @param {string} username Username to use
+ * @param {string} password Password to use
+ * @param {string} calledStationId ID of called station
+ * @param {string} callingStationId ID of calling station
+ * @param {string} secret Secret to use
+ * @param {number} [port=1812] Port to contact radius server on
+ * @returns {Promise<any>}
+ */
 exports.radius = function (
     hostname,
     username,
@@ -298,9 +309,11 @@ exports.radius = function (
     calledStationId,
     callingStationId,
     secret,
+    port = 1812,
 ) {
     const client = new radiusClient({
         host: hostname,
+        hostPort: port,
         dictionaries: [ file ],
     });
 
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index 99cbeb95..6d1a7e51 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -97,8 +97,8 @@
                             </div>
 
                             <!-- Port -->
-                            <!-- For TCP Port / Steam / MQTT Type -->
-                            <div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'mqtt'" class="my-3">
+                            <!-- For TCP Port / Steam / MQTT / Radius Type -->
+                            <div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
                                 <label for="port" class="form-label">{{ $t("Port") }}</label>
                                 <input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
                             </div>
@@ -616,9 +616,11 @@ export default {
             }
 
             // Set default port for DNS if not already defined
-            if (! this.monitor.port || this.monitor.port === "53") {
+            if (! this.monitor.port || this.monitor.port === "53" || this.monitor.port === "1812") {
                 if (this.monitor.type === "dns") {
                     this.monitor.port = "53";
+                } else if (this.monitor.type === "radius") {
+                    this.monitor.port = "1812";
                 } else {
                     this.monitor.port = undefined;
                 }

From c662d259b0a59e59a650f00a04253e0d81707ab2 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Thu, 13 Oct 2022 19:28:02 +0800
Subject: [PATCH 122/134]  Firefox Better Support #2206

---
 src/components/HiddenInput.vue                | 2 +-
 src/components/notifications/ClickSendSMS.vue | 2 +-
 src/components/notifications/GoAlert.vue      | 2 +-
 src/components/notifications/Gotify.vue       | 2 +-
 src/components/notifications/Line.vue         | 2 +-
 src/components/notifications/Matrix.vue       | 2 +-
 src/components/notifications/Octopush.vue     | 2 +-
 src/components/notifications/PromoSMS.vue     | 2 +-
 src/components/notifications/PushDeer.vue     | 2 +-
 src/components/notifications/Pushbullet.vue   | 2 +-
 src/components/notifications/Pushover.vue     | 4 ++--
 src/components/notifications/Pushy.vue        | 4 ++--
 src/components/notifications/SMTP.vue         | 2 +-
 src/components/notifications/ServerChan.vue   | 2 +-
 src/components/notifications/SerwerSMS.vue    | 2 +-
 src/components/notifications/TechulusPush.vue | 2 +-
 src/components/notifications/Telegram.vue     | 2 +-
 src/components/settings/General.vue           | 5 +++--
 src/components/settings/ReverseProxy.vue      | 2 +-
 19 files changed, 23 insertions(+), 22 deletions(-)

diff --git a/src/components/HiddenInput.vue b/src/components/HiddenInput.vue
index 6287af05..fb86a398 100644
--- a/src/components/HiddenInput.vue
+++ b/src/components/HiddenInput.vue
@@ -42,7 +42,7 @@ export default {
         /** Should the field auto complete */
         autocomplete: {
             type: String,
-            default: undefined,
+            default: "new-password",
         },
         /** Is the input required? */
         required: {
diff --git a/src/components/notifications/ClickSendSMS.vue b/src/components/notifications/ClickSendSMS.vue
index dbd4d0aa..dbaca045 100644
--- a/src/components/notifications/ClickSendSMS.vue
+++ b/src/components/notifications/ClickSendSMS.vue
@@ -6,7 +6,7 @@
         </i18n-t>
         <input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
         <label for="clicksendsms-key" class="form-label">{{ $t("API Key") }}</label>
-        <HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
+        <HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
     </div>
     <div class="mb-3">
         <div class="form-text">
diff --git a/src/components/notifications/GoAlert.vue b/src/components/notifications/GoAlert.vue
index a1465b50..cefb848d 100644
--- a/src/components/notifications/GoAlert.vue
+++ b/src/components/notifications/GoAlert.vue
@@ -11,7 +11,7 @@
 
     <div class="mb-3">
         <label for="goalert-token" class="form-label">{{ $t("Token") }}</label>
-        <HiddenInput id="goalert-token" v-model="$parent.notification.goAlertToken" autocomplete="one-time-code" :required="true"></HiddenInput>
+        <HiddenInput id="goalert-token" v-model="$parent.notification.goAlertToken" autocomplete="new-password" :required="true"></HiddenInput>
 
         <div class="form-text">
             {{ $t("goAlertIntegrationKeyInfo") }}
diff --git a/src/components/notifications/Gotify.vue b/src/components/notifications/Gotify.vue
index 5c294f6d..8b9a8a16 100644
--- a/src/components/notifications/Gotify.vue
+++ b/src/components/notifications/Gotify.vue
@@ -1,7 +1,7 @@
 <template>
     <div class="mb-3">
         <label for="gotify-application-token" class="form-label">{{ $t("Application Token") }}</label>
-        <HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
+        <HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="new-password"></HiddenInput>
     </div>
     <div class="mb-3">
         <label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>
diff --git a/src/components/notifications/Line.vue b/src/components/notifications/Line.vue
index 34ceb4ac..dcd6142b 100644
--- a/src/components/notifications/Line.vue
+++ b/src/components/notifications/Line.vue
@@ -1,7 +1,7 @@
 <template>
     <div class="mb-3">
         <label for="line-channel-access-token" class="form-label">{{ $t("Channel access token") }}</label>
-        <HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
+        <HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="new-password"></HiddenInput>
     </div>
     <i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
         <b>{{ $t("Basic Settings") }}</b>
diff --git a/src/components/notifications/Matrix.vue b/src/components/notifications/Matrix.vue
index d4790646..a9fd6340 100644
--- a/src/components/notifications/Matrix.vue
+++ b/src/components/notifications/Matrix.vue
@@ -9,7 +9,7 @@
     </div>
     <div class="mb-3">
         <label for="access-token" class="form-label">{{ $t("Access Token") }}</label><span style="color: red;"><sup>*</sup></span>
-        <HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="one-time-code" :maxlength="500"></HiddenInput>
+        <HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="new-password" :maxlength="500"></HiddenInput>
     </div>
 
     <div class="form-text">
diff --git a/src/components/notifications/Octopush.vue b/src/components/notifications/Octopush.vue
index 7d5fe469..15cebe8b 100644
--- a/src/components/notifications/Octopush.vue
+++ b/src/components/notifications/Octopush.vue
@@ -11,7 +11,7 @@
     </div>
     <div class="mb-3">
         <label for="octopush-key" class="form-label">{{ $t("octopushAPIKey") }}</label>
-        <HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
+        <HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
         <label for="octopush-login" class="form-label">{{ $t("octopushLogin") }}</label>
         <input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
     </div>
diff --git a/src/components/notifications/PromoSMS.vue b/src/components/notifications/PromoSMS.vue
index 93ecdc8c..03c02222 100644
--- a/src/components/notifications/PromoSMS.vue
+++ b/src/components/notifications/PromoSMS.vue
@@ -3,7 +3,7 @@
         <label for="promosms-login" class="form-label">{{ $t("promosmsLogin") }}</label>
         <input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
         <label for="promosms-key" class="form-label">{{ $t("promosmsPassword") }}</label>
-        <HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
+        <HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
     </div>
     <div class="mb-3">
         <label for="promosms-type-sms" class="form-label">{{ $t("SMS Type") }}</label>
diff --git a/src/components/notifications/PushDeer.vue b/src/components/notifications/PushDeer.vue
index c2b7f5cb..f6184963 100644
--- a/src/components/notifications/PushDeer.vue
+++ b/src/components/notifications/PushDeer.vue
@@ -1,7 +1,7 @@
 <template>
     <div class="mb-3">
         <label for="pushdeer-key" class="form-label">{{ $t("PushDeer Key") }}</label>
-        <HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="one-time-code" placeholder="PDUxxxx"></HiddenInput>
+        <HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="new-password" placeholder="PDUxxxx"></HiddenInput>
     </div>
 
     <i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
diff --git a/src/components/notifications/Pushbullet.vue b/src/components/notifications/Pushbullet.vue
index 37a2e095..866576aa 100644
--- a/src/components/notifications/Pushbullet.vue
+++ b/src/components/notifications/Pushbullet.vue
@@ -1,7 +1,7 @@
 <template>
     <div class="mb-3">
         <label for="pushbullet-access-token" class="form-label">{{ $t("Access Token") }}</label>
-        <HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
+        <HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="new-password"></HiddenInput>
     </div>
 
     <i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
diff --git a/src/components/notifications/Pushover.vue b/src/components/notifications/Pushover.vue
index 4bf6edb3..d9f24b29 100644
--- a/src/components/notifications/Pushover.vue
+++ b/src/components/notifications/Pushover.vue
@@ -1,9 +1,9 @@
 <template>
     <div class="mb-3">
         <label for="pushover-user" class="form-label">{{ $t("User Key") }}<span style="color: red;"><sup>*</sup></span></label>
-        <HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
+        <HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="new-password"></HiddenInput>
         <label for="pushover-app-token" class="form-label">{{ $t("Application Token") }}<span style="color: red;"><sup>*</sup></span></label>
-        <HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
+        <HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="new-password"></HiddenInput>
         <label for="pushover-device" class="form-label">{{ $t("Device") }}</label>
         <input id="pushover-device" v-model="$parent.notification.pushoverdevice" type="text" class="form-control">
         <label for="pushover-device" class="form-label">{{ $t("Message Title") }}</label>
diff --git a/src/components/notifications/Pushy.vue b/src/components/notifications/Pushy.vue
index 3537eb4f..13e7b499 100644
--- a/src/components/notifications/Pushy.vue
+++ b/src/components/notifications/Pushy.vue
@@ -1,13 +1,13 @@
 <template>
     <div class="mb-3">
         <label for="pushy-app-token" class="form-label">{{ $t("pushyAPIKey") }}</label>
-        <HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
+        <HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
     </div>
 
     <div class="mb-3">
         <label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
         <div class="input-group mb-3">
-            <HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
+            <HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="new-password"></HiddenInput>
         </div>
     </div>
     <i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
diff --git a/src/components/notifications/SMTP.vue b/src/components/notifications/SMTP.vue
index 899f8f9b..54470796 100644
--- a/src/components/notifications/SMTP.vue
+++ b/src/components/notifications/SMTP.vue
@@ -34,7 +34,7 @@
 
         <div class="mb-3">
             <label for="password" class="form-label">{{ $t("Password") }}</label>
-            <HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="false" autocomplete="one-time-code"></HiddenInput>
+            <HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="false" autocomplete="new-password"></HiddenInput>
         </div>
 
         <div class="mb-3">
diff --git a/src/components/notifications/ServerChan.vue b/src/components/notifications/ServerChan.vue
index cec75675..c7476c20 100644
--- a/src/components/notifications/ServerChan.vue
+++ b/src/components/notifications/ServerChan.vue
@@ -1,7 +1,7 @@
 <template>
     <div class="mb-3">
         <label for="serverchan-sendkey" class="form-label">{{ $t("SendKey") }}</label>
-        <HiddenInput id="serverchan-sendkey" v-model="$parent.notification.serverChanSendKey" :required="true" autocomplete="one-time-code"></HiddenInput>
+        <HiddenInput id="serverchan-sendkey" v-model="$parent.notification.serverChanSendKey" :required="true" autocomplete="new-password"></HiddenInput>
     </div>
 </template>
 
diff --git a/src/components/notifications/SerwerSMS.vue b/src/components/notifications/SerwerSMS.vue
index f2c3463b..32a0ff7a 100644
--- a/src/components/notifications/SerwerSMS.vue
+++ b/src/components/notifications/SerwerSMS.vue
@@ -5,7 +5,7 @@
     </div>
     <div class="mb-3">
         <label for="serwersms-key" class="form-label">{{ $t('serwersmsAPIPassword') }}</label>
-        <HiddenInput id="serwersms-key" v-model="$parent.notification.serwersmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
+        <HiddenInput id="serwersms-key" v-model="$parent.notification.serwersmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
     </div>
     <div class="mb-3">
         <label for="serwersms-phone-number" class="form-label">{{ $t("serwersmsPhoneNumber") }}</label>
diff --git a/src/components/notifications/TechulusPush.vue b/src/components/notifications/TechulusPush.vue
index 86d4e5fe..bece17e2 100644
--- a/src/components/notifications/TechulusPush.vue
+++ b/src/components/notifications/TechulusPush.vue
@@ -1,7 +1,7 @@
 <template>
     <div class="mb-3">
         <label for="push-api-key" class="form-label">{{ $t("API Key") }}</label>
-        <HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
+        <HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
     </div>
 
     <i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
diff --git a/src/components/notifications/Telegram.vue b/src/components/notifications/Telegram.vue
index 20deff7b..9daf31ac 100644
--- a/src/components/notifications/Telegram.vue
+++ b/src/components/notifications/Telegram.vue
@@ -1,7 +1,7 @@
 <template>
     <div class="mb-3">
         <label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
-        <HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput>
+        <HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="new-password"></HiddenInput>
         <i18n-t tag="div" keypath="wayToGetTelegramToken" class="form-text">
             <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>
         </i18n-t>
diff --git a/src/components/settings/General.vue b/src/components/settings/General.vue
index 19dc8077..c460dc53 100644
--- a/src/components/settings/General.vue
+++ b/src/components/settings/General.vue
@@ -1,6 +1,6 @@
 <template>
     <div>
-        <form class="my-4" @submit.prevent="saveGeneral">
+        <form class="my-4" autocomplete="off" @submit.prevent="saveGeneral">
             <!-- Timezone -->
             <div class="mb-4">
                 <label for="timezone" class="form-label">
@@ -105,6 +105,7 @@
                         name="primaryBaseURL"
                         placeholder="https://"
                         pattern="https?://.+"
+                        autocomplete="new-password"
                     />
                     <button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryBaseURL">
                         {{ $t("Auto Get") }}
@@ -122,7 +123,7 @@
                 <HiddenInput
                     id="steamAPIKey"
                     v-model="settings.steamAPIKey"
-                    autocomplete="one-time-code"
+                    autocomplete="new-password"
                 />
                 <div class="form-text">
                     {{ $t("steamApiKeyDescription") }}
diff --git a/src/components/settings/ReverseProxy.vue b/src/components/settings/ReverseProxy.vue
index 0fab7629..04ed9c0c 100644
--- a/src/components/settings/ReverseProxy.vue
+++ b/src/components/settings/ReverseProxy.vue
@@ -41,7 +41,7 @@
                 <HiddenInput
                     id="cloudflareTunnelToken"
                     v-model="cloudflareTunnelToken"
-                    autocomplete="one-time-code"
+                    autocomplete="new-password"
                     :readonly="running"
                 />
                 <div class="form-text">

From aecf95864eaf2c2d9a939bdca7336c88d05f4ac8 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Fri, 14 Oct 2022 13:26:41 +0800
Subject: [PATCH 123/134] Add index for maintenance tables

---
 db/patch-maintenance-table2.sql | 27 ++++++++++++++++++++++++++-
 1 file changed, 26 insertions(+), 1 deletion(-)

diff --git a/db/patch-maintenance-table2.sql b/db/patch-maintenance-table2.sql
index 76644596..24f6da8c 100644
--- a/db/patch-maintenance-table2.sql
+++ b/db/patch-maintenance-table2.sql
@@ -24,7 +24,14 @@ CREATE TABLE [maintenance] (
     [interval_day] INTEGER
 );
 
-CREATE INDEX [maintenance_user_id] ON [maintenance]([user_id]);
+CREATE INDEX [manual_active] ON [maintenance] (
+    [strategy],
+    [active]
+);
+
+CREATE INDEX [active] ON [maintenance] ([active]);
+
+CREATE INDEX [maintenance_user_id] ON [maintenance] ([user_id]);
 
 -- maintenance_status_page
 CREATE TABLE maintenance_status_page (
@@ -35,6 +42,12 @@ CREATE TABLE maintenance_status_page (
     CONSTRAINT FK_status_page FOREIGN KEY (status_page_id) REFERENCES status_page (id) ON DELETE CASCADE ON UPDATE CASCADE
 );
 
+CREATE INDEX [status_page_id_index]
+    ON [maintenance_status_page]([status_page_id]);
+
+CREATE INDEX [maintenance_id_index]
+    ON [maintenance_status_page]([maintenance_id]);
+
 -- maintenance_timeslot
 CREATE TABLE [maintenance_timeslot] (
     [id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
@@ -44,6 +57,14 @@ CREATE TABLE [maintenance_timeslot] (
     [generated_next] BOOLEAN DEFAULT 0
 );
 
+CREATE INDEX [maintenance_id] ON [maintenance_timeslot] ([maintenance_id] DESC);
+
+CREATE INDEX [active_timeslot_index] ON [maintenance_timeslot] (
+    [maintenance_id] DESC,
+    [start_date] DESC,
+    [end_date] DESC
+);
+
 -- monitor_maintenance
 CREATE TABLE monitor_maintenance (
     id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
@@ -53,4 +74,8 @@ CREATE TABLE monitor_maintenance (
     CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor (id) ON DELETE CASCADE ON UPDATE CASCADE
 );
 
+CREATE INDEX [maintenance_id_index2] ON [monitor_maintenance]([maintenance_id]);
+
+CREATE INDEX [monitor_id_index] ON [monitor_maintenance]([monitor_id]);
+
 COMMIT;

From b60dde0b2d62eaaf550a85965cfc5f735fea78bb Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 15 Oct 2022 15:18:54 +0800
Subject: [PATCH 124/134] Update SQLite

---
 package-lock.json | 18 +++++++++---------
 package.json      |  2 +-
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 44af1469..77678352 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,15 @@
 {
     "name": "uptime-kuma",
-    "version": "1.18.4",
+    "version": "1.18.5",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "uptime-kuma",
-            "version": "1.18.4",
+            "version": "1.18.5",
             "license": "MIT",
             "dependencies": {
-                "@louislam/sqlite3": "~15.0.6",
+                "@louislam/sqlite3": "15.1.2",
                 "args-parser": "~1.3.0",
                 "axios": "~0.27.0",
                 "axios-ntlm": "~1.3.0",
@@ -3115,9 +3115,9 @@
             "integrity": "sha512-iHHyIRLEfXLqBN+BkyH8u8imMYr4ihRbFDEk8toqTwUECETVQFCTh2U59Sw2oMoRVaS3XRIb7pyCulltq2jFVA=="
         },
         "node_modules/@louislam/sqlite3": {
-            "version": "15.0.6",
-            "resolved": "https://registry.npmjs.org/@louislam/sqlite3/-/sqlite3-15.0.6.tgz",
-            "integrity": "sha512-+HF/4OEy+yakYzJlSPJbLDtf499t0s0eaglXC9y3Oa9OBZ+dKAaTW5+Ft1RCvfUJLFw/oyYjHtMsg9V+7NT05g==",
+            "version": "15.1.2",
+            "resolved": "https://registry.npmjs.org/@louislam/sqlite3/-/sqlite3-15.1.2.tgz",
+            "integrity": "sha512-VRquWrCKKwfOnzwVh6hOud8lHPvv2R7Jic3gyZCL5kiZpNfmJ71DLCV9SNgLaMDloU+mVWymLev8vehlf7xf5g==",
             "hasInstallScript": true,
             "dependencies": {
                 "@mapbox/node-pre-gyp": "^1.0.0",
@@ -18851,9 +18851,9 @@
             "integrity": "sha512-iHHyIRLEfXLqBN+BkyH8u8imMYr4ihRbFDEk8toqTwUECETVQFCTh2U59Sw2oMoRVaS3XRIb7pyCulltq2jFVA=="
         },
         "@louislam/sqlite3": {
-            "version": "15.0.6",
-            "resolved": "https://registry.npmjs.org/@louislam/sqlite3/-/sqlite3-15.0.6.tgz",
-            "integrity": "sha512-+HF/4OEy+yakYzJlSPJbLDtf499t0s0eaglXC9y3Oa9OBZ+dKAaTW5+Ft1RCvfUJLFw/oyYjHtMsg9V+7NT05g==",
+            "version": "15.1.2",
+            "resolved": "https://registry.npmjs.org/@louislam/sqlite3/-/sqlite3-15.1.2.tgz",
+            "integrity": "sha512-VRquWrCKKwfOnzwVh6hOud8lHPvv2R7Jic3gyZCL5kiZpNfmJ71DLCV9SNgLaMDloU+mVWymLev8vehlf7xf5g==",
             "requires": {
                 "@mapbox/node-pre-gyp": "^1.0.0",
                 "node-addon-api": "^4.2.0",
diff --git a/package.json b/package.json
index b1a76d36..ee7496d9 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
         "cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\""
     },
     "dependencies": {
-        "@louislam/sqlite3": "~15.0.6",
+        "@louislam/sqlite3": "15.1.2",
         "args-parser": "~1.3.0",
         "axios": "~0.27.0",
         "axios-ntlm": "~1.3.0",

From 0b8d4cdaac1c613c27bb15ff9119ec6a72931a1b Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 15 Oct 2022 17:17:26 +0800
Subject: [PATCH 125/134] Generate Next Timeslot for recurring interval

---
 db/patch-maintenance-table2.sql |  2 ++
 server/server.js                |  2 ++
 server/uptime-kuma-server.js    | 25 +++++++++++++++++++++++++
 3 files changed, 29 insertions(+)

diff --git a/db/patch-maintenance-table2.sql b/db/patch-maintenance-table2.sql
index 24f6da8c..96b2ebde 100644
--- a/db/patch-maintenance-table2.sql
+++ b/db/patch-maintenance-table2.sql
@@ -65,6 +65,8 @@ CREATE INDEX [active_timeslot_index] ON [maintenance_timeslot] (
     [end_date] DESC
 );
 
+CREATE INDEX [generated_next_index] ON [maintenance_timeslot] ([generated_next]);
+
 -- monitor_maintenance
 CREATE TABLE monitor_maintenance (
     id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
diff --git a/server/server.js b/server/server.js
index fb6a0a21..888294b1 100644
--- a/server/server.js
+++ b/server/server.js
@@ -1721,6 +1721,8 @@ async function shutdownFunction(signal) {
     log.info("server", "Shutdown requested");
     log.info("server", "Called signal: " + signal);
 
+    await server.stop();
+
     log.info("server", "Stopping all monitors");
     for (let id in server.monitorList) {
         let monitor = server.monitorList[id];
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index ac832f8e..f84bf312 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -45,6 +45,8 @@ class UptimeKumaServer {
      */
     indexHTML = "";
 
+    generateMaintenanceTimeslotsInterval = undefined;
+
     static getInstance(args) {
         if (UptimeKumaServer.instance == null) {
             UptimeKumaServer.instance = new UptimeKumaServer(args);
@@ -90,6 +92,9 @@ class UptimeKumaServer {
         dayjs.tz.setDefault(process.env.TZ);
         log.debug("DEBUG", "Timezone: " + process.env.TZ);
         log.debug("DEBUG", "Current Time: " + dayjs.tz().format());
+
+        await this.generateMaintenanceTimeslots();
+        this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
     }
 
     async sendMonitorList(socket) {
@@ -213,8 +218,28 @@ class UptimeKumaServer {
         process.env.TZ = timezone;
         dayjs.tz.setDefault(timezone);
     }
+
+    async generateMaintenanceTimeslots() {
+
+        let list = await R.find("maintenance_timeslot", " generated_next = 0 AND start_date <= DATETIME('now') ");
+
+        for (let maintenanceTimeslot of list) {
+            let maintenance = await maintenanceTimeslot.maintenance;
+            await MaintenanceTimeslot.generateTimeslot(maintenance, maintenanceTimeslot.end_date, false);
+            maintenanceTimeslot.generated_next = true;
+            await R.store(maintenanceTimeslot);
+        }
+
+    }
+
+    async stop() {
+        clearTimeout(this.generateMaintenanceTimeslotsInterval);
+    }
 }
 
 module.exports = {
     UptimeKumaServer
 };
+
+// Must be at the end
+const MaintenanceTimeslot = require("./model/maintenance_timeslot");

From d8a676abb63d7f49691f84b3b609a5b8e9d1f2a6 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 15 Oct 2022 18:49:09 +0800
Subject: [PATCH 126/134] Implement recurring day of month and day of week

---
 server/model/maintenance.js          |  34 ++++++
 server/model/maintenance_timeslot.js | 168 ++++++++++++++++++---------
 src/languages/en.js                  |   1 +
 src/pages/EditMaintenance.vue        |  11 +-
 4 files changed, 154 insertions(+), 60 deletions(-)

diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index a507f870..35030801 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -112,6 +112,40 @@ class Maintenance extends BeanModel {
         return this.toPublicJSON(timezone);
     }
 
+    getDayOfWeekList() {
+        log.debug("timeslot", "List: " + this.weekdays);
+        return JSON.parse(this.weekdays).sort(function (a, b) {
+            return a - b;
+        });
+    }
+
+    getDayOfMonthList() {
+        return JSON.parse(this.days_of_month).sort(function (a, b) {
+            return a - b;
+        });
+    }
+
+    getStartDateTime() {
+        let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
+        log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
+
+        // Start Time
+        let startTimeSecond = dayjs.utc(this.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
+        log.debug("timeslot", "startTime: " + startTimeSecond);
+
+        // Bake StartDate + StartTime = Start DateTime
+        return dayjs.utc(this.start_date).add(startTimeSecond, "second");
+    }
+
+    getDuration() {
+        let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
+        // Add 24hours if it is across day
+        if (duration < 0) {
+            duration += 24 * 3600;
+        }
+        return duration;
+    }
+
     static jsonToBean(bean, obj) {
         if (obj.id) {
             bean.id = obj.id;
diff --git a/server/model/maintenance_timeslot.js b/server/model/maintenance_timeslot.js
index 4c13632d..b20b9473 100644
--- a/server/model/maintenance_timeslot.js
+++ b/server/model/maintenance_timeslot.js
@@ -40,6 +40,7 @@ class MaintenanceTimeslot extends BeanModel {
 
         if (maintenance.strategy === "manual") {
             log.debug("maintenance", "No need to generate timeslot for manual type");
+
         } else if (maintenance.strategy === "single") {
             let bean = R.dispense("maintenance_timeslot");
             bean.maintenance_id = maintenance.id;
@@ -47,74 +48,131 @@ class MaintenanceTimeslot extends BeanModel {
             bean.end_date = maintenance.end_date;
             bean.generated_next = true;
             return await R.store(bean);
-        } else if (maintenance.strategy === "recurring-interval") {
-            let bean = R.dispense("maintenance_timeslot");
 
+        } else if (maintenance.strategy === "recurring-interval") {
             // Prevent dead loop, in case interval_day is not set
             if (!maintenance.interval_day || maintenance.interval_day <= 0) {
                 maintenance.interval_day = 1;
             }
 
-            let startOfTheDay = dayjs.utc(maintenance.start_date).format("HH:mm");
-            log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
+            return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
+                return startDateTime.add(maintenance.interval_day, "day");
+            });
 
-            // Start Time
-            let startTimeSecond = dayjs.utc(maintenance.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
-            log.debug("timeslot", "startTime: " + startTimeSecond);
-
-            // Duration
-            let duration = dayjs.utc(maintenance.end_time, "HH:mm").diff(dayjs.utc(maintenance.start_time, "HH:mm"), "second");
-            // Add 24hours if it is across day
-            if (duration < 0) {
-                duration += 24 * 3600;
-            }
-
-            // Bake StartDate + StartTime = Start DateTime
-            let startDateTime = dayjs.utc(maintenance.start_date).add(startTimeSecond, "second");
-            let endDateTime;
-
-            // Keep generating from the first possible date, until it is ok
-            while (true) {
-                log.debug("timeslot", "startDateTime: " + startDateTime.format());
-
-                // Handling out of effective date range
-                if (startDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
-                    log.debug("timeslot", "Out of effective date range");
-                    return null;
-                }
-
-                endDateTime = startDateTime.add(duration, "second");
-
-                // If endDateTime is out of effective date range, use the end datetime from effective date range
-                if (endDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
-                    endDateTime = dayjs.utc(maintenance.end_date);
-                }
-
-                // If minDate is set, the endDateTime must be bigger than it.
-                // And the endDateTime must be bigger current time
-                if (
-                    (!minDate || endDateTime.diff(minDate) > 0) &&
-                    endDateTime.diff(dayjs()) > 0
-                ) {
-                    break;
-                }
-
-                startDateTime = startDateTime.add(maintenance.interval_day, "day");
-            }
-
-            bean.maintenance_id = maintenance.id;
-            bean.start_date = localToUTC(startDateTime);
-            bean.end_date = localToUTC(endDateTime);
-            bean.generated_next = false;
-            return await R.store(bean);
         } else if (maintenance.strategy === "recurring-weekday") {
-            // TODO
+            let dayOfWeekList = maintenance.getDayOfWeekList();
+
+            if (dayOfWeekList.length <= 0) {
+                log.debug("timeslot", "No weekdays selected?");
+                return null;
+            }
+
+            return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
+                while (true) {
+                    startDateTime = startDateTime.add(1, "day");
+
+                    log.debug("timeslot", "nextDateTime: " + startDateTime);
+
+                    let day = startDateTime.local().day();
+                    log.debug("timeslot", "nextDateTime.day(): " + day);
+
+                    if (dayOfWeekList.includes(day)) {
+                        return startDateTime;
+                    }
+                }
+            });
+
         } else if (maintenance.strategy === "recurring-day-of-month") {
-            // TODO
+            let dayOfMonthList = maintenance.getDayOfMonthList();
+            if (dayOfMonthList.length <= 0) {
+                log.debug("timeslot", "No day selected?");
+                return null;
+            }
+
+            return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
+                while (true) {
+
+                    startDateTime = startDateTime.add(1, "day");
+
+                    let day = parseInt(startDateTime.local().format("D"));
+
+                    log.debug("timeslot", "day: " + day);
+
+                    // Check 1-31
+                    if (dayOfMonthList.includes(day)) {
+                        return startDateTime;
+                    }
+
+                    // Check "lastDay1","lastDay2"...
+                    let daysInMonth = startDateTime.daysInMonth();
+                    let lastDayList = [];
+
+                    // Small first, e.g. 28 > 29 > 30 > 31
+                    for (let i = 4; i >= 1; i--) {
+                        if (dayOfMonthList.includes("lastDay" + i)) {
+                            lastDayList.push(daysInMonth - i + 1);
+                        }
+                    }
+                    log.debug("timeslot", "lastDayList: " + lastDayList);
+                    if (lastDayList.includes(day)) {
+                        return startDateTime;
+                    }
+                }
+            });
         } else {
             throw new Error("Unknown maintenance strategy");
         }
     }
+
+    /**
+     * Generate a next timeslot for all recurring types
+     * @param maintenance
+     * @param minDate
+     * @param nextDayCallback The logic how to get the next possible day
+     * @returns {Promise<null|MaintenanceTimeslot>}
+     */
+    static async handleRecurringType(maintenance, minDate, nextDayCallback) {
+        let bean = R.dispense("maintenance_timeslot");
+
+        let duration = maintenance.getDuration();
+        let startDateTime = maintenance.getStartDateTime();
+        let endDateTime;
+
+        // Keep generating from the first possible date, until it is ok
+        while (true) {
+            log.debug("timeslot", "startDateTime: " + startDateTime.format());
+
+            // Handling out of effective date range
+            if (startDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
+                log.debug("timeslot", "Out of effective date range");
+                return null;
+            }
+
+            endDateTime = startDateTime.add(duration, "second");
+
+            // If endDateTime is out of effective date range, use the end datetime from effective date range
+            if (endDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
+                endDateTime = dayjs.utc(maintenance.end_date);
+            }
+
+            // If minDate is set, the endDateTime must be bigger than it.
+            // And the endDateTime must be bigger current time
+            if (
+                (!minDate || endDateTime.diff(minDate) > 0) &&
+                endDateTime.diff(dayjs()) > 0
+            ) {
+                break;
+            }
+
+            startDateTime = nextDayCallback(startDateTime);
+        }
+
+        bean.maintenance_id = maintenance.id;
+        bean.start_date = localToUTC(startDateTime);
+        bean.end_date = localToUTC(endDateTime);
+        bean.generated_next = false;
+        return await R.store(bean);
+    }
 }
 
 module.exports = MaintenanceTimeslot;
diff --git a/src/languages/en.js b/src/languages/en.js
index 7db8147f..187d4810 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -620,6 +620,7 @@ export default {
     weekdayShortFri: "Fri",
     weekdayShortSat: "Sat",
     weekdayShortSun: "Sun",
+    dayOfWeek: "Day of Week",
     dayOfMonth: "Day of Month",
     lastDay: "Last Day",
     lastDay1: "Last Day of Month",
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 8cf28dfb..8b2be35a 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -91,8 +91,8 @@
                                     <option value="manual">{{ $t("strategyManual") }}</option>
                                     <option value="single">Single Maintenance Window</option>
                                     <option value="recurring-interval">{{ $t("Recurring") }} - {{ $t("recurringInterval") }}</option>
-                                    <option value="recurring-weekday">{{ $t("Recurring") }} - Weekday</option>
-                                    <option value="recurring-day-of-month">{{ $t("Recurring") }} - Day of Month</option>
+                                    <option value="recurring-weekday">{{ $t("Recurring") }} - {{ $t("dayOfWeek") }}</option>
+                                    <option value="recurring-day-of-month">{{ $t("Recurring") }} - {{ $t("dayOfMonth") }}</option>
                                     <option v-if="false" value="recurring-day-of-year">{{ $t("Recurring") }} - Day of Year</option>
                                 </select>
                             </div>
@@ -136,7 +136,7 @@
                             <template v-if="maintenance.strategy === 'recurring-weekday'">
                                 <div class="my-3">
                                     <label for="interval-day" class="form-label">
-                                        {{ $t("Weekday") }}
+                                        {{ $t("dayOfWeek") }}
                                     </label>
 
                                     <!-- Weekday Picker -->
@@ -201,6 +201,7 @@
                                         :minDate="minDate"
                                         format="yyyy-MM-dd HH:mm:ss"
                                         modelType="yyyy-MM-dd HH:mm:ss"
+                                        required
                                     />
                                 </div>
                             </template>
@@ -297,9 +298,9 @@ export default {
                     value: 6,
                 },
                 {
-                    id: "weekday7",
+                    id: "weekday0",
                     langKey: "weekdayShortSun",
-                    value: 7,
+                    value: 0,
                 },
             ],
         };

From 24cb212a37cd50b50f9bc1463e7b48bb484475b7 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 15 Oct 2022 20:15:50 +0800
Subject: [PATCH 127/134] Fix recurring

---
 server/model/maintenance_timeslot.js | 81 ++++++++++++++++------------
 server/model/monitor.js              |  2 +
 server/uptime-kuma-server.js         |  9 +++-
 3 files changed, 55 insertions(+), 37 deletions(-)

diff --git a/server/model/maintenance_timeslot.js b/server/model/maintenance_timeslot.js
index b20b9473..2babe6bc 100644
--- a/server/model/maintenance_timeslot.js
+++ b/server/model/maintenance_timeslot.js
@@ -57,30 +57,37 @@ class MaintenanceTimeslot extends BeanModel {
 
             return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
                 return startDateTime.add(maintenance.interval_day, "day");
+            }, () => {
+                return true;
             });
 
         } else if (maintenance.strategy === "recurring-weekday") {
             let dayOfWeekList = maintenance.getDayOfWeekList();
+            log.debug("timeslot", dayOfWeekList);
 
             if (dayOfWeekList.length <= 0) {
                 log.debug("timeslot", "No weekdays selected?");
                 return null;
             }
 
+            const isValid = (startDateTime) => {
+                log.debug("timeslot", "nextDateTime: " + startDateTime);
+
+                let day = startDateTime.local().day();
+                log.debug("timeslot", "nextDateTime.day(): " + day);
+
+                return dayOfWeekList.includes(day);
+            };
+
             return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
                 while (true) {
                     startDateTime = startDateTime.add(1, "day");
 
-                    log.debug("timeslot", "nextDateTime: " + startDateTime);
-
-                    let day = startDateTime.local().day();
-                    log.debug("timeslot", "nextDateTime.day(): " + day);
-
-                    if (dayOfWeekList.includes(day)) {
+                    if (isValid(startDateTime)) {
                         return startDateTime;
                     }
                 }
-            });
+            }, isValid);
 
         } else if (maintenance.strategy === "recurring-day-of-month") {
             let dayOfMonthList = maintenance.getDayOfMonthList();
@@ -89,36 +96,38 @@ class MaintenanceTimeslot extends BeanModel {
                 return null;
             }
 
+            const isValid = (startDateTime) => {
+                let day = parseInt(startDateTime.local().format("D"));
+
+                log.debug("timeslot", "day: " + day);
+
+                // Check 1-31
+                if (dayOfMonthList.includes(day)) {
+                    return startDateTime;
+                }
+
+                // Check "lastDay1","lastDay2"...
+                let daysInMonth = startDateTime.daysInMonth();
+                let lastDayList = [];
+
+                // Small first, e.g. 28 > 29 > 30 > 31
+                for (let i = 4; i >= 1; i--) {
+                    if (dayOfMonthList.includes("lastDay" + i)) {
+                        lastDayList.push(daysInMonth - i + 1);
+                    }
+                }
+                log.debug("timeslot", lastDayList);
+                return lastDayList.includes(day);
+            };
+
             return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
                 while (true) {
-
                     startDateTime = startDateTime.add(1, "day");
-
-                    let day = parseInt(startDateTime.local().format("D"));
-
-                    log.debug("timeslot", "day: " + day);
-
-                    // Check 1-31
-                    if (dayOfMonthList.includes(day)) {
-                        return startDateTime;
-                    }
-
-                    // Check "lastDay1","lastDay2"...
-                    let daysInMonth = startDateTime.daysInMonth();
-                    let lastDayList = [];
-
-                    // Small first, e.g. 28 > 29 > 30 > 31
-                    for (let i = 4; i >= 1; i--) {
-                        if (dayOfMonthList.includes("lastDay" + i)) {
-                            lastDayList.push(daysInMonth - i + 1);
-                        }
-                    }
-                    log.debug("timeslot", "lastDayList: " + lastDayList);
-                    if (lastDayList.includes(day)) {
+                    if (isValid(startDateTime)) {
                         return startDateTime;
                     }
                 }
-            });
+            }, isValid);
         } else {
             throw new Error("Unknown maintenance strategy");
         }
@@ -128,10 +137,11 @@ class MaintenanceTimeslot extends BeanModel {
      * Generate a next timeslot for all recurring types
      * @param maintenance
      * @param minDate
-     * @param nextDayCallback The logic how to get the next possible day
+     * @param {function} nextDayCallback The logic how to get the next possible day
+     * @param {function} isValidCallback Check the day whether is matched the current strategy
      * @returns {Promise<null|MaintenanceTimeslot>}
      */
-    static async handleRecurringType(maintenance, minDate, nextDayCallback) {
+    static async handleRecurringType(maintenance, minDate, nextDayCallback, isValidCallback) {
         let bean = R.dispense("maintenance_timeslot");
 
         let duration = maintenance.getDuration();
@@ -157,13 +167,14 @@ class MaintenanceTimeslot extends BeanModel {
 
             // If minDate is set, the endDateTime must be bigger than it.
             // And the endDateTime must be bigger current time
+            // Is valid under current recurring strategy
             if (
                 (!minDate || endDateTime.diff(minDate) > 0) &&
-                endDateTime.diff(dayjs()) > 0
+                endDateTime.diff(dayjs()) > 0 &&
+                isValidCallback(startDateTime)
             ) {
                 break;
             }
-
             startDateTime = nextDayCallback(startDateTime);
         }
 
diff --git a/server/model/monitor.js b/server/model/monitor.js
index 43e9e358..e8342b09 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -623,6 +623,8 @@ class Monitor extends BeanModel {
                 log.debug("monitor", `[${this.name}] apicache clear`);
                 apicache.clear();
 
+                UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
+
             } else {
                 bean.important = false;
 
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index f84bf312..078cc31d 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -10,6 +10,7 @@ const util = require("util");
 const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
 const { Settings } = require("./settings");
 const dayjs = require("dayjs");
+// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
 
 /**
  * `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
@@ -130,8 +131,12 @@ class UptimeKumaServer {
      * @returns {Object}
      */
     async sendMaintenanceList(socket) {
-        let list = await this.getMaintenanceJSONList(socket.userID);
-        this.io.to(socket.userID).emit("maintenanceList", list);
+        return await this.sendMaintenanceListByUserID(socket.userID);
+    }
+
+    async sendMaintenanceListByUserID(userID) {
+        let list = await this.getMaintenanceJSONList(userID);
+        this.io.to(userID).emit("maintenanceList", list);
         return list;
     }
 

From 6f4424de280d0c6f5740018b34c3cc46253d08c8 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 15 Oct 2022 20:44:02 +0800
Subject: [PATCH 128/134] Remove unused language keys

---
 src/languages/en.js | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/src/languages/en.js b/src/languages/en.js
index 187d4810..f7434d39 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -15,17 +15,12 @@ export default {
     "Affected Monitors": "Affected Monitors",
     "Pick Affected Monitors...": "Pick Affected Monitors...",
     "Start of maintenance": "Start of maintenance",
-    "Expected end of maintenance": "Expected end of maintenance",
     "All Status Pages": "All Status Pages",
-    "Selected status pages": "Selected status pages",
     "Select status pages...": "Select status pages...",
     recurringIntervalMessage: "Run once every day | Run once every {0} days",
     affectedMonitorsDescription: "Select monitors that are affected by current maintenance",
     affectedStatusPages: "Show this maintenance message on selected status pages",
     atLeastOneMonitor: "Select at least one affected monitor",
-    selectedStatusPagesDescription: "Select status pages to display maintenance info on",
-    maintenanceTitleExample: "Network infrastructure maintenance",
-    maintenanceDescriptionExample: "Example: Network infrastructure maintenance is underway which will affect some of our services.",
     passwordNotMatchMsg: "The repeat password does not match.",
     notificationDescription: "Notifications must be assigned to a monitor to function.",
     keywordDescription: "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
@@ -136,7 +131,6 @@ export default {
     "Remember me": "Remember me",
     Login: "Login",
     "No Monitors, please": "No Monitors, please",
-    "No Maintenance, please": "No Maintenance, please",
     "add one": "add one",
     "Notification Type": "Notification Type",
     Email: "Email",

From 64f84eb11834281fa79b20756ccc93f1c112fd91 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 15 Oct 2022 21:01:48 +0800
Subject: [PATCH 129/134] Update Details.vue's button styles

---
 src/pages/Details.vue | 37 ++++++++++++++-----------------------
 1 file changed, 14 insertions(+), 23 deletions(-)

diff --git a/src/pages/Details.vue b/src/pages/Details.vue
index 7cf25892..6d6a8dd9 100644
--- a/src/pages/Details.vue
+++ b/src/pages/Details.vue
@@ -20,18 +20,20 @@
             </p>
 
             <div class="functions">
-                <button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
-                    <font-awesome-icon icon="pause" /> {{ $t("Pause") }}
-                </button>
-                <button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
-                    <font-awesome-icon icon="play" /> {{ $t("Resume") }}
-                </button>
-                <router-link :to=" '/edit/' + monitor.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 class="btn-group" role="group">
+                    <button v-if="monitor.active" class="btn btn-normal" @click="pauseDialog">
+                        <font-awesome-icon icon="pause" /> {{ $t("Pause") }}
+                    </button>
+                    <button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
+                        <font-awesome-icon icon="play" /> {{ $t("Resume") }}
+                    </button>
+                    <router-link :to=" '/edit/' + monitor.id " class="btn btn-normal">
+                        <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>
             </div>
 
             <div class="shadow-box">
@@ -392,11 +394,6 @@ export default {
 @media (max-width: 550px) {
     .functions {
         text-align: center;
-
-        button, a {
-            margin-left: 10px !important;
-            margin-right: 10px !important;
-        }
     }
 
     .ping-chart-wrapper {
@@ -439,12 +436,6 @@ export default {
     }
 }
 
-.functions {
-    button, a {
-        margin-right: 20px;
-    }
-}
-
 .shadow-box {
     padding: 20px;
     margin-top: 25px;

From 06755f249d78c6a236b4ba2d83d1a5fc13b7c268 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sat, 15 Oct 2022 21:02:56 +0800
Subject: [PATCH 130/134] Update to 1.19.0-beta.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 8c774d71..5b9f3736 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "uptime-kuma",
-    "version": "1.18.5",
+    "version": "1.19.0-beta.0",
     "license": "MIT",
     "repository": {
         "type": "git",

From a774b3736942552d191ad4267ca61a31e327aeaa Mon Sep 17 00:00:00 2001
From: Cyril59310 <70776486+cyril59310@users.noreply.github.com>
Date: Sat, 15 Oct 2022 19:42:28 +0200
Subject: [PATCH 131/134] Update FR language + fixed daytime error (#2226)

* Update FR language

* fix a daytime error + add for translation

* Update language file FR + fixed daytime error
---
 src/languages/fr-FR.js        | 53 +++++++++++++++++++++++++++++++++++
 src/pages/EditMaintenance.vue |  4 +--
 2 files changed, 55 insertions(+), 2 deletions(-)

diff --git a/src/languages/fr-FR.js b/src/languages/fr-FR.js
index 22df2728..034a69ba 100644
--- a/src/languages/fr-FR.js
+++ b/src/languages/fr-FR.js
@@ -531,4 +531,57 @@ export default {
     backupRecommend: "Veuillez sauvegarder le volume ou le dossier de données (./data/) directement à la place.",
     Optional: "Optionnel",
     squadcast: "Squadcast",
+    Maintenance: "Maintenance",
+    statusMaintenance: "Maintenance",
+    "Schedule maintenance": "Planifier la maintenance",
+    "Affected Monitors": "Moniteurs concernés",
+    "Pick Affected Monitors...": "Sélectionnez les moniteurs concernés...",
+    "Start of maintenance": "Début de la maintenance",
+    "All Status Pages": "Toutes les pages d'état",
+    "Select status pages...": "Sélectionnez les pages d'état...",
+    recurringIntervalMessage: "Exécuter une fois par jour | Exécuter une fois tous les {0} jours",
+    affectedMonitorsDescription: "Sélectionnez les moniteurs concernés par la maintenance en cours",
+    affectedStatusPages: "Afficher ce message de maintenance sur les pages d'état sélectionnées",
+    atLeastOneMonitor: "Sélectionnez au moins un moniteur concerné",
+    deleteMaintenanceMsg: "Voulez-vous vraiment supprimer cette maintenance ?",
+    pushyAPIKey: "Clé API secrète",
+    pushyToken: "Jeton d'appareil",
+    "You can divide numbers with": "Vous pouvez diviser des nombres avec",
+    or: "ou",
+    recurringInterval: "Intervalle",
+    Recurring: "Récurrent",
+    "Single Maintenance Window": "Fenêtre de maintenance unique",
+    "Maintenance Time Window of a Day": "Fenêtre de temps de maintenance",
+    "Effective Date Range": "Plage de dates d'effet",
+    strategyManual: "activer/desactiver manuellement",
+    warningTimezone: "Il utilise le fuseau horaire du serveur",
+    weekdayShortMon: "Lun",
+    weekdayShortTue: "Mar",
+    weekdayShortWed: "Mer",
+    weekdayShortThu: "Jeu",
+    weekdayShortFri: "Ven",
+    weekdayShortSat: "Sam",
+    weekdayShortSun: "Dim",
+    dayOfWeek: "Jour de la semaine",
+    dayOfMonth: "Jour du mois",
+    lastDay: "Dernier jour",
+    lastDay1: "Dernier jour du mois",
+    lastDay2: "2ème dernier jour du mois",
+    lastDay3: "3ème dernier jour du mois",
+    lastDay4: "4ème dernier jour du mois",
+    "No Maintenance": "Aucune Maintenance",
+    pauseMaintenanceMsg: "Voulez-vous vraiment mettre en pause ?",
+    "maintenanceStatus-under-maintenance": "En maintenance",
+    "maintenanceStatus-inactive": "Inactif",
+    "maintenanceStatus-scheduled": "Programmé",
+    "maintenanceStatus-ended": "Terminé",
+    "maintenanceStatus-unknown": "Inconnue",
+    "Display Timezone": "Afficher le fuseau horaire",
+    "Server Timezone": "Fuseau horaire du serveur",
+    "Date and Time": "Date et heure",
+    "DateTime Range": "Plage de dates et d'heures",
+    Strategy: "Stratégie",
+    statusPageMaintenanceEndDate: "Fin",
+    "Free Mobile User Identifier": "Identifiant d'utilisateur Free Mobile",
+    "Free Mobile API Key": "Clé API Free Mobile",
 };
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index 8b2be35a..d668d1ad 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -89,7 +89,7 @@
                                 <label for="strategy" class="form-label">{{ $t("Strategy") }}</label>
                                 <select id="strategy" v-model="maintenance.strategy" class="form-select">
                                     <option value="manual">{{ $t("strategyManual") }}</option>
-                                    <option value="single">Single Maintenance Window</option>
+                                    <option value="single">{{ $t("Single Maintenance Window") }}</option>
                                     <option value="recurring-interval">{{ $t("Recurring") }} - {{ $t("recurringInterval") }}</option>
                                     <option value="recurring-weekday">{{ $t("Recurring") }} - {{ $t("dayOfWeek") }}</option>
                                     <option value="recurring-day-of-month">{{ $t("Recurring") }} - {{ $t("dayOfMonth") }}</option>
@@ -284,7 +284,7 @@ export default {
                 },
                 {
                     id: "weekday4",
-                    langKey: "weekdayShortTue",
+                    langKey: "weekdayShortThu",
                     value: 4,
                 },
                 {

From 92ea019fd43a2b819d270f4316fb3cb001302c62 Mon Sep 17 00:00:00 2001
From: MrEddX <66828538+MrEddX@users.noreply.github.com>
Date: Sun, 16 Oct 2022 07:21:23 +0300
Subject: [PATCH 132/134] Update bg-BG.js

- Added new  fields
- Translated new fields
---
 src/languages/bg-BG.js | 49 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 49 insertions(+)

diff --git a/src/languages/bg-BG.js b/src/languages/bg-BG.js
index 2002bc46..994d0d7f 100644
--- a/src/languages/bg-BG.js
+++ b/src/languages/bg-BG.js
@@ -582,4 +582,53 @@ export default {
     goAlert: "GoAlert",
     backupOutdatedWarning: "Отпаднало: Тъй като са добавени много функции, тази опция за архивиране не е достатъчно поддържана и не може да генерира или възстанови пълен архив.",
     backupRecommend: "Моля, архивирайте дяла или папката (./data/) директно вместо това.",
+    Maintenance: "Поддръжка",
+    statusMaintenance: "Поддръжка",
+    "Schedule maintenance": "Планиране на поддръжка",
+    "Affected Monitors": "Засегнати монитори",
+    "Pick Affected Monitors...": "Изберете засегнати монитори...",
+    "Start of maintenance": "Стартирай поддръжка",
+    "All Status Pages": "Всички статус страници",
+    "Select status pages...": "Изберете статус страници...",
+    recurringIntervalMessage: "Изпълнявай ежедневно | Изпълнявай всеки {0} дни",
+    affectedMonitorsDescription: "Изберете монитори, засегнати от текущата поддръжка",
+    affectedStatusPages: "Покажи това съобщение за поддръжка на избрани статус страници",
+    atLeastOneMonitor: "Изберете поне един засегнат монитор",
+    deleteMaintenanceMsg: "Сигурни ли сте, че желаете да изтриете тази поддръжка?",
+    Optional: "По желание",
+    squadcast: "Squadcast",
+    SendKey: "SendKey",
+    "SMSManager API Docs": "SMSManager API Документация ",
+    "Gateway Type": "Тип на шлюза",
+    SMSManager: "SMSManager",
+    "You can divide numbers with": "Може да разделяте числата с",
+    or: "или",
+    recurringInterval: "Интервал",
+    Recurring: "Повтаряне",
+    strategyManual: "Активен/Неактивен ръчно",
+    warningTimezone: "Използва се часовата зона на сървъра",
+    weekdayShortMon: "Пон",
+    weekdayShortTue: "Вт",
+    weekdayShortWed: "Ср",
+    weekdayShortThu: "Чет",
+    weekdayShortFri: "Пет",
+    weekdayShortSat: "Съб",
+    weekdayShortSun: "Нед",
+    dayOfWeek: "Ден",
+    dayOfMonth: "Дата",
+    lastDay: "Последен ден",
+    lastDay1: "Последен ден от месеца",
+    lastDay2: "2-ри последен ден на месеца",
+    lastDay3: "3-ти последен ден на месеца",
+    lastDay4: "4-ти последен ден на месеца",
+    "No Maintenance": "Няма поддръжка",
+    pauseMaintenanceMsg: "Сигурни ли сте, че желаете да направите пауза?",
+    "maintenanceStatus-under-maintenance": "В режим подръжка",
+    "maintenanceStatus-inactive": "Неактивен",
+    "maintenanceStatus-scheduled": "Планиран",
+    "maintenanceStatus-ended": "Прилючена",
+    "maintenanceStatus-unknown": "Неизвестен",
+    "Display Timezone": "Покажи часова зона",
+    "Server Timezone": "Часова зона на сървъра",
+    statusPageMaintenanceEndDate: "Край",
 };

From 5761bc9b90cb3139f3268bcf5930abb3196d61d2 Mon Sep 17 00:00:00 2001
From: falentio <riv.kevinfalent.io@gmail.com>
Date: Sun, 16 Oct 2022 13:49:25 +0700
Subject: [PATCH 133/134] fix typos in id lang

---
 src/languages/id-ID.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/languages/id-ID.js b/src/languages/id-ID.js
index fe5d3594..da8a0fe6 100644
--- a/src/languages/id-ID.js
+++ b/src/languages/id-ID.js
@@ -568,7 +568,7 @@ export default {
     "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Token Akses Berumur Panjang dapat dibuat dengan mengklik nama profil Anda (kiri bawah) dan menggulir ke bawah lalu klik Buat Token. ",
     "Notification Service": "Layanan Pemberitahuan",
     "default: notify all devices": "bawaan: notifikasi seluruh perangkat",
-    "A listof Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Daftar Layanan Pemberitahuan dapat ditemukan di Home Assistant pada \"Developer Tools > Services\" cari \"notification\" lalu cari nama perangkat Anda.",
+    "A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Daftar Layanan Pemberitahuan dapat ditemukan di Home Assistant pada \"Developer Tools > Services\" cari \"notification\" lalu cari nama perangkat Anda.",
     "Automations can optionally be triggered in Home Assistant:": "Otomatisasi dapat dipicu secara opsional di Home Assistant:",
     "Trigger type:": "Tipe Trigger/Pemicu:",
     "Event type:": "Tipe event:",

From c0e67b6de9bfd3110ad8549fe1dc9a38250a9a87 Mon Sep 17 00:00:00 2001
From: Dave <dave.turmawan@outlook.com>
Date: Mon, 17 Oct 2022 20:09:25 +0700
Subject: [PATCH 134/134] Update id-ID.js

---
 src/languages/id-ID.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/languages/id-ID.js b/src/languages/id-ID.js
index da8a0fe6..3a716392 100644
--- a/src/languages/id-ID.js
+++ b/src/languages/id-ID.js
@@ -27,8 +27,8 @@ export default {
     confirmImportMsg: "Apakah Anda yakin untuk mengimpor cadangan? Pastikan Anda telah memilih opsi impor yang tepat.",
     twoFAVerifyLabel: "Silakan ketik token Anda untuk memverifikasi bahwa 2FA berfungsi",
     tokenValidSettingsMsg: "Token benar! Anda sekarang dapat menyimpan pengaturan 2FA.",
-    confirmEnableTwoFAMsg: "Apakah anda yakin ingin mengaktifkan 2FA?",
-    confirmDisableTwoFAMsg: "Apakah anda yakin ingin menonaktifkan 2FA?",
+    confirmEnableTwoFAMsg: "Apakah Anda yakin ingin mengaktifkan 2FA?",
+    confirmDisableTwoFAMsg: "Apakah Anda yakin ingin menonaktifkan 2FA?",
     Settings: "Pengaturan",
     Dashboard: "Dasbor",
     "New Update": "Pembaruan Baru",
@@ -126,7 +126,7 @@ export default {
     "Resolver Server": "Resolver Server",
     "Resource Record Type": "Resource Record Type",
     "Last Result": "Hasil Terakhir",
-    "Create your admin account": "Buat akun admin anda",
+    "Create your admin account": "Buat akun admin Anda",
     "Repeat Password": "Ulangi Sandi",
     "Import Backup": "Impor Cadangan",
     "Export Backup": "Ekspor Cadangan",