mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-11-04 13:46:13 +08:00 
			
		
		
		
	A complete maintenance planning system (#1213)
A complete maintenance planning system from karelkryda/master
This commit is contained in:
		
							
								
								
									
										83
									
								
								db/patch-maintenance-table2.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								db/patch-maintenance-table2.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
			
		||||
-- 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 [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 (
 | 
			
		||||
    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 [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,
 | 
			
		||||
    [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
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX [generated_next_index] ON [maintenance_timeslot] ([generated_next]);
 | 
			
		||||
 | 
			
		||||
-- 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
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX [maintenance_id_index2] ON [monitor_maintenance]([maintenance_id]);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX [monitor_id_index] ON [monitor_maintenance]([monitor_id]);
 | 
			
		||||
 | 
			
		||||
COMMIT;
 | 
			
		||||
							
								
								
									
										25
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										25
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -69,6 +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",
 | 
			
		||||
                "aedes": "^0.46.3",
 | 
			
		||||
                "babel-plugin-rewire": "~1.2.0",
 | 
			
		||||
                "bootstrap": "5.1.3",
 | 
			
		||||
@@ -3925,6 +3926,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",
 | 
			
		||||
@@ -19613,6 +19629,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",
 | 
			
		||||
 
 | 
			
		||||
@@ -123,6 +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",
 | 
			
		||||
        "aedes": "^0.46.3",
 | 
			
		||||
        "babel-plugin-rewire": "~1.2.0",
 | 
			
		||||
        "bootstrap": "5.1.3",
 | 
			
		||||
 
 | 
			
		||||
@@ -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,9 @@ 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(),
 | 
			
		||||
        serverTimezoneOffset: server.getTimezoneOffset(),
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -64,6 +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-table2.sql": true,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -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");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -10,6 +6,7 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
 | 
			
		||||
 *      0 = DOWN
 | 
			
		||||
 *      1 = UP
 | 
			
		||||
 *      2 = PENDING
 | 
			
		||||
 *      3 = MAINTENANCE
 | 
			
		||||
 */
 | 
			
		||||
class Heartbeat extends BeanModel {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										215
									
								
								server/model/maintenance.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								server/model/maintenance.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,215 @@
 | 
			
		||||
const { BeanModel } = require("redbean-node/dist/bean-model");
 | 
			
		||||
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
 | 
			
		||||
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
 | 
			
		||||
     * @returns {Object}
 | 
			
		||||
     */
 | 
			
		||||
    async toPublicJSON() {
 | 
			
		||||
 | 
			
		||||
        let dateRange = [];
 | 
			
		||||
        if (this.start_date) {
 | 
			
		||||
            dateRange.push(utcToLocal(this.start_date));
 | 
			
		||||
            if (this.end_date) {
 | 
			
		||||
                dateRange.push(utcToLocal(this.end_date));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let timeRange = [];
 | 
			
		||||
        let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
 | 
			
		||||
        timeRange.push(startTime);
 | 
			
		||||
        let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
 | 
			
		||||
        timeRange.push(endTime);
 | 
			
		||||
 | 
			
		||||
        let obj = {
 | 
			
		||||
            id: this.id,
 | 
			
		||||
            title: this.title,
 | 
			
		||||
            description: this.description,
 | 
			
		||||
            strategy: this.strategy,
 | 
			
		||||
            intervalDay: this.interval_day,
 | 
			
		||||
            active: !!this.active,
 | 
			
		||||
            dateRange: dateRange,
 | 
			
		||||
            timeRange: timeRange,
 | 
			
		||||
            weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
 | 
			
		||||
            daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
 | 
			
		||||
            timeslotList: [],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const timeslotList = await this.getTimeslotList();
 | 
			
		||||
 | 
			
		||||
        for (let timeslot of timeslotList) {
 | 
			
		||||
            obj.timeslotList.push(await timeslot.toPublicJSON());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!Array.isArray(obj.weekdays)) {
 | 
			
		||||
            obj.weekdays = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!Array.isArray(obj.daysOfMonth)) {
 | 
			
		||||
            obj.daysOfMonth = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Maintenance Status
 | 
			
		||||
        if (!obj.active) {
 | 
			
		||||
            obj.status = "inactive";
 | 
			
		||||
        } 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.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;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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 R.convertToBeans("maintenance_timeslot", 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
 | 
			
		||||
     * @returns {Object}
 | 
			
		||||
     */
 | 
			
		||||
    async toJSON(timezone = null) {
 | 
			
		||||
        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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Apply timezone offset to timeRange, as it cannot apply automatically.
 | 
			
		||||
        if (obj.timeRange[0]) {
 | 
			
		||||
            timeObjectToUTC(obj.timeRange[0]);
 | 
			
		||||
            if (obj.timeRange[1]) {
 | 
			
		||||
                timeObjectToUTC(obj.timeRange[1]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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 = localToUTC(obj.dateRange[0]);
 | 
			
		||||
 | 
			
		||||
            if (obj.dateRange[1]) {
 | 
			
		||||
                bean.end_date = localToUTC(obj.dateRange[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);
 | 
			
		||||
 | 
			
		||||
        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)
 | 
			
		||||
            OR
 | 
			
		||||
            (maintenance.strategy = 'manual' AND active = 1)
 | 
			
		||||
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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;
 | 
			
		||||
							
								
								
									
										189
									
								
								server/model/maintenance_timeslot.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								server/model/maintenance_timeslot.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,189 @@
 | 
			
		||||
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, localToUTC } = require("../../src/util");
 | 
			
		||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
 | 
			
		||||
 | 
			
		||||
class MaintenanceTimeslot extends BeanModel {
 | 
			
		||||
 | 
			
		||||
    async toPublicJSON() {
 | 
			
		||||
        const serverTimezoneOffset = 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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param {Maintenance} maintenance
 | 
			
		||||
     * @param {dayjs} minDate (For recurring type only) Generate a next timeslot from this date.
 | 
			
		||||
     * @param {boolean} removeExist Remove existing timeslot before create
 | 
			
		||||
     * @returns {Promise<MaintenanceTimeslot>}
 | 
			
		||||
     */
 | 
			
		||||
    static async generateTimeslot(maintenance, minDate = null, removeExist = false) {
 | 
			
		||||
        if (removeExist) {
 | 
			
		||||
            await R.exec("DELETE FROM maintenance_timeslot WHERE maintenance_id = ? ", [
 | 
			
		||||
                maintenance.id
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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_date;
 | 
			
		||||
            bean.end_date = maintenance.end_date;
 | 
			
		||||
            bean.generated_next = true;
 | 
			
		||||
            return await R.store(bean);
 | 
			
		||||
 | 
			
		||||
        } 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;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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");
 | 
			
		||||
 | 
			
		||||
                    if (isValid(startDateTime)) {
 | 
			
		||||
                        return startDateTime;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }, isValid);
 | 
			
		||||
 | 
			
		||||
        } else if (maintenance.strategy === "recurring-day-of-month") {
 | 
			
		||||
            let dayOfMonthList = maintenance.getDayOfMonthList();
 | 
			
		||||
            if (dayOfMonthList.length <= 0) {
 | 
			
		||||
                log.debug("timeslot", "No day selected?");
 | 
			
		||||
                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");
 | 
			
		||||
                    if (isValid(startDateTime)) {
 | 
			
		||||
                        return startDateTime;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }, isValid);
 | 
			
		||||
        } else {
 | 
			
		||||
            throw new Error("Unknown maintenance strategy");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Generate a next timeslot for all recurring types
 | 
			
		||||
     * @param maintenance
 | 
			
		||||
     * @param minDate
 | 
			
		||||
     * @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, isValidCallback) {
 | 
			
		||||
        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
 | 
			
		||||
            // Is valid under current recurring strategy
 | 
			
		||||
            if (
 | 
			
		||||
                (!minDate || endDateTime.diff(minDate) > 0) &&
 | 
			
		||||
                endDateTime.diff(dayjs()) > 0 &&
 | 
			
		||||
                isValidCallback(startDateTime)
 | 
			
		||||
            ) {
 | 
			
		||||
                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;
 | 
			
		||||
@@ -1,12 +1,8 @@
 | 
			
		||||
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, flipStatus, TimeLogger } = require("../../src/util");
 | 
			
		||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger } = require("../../src/util");
 | 
			
		||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server");
 | 
			
		||||
const { R } = require("redbean-node");
 | 
			
		||||
const { BeanModel } = require("redbean-node/dist/bean-model");
 | 
			
		||||
@@ -18,12 +14,14 @@ const apicache = require("../modules/apicache");
 | 
			
		||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
 | 
			
		||||
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
 | 
			
		||||
const { DockerHost } = require("../docker");
 | 
			
		||||
const Maintenance = require("./maintenance");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * status:
 | 
			
		||||
 *      0 = DOWN
 | 
			
		||||
 *      1 = UP
 | 
			
		||||
 *      2 = PENDING
 | 
			
		||||
 *      3 = MAINTENANCE
 | 
			
		||||
 */
 | 
			
		||||
class Monitor extends BeanModel {
 | 
			
		||||
 | 
			
		||||
@@ -37,6 +35,7 @@ class Monitor extends BeanModel {
 | 
			
		||||
            id: this.id,
 | 
			
		||||
            name: this.name,
 | 
			
		||||
            sendUrl: this.sendUrl,
 | 
			
		||||
            maintenance: await Monitor.isUnderMaintenance(this.id),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (this.sendUrl) {
 | 
			
		||||
@@ -96,6 +95,7 @@ class Monitor extends BeanModel {
 | 
			
		||||
            proxyId: this.proxy_id,
 | 
			
		||||
            notificationIDList,
 | 
			
		||||
            tags: tags,
 | 
			
		||||
            maintenance: await Monitor.isUnderMaintenance(this.id),
 | 
			
		||||
            mqttUsername: this.mqttUsername,
 | 
			
		||||
            mqttPassword: this.mqttPassword,
 | 
			
		||||
            mqttTopic: this.mqttTopic,
 | 
			
		||||
@@ -230,7 +230,10 @@ class Monitor extends BeanModel {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                if (this.type === "http" || this.type === "keyword") {
 | 
			
		||||
                if (await Monitor.isUnderMaintenance(this.id)) {
 | 
			
		||||
                    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();
 | 
			
		||||
 | 
			
		||||
@@ -606,8 +609,12 @@ class Monitor extends BeanModel {
 | 
			
		||||
            if (isImportant) {
 | 
			
		||||
                bean.important = true;
 | 
			
		||||
 | 
			
		||||
                log.debug("monitor", `[${this.name}] sendNotification`);
 | 
			
		||||
                await Monitor.sendNotification(isFirstBeat, this, bean);
 | 
			
		||||
                if (Monitor.isImportantForNotification(isFirstBeat, previousBeat?.status, bean.status)) {
 | 
			
		||||
                    log.debug("monitor", `[${this.name}] sendNotification`);
 | 
			
		||||
                    await Monitor.sendNotification(isFirstBeat, this, bean);
 | 
			
		||||
                } else {
 | 
			
		||||
                    log.debug("monitor", `[${this.name}] will not sendNotification because it is (or was) under maintenance`);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Reset down count
 | 
			
		||||
                bean.downCount = 0;
 | 
			
		||||
@@ -616,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;
 | 
			
		||||
 | 
			
		||||
@@ -639,6 +648,8 @@ class Monitor extends BeanModel {
 | 
			
		||||
                    beatInterval = this.retryInterval;
 | 
			
		||||
                }
 | 
			
		||||
                log.warn("monitor", `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) {
 | 
			
		||||
                log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
 | 
			
		||||
            } else {
 | 
			
		||||
                log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
 | 
			
		||||
            }
 | 
			
		||||
@@ -849,7 +860,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
 | 
			
		||||
@@ -920,11 +931,49 @@ 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
        // * 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -1061,6 +1110,26 @@ class Monitor extends BeanModel {
 | 
			
		||||
            monitorID
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if monitor is under maintenance
 | 
			
		||||
     * @param {number} monitorID ID of monitor to check
 | 
			
		||||
     * @returns {Promise<boolean>}
 | 
			
		||||
     */
 | 
			
		||||
    static async isUnderMaintenance(monitorID) {
 | 
			
		||||
        let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
 | 
			
		||||
        const maintenance = await R.getRow(`
 | 
			
		||||
            SELECT COUNT(*) AS count
 | 
			
		||||
            FROM monitor_maintenance mm
 | 
			
		||||
            JOIN maintenance
 | 
			
		||||
                ON mm.maintenance_id = maintenance.id
 | 
			
		||||
                AND mm.monitor_id = ?
 | 
			
		||||
            LEFT JOIN maintenance_timeslot
 | 
			
		||||
                ON maintenance_timeslot.maintenance_id = maintenance.id
 | 
			
		||||
            WHERE ${activeCondition}
 | 
			
		||||
            LIMIT 1`, [ monitorID ]);
 | 
			
		||||
        return maintenance.count !== 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = Monitor;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ const { R } = require("redbean-node");
 | 
			
		||||
const cheerio = require("cheerio");
 | 
			
		||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
 | 
			
		||||
const jsesc = require("jsesc");
 | 
			
		||||
const Maintenance = require("./maintenance");
 | 
			
		||||
 | 
			
		||||
class StatusPage extends BeanModel {
 | 
			
		||||
 | 
			
		||||
@@ -90,6 +91,8 @@ class StatusPage extends BeanModel {
 | 
			
		||||
            incident = incident.toPublicJSON();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let maintenanceList = await StatusPage.getMaintenanceList(statusPage.id);
 | 
			
		||||
 | 
			
		||||
        // Public Group List
 | 
			
		||||
        const publicGroupList = [];
 | 
			
		||||
        const showTags = !!statusPage.show_tags;
 | 
			
		||||
@@ -107,7 +110,8 @@ class StatusPage extends BeanModel {
 | 
			
		||||
        return {
 | 
			
		||||
            config: await statusPage.toPublicJSON(),
 | 
			
		||||
            incident,
 | 
			
		||||
            publicGroupList
 | 
			
		||||
            publicGroupList,
 | 
			
		||||
            maintenanceList,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -266,6 +270,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 activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
 | 
			
		||||
            let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
 | 
			
		||||
                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 maintenance.end_date
 | 
			
		||||
            `, [ statusPageId ]));
 | 
			
		||||
 | 
			
		||||
            for (const bean of maintenanceBeanList) {
 | 
			
		||||
                publicMaintenanceList.push(await bean.toPublicJSON());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return publicMaintenanceList;
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = StatusPage;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ const { R } = require("redbean-node");
 | 
			
		||||
const apicache = require("../modules/apicache");
 | 
			
		||||
const Monitor = require("../model/monitor");
 | 
			
		||||
const dayjs = require("dayjs");
 | 
			
		||||
const { UP, DOWN, flipStatus, log } = require("../../src/util");
 | 
			
		||||
const { UP, MAINTENANCE, DOWN, flipStatus, log } = require("../../src/util");
 | 
			
		||||
const StatusPage = require("../model/status_page");
 | 
			
		||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
 | 
			
		||||
const { makeBadge } = require("badge-maker");
 | 
			
		||||
@@ -67,6 +67,11 @@ router.get("/api/push/:pushToken", async (request, response) => {
 | 
			
		||||
            duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (await Monitor.isUnderMaintenance(monitor.id)) {
 | 
			
		||||
            msg = "Monitor under maintenance";
 | 
			
		||||
            status = MAINTENANCE;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
 | 
			
		||||
        log.debug("router", "PreviousStatus: " + previousStatus);
 | 
			
		||||
        log.debug("router", "Current Status: " + status);
 | 
			
		||||
@@ -87,7 +92,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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,12 @@
 | 
			
		||||
 */
 | 
			
		||||
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"));
 | 
			
		||||
dayjs.extend(require("dayjs/plugin/customParseFormat"));
 | 
			
		||||
 | 
			
		||||
// Check Node.js Version
 | 
			
		||||
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
 | 
			
		||||
const requiredVersion = 14;
 | 
			
		||||
@@ -33,6 +39,7 @@ log.info("server", "Importing Node libraries");
 | 
			
		||||
const fs = require("fs");
 | 
			
		||||
 | 
			
		||||
log.info("server", "Importing 3rd-party libraries");
 | 
			
		||||
 | 
			
		||||
log.debug("server", "Importing express");
 | 
			
		||||
const express = require("express");
 | 
			
		||||
const expressStaticGzip = require("express-static-gzip");
 | 
			
		||||
@@ -127,6 +134,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 { maintenanceSocketHandler } = require("./socket-handlers/maintenance-socket-handler");
 | 
			
		||||
const { Settings } = require("./settings");
 | 
			
		||||
 | 
			
		||||
app.use(express.json());
 | 
			
		||||
@@ -155,6 +163,7 @@ let needSetup = false;
 | 
			
		||||
(async () => {
 | 
			
		||||
    Database.init(args);
 | 
			
		||||
    await initDatabase(testMode);
 | 
			
		||||
    await server.initAfterDatabaseReady();
 | 
			
		||||
 | 
			
		||||
    server.entryPage = await Settings.get("entryPage");
 | 
			
		||||
    await StatusPage.loadDomainMappingList();
 | 
			
		||||
@@ -1057,10 +1066,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) {
 | 
			
		||||
@@ -1088,12 +1102,18 @@ let needSetup = false;
 | 
			
		||||
                await setSettings("general", data);
 | 
			
		||||
                server.entryPage = data.entryPage;
 | 
			
		||||
 | 
			
		||||
                // Also need to apply timezone globally
 | 
			
		||||
                if (data.serverTimezone) {
 | 
			
		||||
                    await server.setTimezone(data.serverTimezone);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                callback({
 | 
			
		||||
                    ok: true,
 | 
			
		||||
                    msg: "Saved"
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                sendInfo(socket);
 | 
			
		||||
                server.sendMaintenanceList(socket);
 | 
			
		||||
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                callback({
 | 
			
		||||
@@ -1452,6 +1472,7 @@ let needSetup = false;
 | 
			
		||||
        databaseSocketHandler(socket);
 | 
			
		||||
        proxySocketHandler(socket);
 | 
			
		||||
        dockerSocketHandler(socket);
 | 
			
		||||
        maintenanceSocketHandler(socket);
 | 
			
		||||
 | 
			
		||||
        log.debug("server", "added all socket handlers");
 | 
			
		||||
 | 
			
		||||
@@ -1554,6 +1575,7 @@ async function afterLogin(socket, user) {
 | 
			
		||||
    socket.join(user.id);
 | 
			
		||||
 | 
			
		||||
    let monitorList = await server.sendMonitorList(socket);
 | 
			
		||||
    server.sendMaintenanceList(socket);
 | 
			
		||||
    sendNotificationList(socket);
 | 
			
		||||
    sendProxyList(socket);
 | 
			
		||||
    sendDockerHostList(socket);
 | 
			
		||||
@@ -1699,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];
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										311
									
								
								server/socket-handlers/maintenance-socket-handler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								server/socket-handlers/maintenance-socket-handler.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,311 @@
 | 
			
		||||
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 Maintenance = require("../model/maintenance");
 | 
			
		||||
const server = UptimeKumaServer.getInstance();
 | 
			
		||||
const MaintenanceTimeslot = require("../model/maintenance_timeslot");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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);
 | 
			
		||||
 | 
			
		||||
            log.debug("maintenance", maintenance);
 | 
			
		||||
 | 
			
		||||
            let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
 | 
			
		||||
            bean.user_id = socket.userID;
 | 
			
		||||
            let maintenanceID = await R.store(bean);
 | 
			
		||||
            await MaintenanceTimeslot.generateTimeslot(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.");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Maintenance.jsonToBean(bean, maintenance);
 | 
			
		||||
 | 
			
		||||
            await R.store(bean);
 | 
			
		||||
            await MaintenanceTimeslot.generateTimeslot(bean, null, true);
 | 
			
		||||
 | 
			
		||||
            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("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);
 | 
			
		||||
            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);
 | 
			
		||||
 | 
			
		||||
            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,
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: true,
 | 
			
		||||
                monitors,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.error(e);
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: false,
 | 
			
		||||
                msg: e.message,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    socket.on("getMaintenanceStatusPage", async (maintenanceID, callback) => {
 | 
			
		||||
        try {
 | 
			
		||||
            checkLogin(socket);
 | 
			
		||||
 | 
			
		||||
            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,
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: true,
 | 
			
		||||
                statusPages,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.error(e);
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: false,
 | 
			
		||||
                msg: e.message,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    socket.on("deleteMaintenance", async (maintenanceID, callback) => {
 | 
			
		||||
        try {
 | 
			
		||||
            checkLogin(socket);
 | 
			
		||||
 | 
			
		||||
            log.debug("maintenance", `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,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
@@ -9,6 +9,8 @@ const Database = require("./database");
 | 
			
		||||
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.
 | 
			
		||||
@@ -26,6 +28,13 @@ class UptimeKumaServer {
 | 
			
		||||
     * @type {{}}
 | 
			
		||||
     */
 | 
			
		||||
    monitorList = {};
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Main maintenance list
 | 
			
		||||
     * @type {{}}
 | 
			
		||||
     */
 | 
			
		||||
    maintenanceList = {};
 | 
			
		||||
 | 
			
		||||
    entryPage = "dashboard";
 | 
			
		||||
    app = undefined;
 | 
			
		||||
    httpServer = undefined;
 | 
			
		||||
@@ -37,6 +46,8 @@ class UptimeKumaServer {
 | 
			
		||||
     */
 | 
			
		||||
    indexHTML = "";
 | 
			
		||||
 | 
			
		||||
    generateMaintenanceTimeslotsInterval = undefined;
 | 
			
		||||
 | 
			
		||||
    static getInstance(args) {
 | 
			
		||||
        if (UptimeKumaServer.instance == null) {
 | 
			
		||||
            UptimeKumaServer.instance = new UptimeKumaServer(args);
 | 
			
		||||
@@ -77,6 +88,16 @@ 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());
 | 
			
		||||
 | 
			
		||||
        await this.generateMaintenanceTimeslots();
 | 
			
		||||
        this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async sendMonitorList(socket) {
 | 
			
		||||
        let list = await this.getMonitorJSONList(socket.userID);
 | 
			
		||||
        this.io.to(socket.userID).emit("monitorList", list);
 | 
			
		||||
@@ -104,6 +125,40 @@ class UptimeKumaServer {
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send maintenance list to client
 | 
			
		||||
     * @param {Socket} socket Socket.io instance to send to
 | 
			
		||||
     * @returns {Object}
 | 
			
		||||
     */
 | 
			
		||||
    async sendMaintenanceList(socket) {
 | 
			
		||||
        return await this.sendMaintenanceListByUserID(socket.userID);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async sendMaintenanceListByUserID(userID) {
 | 
			
		||||
        let list = await this.getMaintenanceJSONList(userID);
 | 
			
		||||
        this.io.to(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
 | 
			
		||||
@@ -147,8 +202,49 @@ 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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getTimezoneOffset() {
 | 
			
		||||
        return dayjs().format("Z");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async setTimezone(timezone) {
 | 
			
		||||
        await Settings.set("serverTimezone", timezone, "general");
 | 
			
		||||
        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");
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ const {
 | 
			
		||||
        rfc2865: { file, attributes },
 | 
			
		||||
    },
 | 
			
		||||
} = require("node-radius-utils");
 | 
			
		||||
const dayjs = require("dayjs");
 | 
			
		||||
 | 
			
		||||
// From ping-lite
 | 
			
		||||
exports.WIN = /^win/.test(process.platform);
 | 
			
		||||
@@ -658,3 +659,64 @@ module.exports.send403 = (res, msg = "") => {
 | 
			
		||||
        "msg": msg,
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function timeObjectConvertTimezone(obj, timezone, timeObjectToUTC = true) {
 | 
			
		||||
    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));
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        (timeObjectToUTC && offsetString.startsWith("+")) ||
 | 
			
		||||
        (!timeObjectToUTC && offsetString.startsWith("-"))
 | 
			
		||||
    ) {
 | 
			
		||||
        hours *= -1;
 | 
			
		||||
        minutes *= -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    obj.hours += hours;
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return obj;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * @param {object} obj
 | 
			
		||||
 * @param {string} timezone
 | 
			
		||||
 * @returns {object}
 | 
			
		||||
 */
 | 
			
		||||
module.exports.timeObjectToUTC = (obj, timezone = undefined) => {
 | 
			
		||||
    return timeObjectConvertTimezone(obj, timezone, true);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * @param {object} obj
 | 
			
		||||
 * @param {string} timezone
 | 
			
		||||
 * @returns {object}
 | 
			
		||||
 */
 | 
			
		||||
module.exports.timeObjectToLocal = (obj, timezone = undefined) => {
 | 
			
		||||
    return timeObjectConvertTimezone(obj, timezone, false);
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
@@ -107,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;
 | 
			
		||||
 | 
			
		||||
@@ -256,6 +282,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;
 | 
			
		||||
 | 
			
		||||
@@ -323,6 +363,7 @@ optgroup {
 | 
			
		||||
        &.bg-info,
 | 
			
		||||
        &.bg-warning,
 | 
			
		||||
        &.bg-danger,
 | 
			
		||||
        &.bg-maintenance,
 | 
			
		||||
        &.bg-light {
 | 
			
		||||
            color: $dark-font-color2;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
$primary: #5cdd8b;
 | 
			
		||||
$danger: #dc3545;
 | 
			
		||||
$warning: #f8a306;
 | 
			
		||||
$maintenance: #1747f5;
 | 
			
		||||
$link-color: #111;
 | 
			
		||||
$border-radius: 50rem;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										39
									
								
								src/assets/vue-datepicker.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/assets/vue-datepicker.scss
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
}
 | 
			
		||||
@@ -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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -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)"
 | 
			
		||||
            />
 | 
			
		||||
@@ -211,6 +211,10 @@ export default {
 | 
			
		||||
            background-color: $warning;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.maintenance {
 | 
			
		||||
            background-color: $maintenance;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &:not(.empty):hover {
 | 
			
		||||
            transition: all ease-in-out 0.15s;
 | 
			
		||||
            opacity: 0.8;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								src/components/MaintenanceTime.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/components/MaintenanceTime.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <div v-if="maintenance.strategy === 'manual'" class="timeslot">
 | 
			
		||||
            {{ $t("Manual") }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-else-if="maintenance.timeslotList.length > 0" class="timeslot">
 | 
			
		||||
            {{ 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>
 | 
			
		||||
@@ -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, log } from "../util.ts";
 | 
			
		||||
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);
 | 
			
		||||
@@ -163,7 +159,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]) ||
 | 
			
		||||
@@ -185,8 +182,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 {
 | 
			
		||||
@@ -205,7 +203,7 @@ export default {
 | 
			
		||||
                        type: "bar",
 | 
			
		||||
                        data: downData,
 | 
			
		||||
                        borderColor: "#00000000",
 | 
			
		||||
                        backgroundColor: "#DC354568",
 | 
			
		||||
                        backgroundColor: colorData,
 | 
			
		||||
                        yAxisID: "y1",
 | 
			
		||||
                        barThickness: "flex",
 | 
			
		||||
                        barPercentage: 1,
 | 
			
		||||
 
 | 
			
		||||
@@ -225,4 +225,8 @@ export default {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bg-maintenance {
 | 
			
		||||
    background-color: $maintenance;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,10 @@ export default {
 | 
			
		||||
                return "warning";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.status === 3) {
 | 
			
		||||
                return "maintenance";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return "secondary";
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -42,6 +46,10 @@ export default {
 | 
			
		||||
                return this.$t("Pending");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.status === 3) {
 | 
			
		||||
                return this.$t("statusMaintenance");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return this.$t("Unknown");
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,10 @@ export default {
 | 
			
		||||
    computed: {
 | 
			
		||||
        uptime() {
 | 
			
		||||
 | 
			
		||||
            if (this.type === "maintenance") {
 | 
			
		||||
                return this.$t("statusMaintenance");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let key = this.monitor.id + "_" + this.type;
 | 
			
		||||
 | 
			
		||||
            if (this.$root.uptimeList[key] !== undefined) {
 | 
			
		||||
@@ -35,6 +39,10 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        color() {
 | 
			
		||||
            if (this.type === "maintenance" || this.monitor.maintenance) {
 | 
			
		||||
                return "maintenance";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.lastHeartBeat.status === 0) {
 | 
			
		||||
                return "danger";
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <form class="my-4" autocomplete="off" @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,23 @@
 | 
			
		||||
                </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 value="UTC">UTC</option>
 | 
			
		||||
                    <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">
 | 
			
		||||
@@ -146,11 +163,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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,9 @@ import {
 | 
			
		||||
    faUndo,
 | 
			
		||||
    faPlusCircle,
 | 
			
		||||
    faAngleDown,
 | 
			
		||||
    faWrench,
 | 
			
		||||
    faHeartbeat,
 | 
			
		||||
    faFilter,
 | 
			
		||||
} from "@fortawesome/free-solid-svg-icons";
 | 
			
		||||
 | 
			
		||||
library.add(
 | 
			
		||||
@@ -82,6 +85,9 @@ library.add(
 | 
			
		||||
    faPlusCircle,
 | 
			
		||||
    faAngleDown,
 | 
			
		||||
    faLink,
 | 
			
		||||
    faWrench,
 | 
			
		||||
    faHeartbeat,
 | 
			
		||||
    faFilter,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export { FontAwesomeIcon };
 | 
			
		||||
 
 | 
			
		||||
@@ -9,11 +9,24 @@ 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",
 | 
			
		||||
    statusMaintenance: "Maintenance",
 | 
			
		||||
    "Schedule maintenance": "Schedule maintenance",
 | 
			
		||||
    "Affected Monitors": "Affected Monitors",
 | 
			
		||||
    "Pick Affected Monitors...": "Pick Affected Monitors...",
 | 
			
		||||
    "Start of maintenance": "Start of maintenance",
 | 
			
		||||
    "All Status Pages": "All 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",
 | 
			
		||||
    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?",
 | 
			
		||||
    dnsPortDescription: "DNS server port. Defaults to 53. You can change the port at any time.",
 | 
			
		||||
    resolverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.",
 | 
			
		||||
@@ -590,4 +603,32 @@ export default {
 | 
			
		||||
    SMSManager: "SMSManager",
 | 
			
		||||
    "You can divide numbers with": "You can divide numbers with",
 | 
			
		||||
    "or": "or",
 | 
			
		||||
    recurringInterval: "Interval",
 | 
			
		||||
    "Recurring": "Recurring",
 | 
			
		||||
    strategyManual: "Active/Inactive Manually",
 | 
			
		||||
    warningTimezone: "It is using the server's timezone",
 | 
			
		||||
    weekdayShortMon: "Mon",
 | 
			
		||||
    weekdayShortTue: "Tue",
 | 
			
		||||
    weekdayShortWed: "Wed",
 | 
			
		||||
    weekdayShortThu: "Thu",
 | 
			
		||||
    weekdayShortFri: "Fri",
 | 
			
		||||
    weekdayShortSat: "Sat",
 | 
			
		||||
    weekdayShortSun: "Sun",
 | 
			
		||||
    dayOfWeek: "Day of Week",
 | 
			
		||||
    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",
 | 
			
		||||
    "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",
 | 
			
		||||
    "Display Timezone": "Display Timezone",
 | 
			
		||||
    "Server Timezone": "Server Timezone",
 | 
			
		||||
    statusPageMaintenanceEndDate: "End",
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -380,4 +380,6 @@ export default {
 | 
			
		||||
    proxyDescription: "必須將代理伺服器指派給監測器才能運作。",
 | 
			
		||||
    enableProxyDescription: "此代理伺服器在啟用前不會在監測器上生效,您可以藉由控制啟用狀態來暫時對所有的監測器停用代理伺服器。",
 | 
			
		||||
    setAsDefaultProxyDescription: "預設情況下,新監測器將啟用此代理伺服器。您仍可分別停用各監測器的代理伺服器。",
 | 
			
		||||
    Maintenance: "維護",
 | 
			
		||||
    statusMaintenance: "維護中",
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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="/settings" class="dropdown-item" :class="{ active: $route.path.includes('settings') }">
 | 
			
		||||
                                <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/general" 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" />
 | 
			
		||||
@@ -304,5 +317,4 @@ main {
 | 
			
		||||
        background-color: $dark-bg;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -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";
 | 
			
		||||
@@ -15,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: [
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -18,6 +12,19 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        toUTC(value) {
 | 
			
		||||
            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
 | 
			
		||||
@@ -27,6 +34,17 @@ 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.getUTCFullYear() && inputDate.getMonth() === now.getUTCMonth() && inputDate.getDay() === now.getUTCDay()) {
 | 
			
		||||
                return this.datetimeFormat(value, "HH:mm");
 | 
			
		||||
            } else {
 | 
			
		||||
                return this.datetimeFormat(value, "YYYY-MM-DD HH:mm");
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Return a given value in the format YYYY-MM-DD
 | 
			
		||||
         * @param {any} value  Value to format as date
 | 
			
		||||
@@ -64,7 +82,7 @@ export default {
 | 
			
		||||
                return dayjs.utc(value).tz(this.timezone).format(format);
 | 
			
		||||
            }
 | 
			
		||||
            return "";
 | 
			
		||||
        }
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    computed: {
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,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: { },
 | 
			
		||||
@@ -129,6 +130,10 @@ export default {
 | 
			
		||||
                this.monitorList = data;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            socket.on("maintenanceList", (data) => {
 | 
			
		||||
                this.maintenanceList = data;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            socket.on("notificationList", (data) => {
 | 
			
		||||
                this.notificationList = data;
 | 
			
		||||
            });
 | 
			
		||||
@@ -445,6 +450,13 @@ export default {
 | 
			
		||||
            socket.emit("getMonitorList", callback);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        getMaintenanceList(callback) {
 | 
			
		||||
            if (! callback) {
 | 
			
		||||
                callback = () => { };
 | 
			
		||||
            }
 | 
			
		||||
            socket.emit("getMaintenanceList", callback);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Add a monitor
 | 
			
		||||
         * @param {Object} monitor Object representing monitor to add
 | 
			
		||||
@@ -454,6 +466,26 @@ export default {
 | 
			
		||||
            socket.emit("add", monitor, callback);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        addMaintenance(maintenance, callback) {
 | 
			
		||||
            socket.emit("addMaintenance", maintenance, callback);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        addMonitorMaintenance(maintenanceID, monitors, callback) {
 | 
			
		||||
            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);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Delete monitor by ID
 | 
			
		||||
         * @param {number} monitorID ID of monitor to delete
 | 
			
		||||
@@ -463,6 +495,10 @@ export default {
 | 
			
		||||
            socket.emit("deleteMonitor", monitorID, callback);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        deleteMaintenance(maintenanceID, callback) {
 | 
			
		||||
            socket.emit("deleteMaintenance", maintenanceID, callback);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        /** Clear the hearbeat list */
 | 
			
		||||
        clearData() {
 | 
			
		||||
            console.log("reset heartbeat list");
 | 
			
		||||
@@ -550,7 +586,12 @@ 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("statusMaintenance"),
 | 
			
		||||
                        color: "maintenance",
 | 
			
		||||
                    };
 | 
			
		||||
                } else if (! lastHeartBeat) {
 | 
			
		||||
                    result[monitorID] = unknown;
 | 
			
		||||
                } else if (lastHeartBeat.status === 1) {
 | 
			
		||||
                    result[monitorID] = {
 | 
			
		||||
@@ -579,6 +620,7 @@ export default {
 | 
			
		||||
            let result = {
 | 
			
		||||
                up: 0,
 | 
			
		||||
                down: 0,
 | 
			
		||||
                maintenance: 0,
 | 
			
		||||
                unknown: 0,
 | 
			
		||||
                pause: 0,
 | 
			
		||||
            };
 | 
			
		||||
@@ -587,7 +629,9 @@ 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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,10 @@ export default {
 | 
			
		||||
                }
 | 
			
		||||
                return this.userTheme;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        isDark() {
 | 
			
		||||
            return this.theme === "dark";
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
                    <router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
 | 
			
		||||
                </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,10 @@
 | 
			
		||||
                        <h3>{{ $t("Down") }}</h3>
 | 
			
		||||
                        <span class="num text-danger">{{ $root.stats.down }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col">
 | 
			
		||||
                        <h3>{{ $t("Maintenance") }}</h3>
 | 
			
		||||
                        <span class="num text-maintenance">{{ $root.stats.maintenance }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col">
 | 
			
		||||
                        <h3>{{ $t("Unknown") }}</h3>
 | 
			
		||||
                        <span class="num text-secondary">{{ $root.stats.unknown }}</span>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										534
									
								
								src/pages/EditMaintenance.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										534
									
								
								src/pages/EditMaintenance.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,534 @@
 | 
			
		||||
<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-xl-10">
 | 
			
		||||
                            <!-- Title -->
 | 
			
		||||
                            <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"
 | 
			
		||||
                                    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"
 | 
			
		||||
                                ></textarea>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <!-- Affected Monitors -->
 | 
			
		||||
                            <h2 class="mt-5">{{ $t("Affected Monitors") }}</h2>
 | 
			
		||||
                            {{ $t("affectedMonitorsDescription") }}
 | 
			
		||||
 | 
			
		||||
                            <div class="my-3">
 | 
			
		||||
                                <VueMultiselect
 | 
			
		||||
                                    id="affected_monitors"
 | 
			
		||||
                                    v-model="affectedMonitors"
 | 
			
		||||
                                    :options="affectedMonitorsOptions"
 | 
			
		||||
                                    track-by="id"
 | 
			
		||||
                                    label="name"
 | 
			
		||||
                                    :multiple="true"
 | 
			
		||||
                                    :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>
 | 
			
		||||
 | 
			
		||||
                            <!-- 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"
 | 
			
		||||
                                        :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("Date and Time") }}</h2>
 | 
			
		||||
 | 
			
		||||
                            <div>⚠️ {{ $t("warningTimezone") }}: <mark>{{ $root.info.serverTimezone }} ({{ $root.info.serverTimezoneOffset }})</mark></div>
 | 
			
		||||
 | 
			
		||||
                            <!-- Strategy -->
 | 
			
		||||
                            <div class="my-3">
 | 
			
		||||
                                <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") }} - {{ $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>
 | 
			
		||||
 | 
			
		||||
                            <!-- 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.dateRange"
 | 
			
		||||
                                        :dark="$root.isDark"
 | 
			
		||||
                                        range
 | 
			
		||||
                                        :monthChangeOnScroll="false"
 | 
			
		||||
                                        :minDate="minDate"
 | 
			
		||||
                                        format="yyyy-MM-dd HH:mm"
 | 
			
		||||
                                        modelType="yyyy-MM-dd HH:mm:ss"
 | 
			
		||||
                                    />
 | 
			
		||||
                                </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("dayOfWeek") }}
 | 
			
		||||
                                    </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
 | 
			
		||||
                                    />
 | 
			
		||||
                                </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 datePicker
 | 
			
		||||
                                        :monthChangeOnScroll="false"
 | 
			
		||||
                                        :minDate="minDate"
 | 
			
		||||
                                        format="yyyy-MM-dd HH:mm:ss"
 | 
			
		||||
                                        modelType="yyyy-MM-dd HH:mm:ss"
 | 
			
		||||
                                        required
 | 
			
		||||
                                    />
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </template>
 | 
			
		||||
 | 
			
		||||
                            <div class="mt-4 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 { 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() {
 | 
			
		||||
        return {
 | 
			
		||||
            processing: false,
 | 
			
		||||
            maintenance: {},
 | 
			
		||||
            affectedMonitors: [],
 | 
			
		||||
            affectedMonitorsOptions: [],
 | 
			
		||||
            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: "weekday0",
 | 
			
		||||
                    langKey: "weekdayShortSun",
 | 
			
		||||
                    value: 0,
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    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");
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        isAdd() {
 | 
			
		||||
            return this.$route.path === "/add-maintenance";
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        isEdit() {
 | 
			
		||||
            return this.$route.path.startsWith("/maintenance/edit");
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    watch: {
 | 
			
		||||
        "$route.fullPath"() {
 | 
			
		||||
            this.init();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        neverEnd(value) {
 | 
			
		||||
            if (value) {
 | 
			
		||||
                this.maintenance.recurringEndDate = "";
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    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 = [];
 | 
			
		||||
            this.selectedStatusPages = [];
 | 
			
		||||
 | 
			
		||||
            if (this.isAdd) {
 | 
			
		||||
                this.maintenance = {
 | 
			
		||||
                    title: "",
 | 
			
		||||
                    description: "",
 | 
			
		||||
                    strategy: "single",
 | 
			
		||||
                    active: 1,
 | 
			
		||||
                    intervalDay: 1,
 | 
			
		||||
                    dateRange: [ 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) => {
 | 
			
		||||
                    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);
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        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);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        async submit() {
 | 
			
		||||
            this.processing = true;
 | 
			
		||||
 | 
			
		||||
            if (this.affectedMonitors.length === 0) {
 | 
			
		||||
                toast.error(this.$t("atLeastOneMonitor"));
 | 
			
		||||
                return this.processing = false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.isAdd) {
 | 
			
		||||
                this.$root.addMaintenance(this.maintenance, async (res) => {
 | 
			
		||||
                    if (res.ok) {
 | 
			
		||||
                        await this.addMonitorMaintenance(res.maintenanceID, async () => {
 | 
			
		||||
                            await this.addMaintenanceStatusPage(res.maintenanceID, () => {
 | 
			
		||||
                                toast.success(res.msg);
 | 
			
		||||
                                this.processing = false;
 | 
			
		||||
                                this.$root.getMaintenanceList();
 | 
			
		||||
                                this.$router.push("/maintenance");
 | 
			
		||||
                            });
 | 
			
		||||
                        });
 | 
			
		||||
                    } 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, async () => {
 | 
			
		||||
                            await this.addMaintenanceStatusPage(res.maintenanceID, () => {
 | 
			
		||||
                                this.processing = false;
 | 
			
		||||
                                this.$root.toastRes(res);
 | 
			
		||||
                                this.init();
 | 
			
		||||
                                this.$router.push("/maintenance");
 | 
			
		||||
                            });
 | 
			
		||||
                        });
 | 
			
		||||
                    } 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();
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        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>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.shadow-box {
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
textarea {
 | 
			
		||||
    min-height: 150px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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>
 | 
			
		||||
							
								
								
									
										161
									
								
								src/pages/MaintenanceDetails.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/pages/MaintenanceDetails.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,161 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <transition name="slide-fade" appear>
 | 
			
		||||
        <div v-if="maintenance">
 | 
			
		||||
            <h1>{{ maintenance.title }}</h1>
 | 
			
		||||
            <p class="url">
 | 
			
		||||
                <span>{{ $t("Start") }}: {{ $root.datetimeMaintenance(maintenance.start_date) }}</span>
 | 
			
		||||
                <br>
 | 
			
		||||
                <span>{{ $t("End") }}: {{ $root.datetimeMaintenance(maintenance.end_date) }}</span>
 | 
			
		||||
            </p>
 | 
			
		||||
 | 
			
		||||
            <div class="functions" style="margin-top: 10px;">
 | 
			
		||||
                <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">
 | 
			
		||||
                    <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" 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" :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("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 }}
 | 
			
		||||
            </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: [],
 | 
			
		||||
            selectedStatusPages: [],
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    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);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            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() {
 | 
			
		||||
            this.$refs.confirmDelete.show();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        deleteMaintenance() {
 | 
			
		||||
            this.$root.deleteMaintenance(this.maintenance.id, (res) => {
 | 
			
		||||
                if (res.ok) {
 | 
			
		||||
                    toast.success(res.msg);
 | 
			
		||||
                    this.$router.push("/maintenance");
 | 
			
		||||
                } 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark .btn-monitor {
 | 
			
		||||
    color: #020b05 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										280
									
								
								src/pages/ManageMaintenance.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								src/pages/ManageMaintenance.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,280 @@
 | 
			
		||||
<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>
 | 
			
		||||
 | 
			
		||||
                <div
 | 
			
		||||
                    v-for="(item, index) in sortedMaintenanceList"
 | 
			
		||||
                    :key="index"
 | 
			
		||||
                    class="item"
 | 
			
		||||
                    :class="item.status"
 | 
			
		||||
                >
 | 
			
		||||
                    <div class="left-part">
 | 
			
		||||
                        <div
 | 
			
		||||
                            class="circle"
 | 
			
		||||
                        ></div>
 | 
			
		||||
                        <div class="info">
 | 
			
		||||
                            <div class="title">{{ item.title }}</div>
 | 
			
		||||
                            <div v-if="false">{{ item.description }}</div>
 | 
			
		||||
                            <div class="status">
 | 
			
		||||
                                {{ $t("maintenanceStatus-" + item.status) }}
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <MaintenanceTime :maintenance="item" />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="buttons">
 | 
			
		||||
                        <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(item.id)">
 | 
			
		||||
                                <font-awesome-icon icon="pause" /> {{ $t("Pause") }}
 | 
			
		||||
                            </button>
 | 
			
		||||
 | 
			
		||||
                            <button v-if="!item.active" class="btn btn-primary" @click="resumeMaintenance(item.id)">
 | 
			
		||||
                                <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>
 | 
			
		||||
 | 
			
		||||
                            <button class="btn btn-danger" @click="deleteDialog(item.id)">
 | 
			
		||||
                                <font-awesome-icon icon="trash" /> {{ $t("Delete") }}
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </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="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>
 | 
			
		||||
        </div>
 | 
			
		||||
    </transition>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
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() {
 | 
			
		||||
        return {
 | 
			
		||||
            selectedMaintenanceID: undefined,
 | 
			
		||||
            statusOrderList: {
 | 
			
		||||
                "under-maintenance": 1000,
 | 
			
		||||
                "scheduled": 900,
 | 
			
		||||
                "inactive": 800,
 | 
			
		||||
                "ended": 700,
 | 
			
		||||
                "unknown": 0,
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        sortedMaintenanceList() {
 | 
			
		||||
            let result = Object.values(this.$root.maintenanceList);
 | 
			
		||||
 | 
			
		||||
            result.sort((m1, m2) => {
 | 
			
		||||
                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;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    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);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Show dialog to confirm pause
 | 
			
		||||
         */
 | 
			
		||||
        pauseDialog(maintenanceID) {
 | 
			
		||||
            this.selectedMaintenanceID = maintenanceID;
 | 
			
		||||
            this.$refs.confirmPause.show();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Pause maintenance
 | 
			
		||||
         */
 | 
			
		||||
        pauseMaintenance() {
 | 
			
		||||
            this.$root.getSocket().emit("pauseMaintenance", this.selectedMaintenanceID, (res) => {
 | 
			
		||||
                this.$root.toastRes(res);
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Resume maintenance
 | 
			
		||||
         */
 | 
			
		||||
        resumeMaintenance(id) {
 | 
			
		||||
            this.$root.getSocket().emit("resumeMaintenance", id, (res) => {
 | 
			
		||||
                this.$root.toastRes(res);
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
</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;
 | 
			
		||||
        justify-content: space-between;
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
        min-height: 90px;
 | 
			
		||||
        margin-bottom: 5px;
 | 
			
		||||
 | 
			
		||||
        &:hover {
 | 
			
		||||
            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.3;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .circle {
 | 
			
		||||
                background-color: $dark-font-color;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.unknown {
 | 
			
		||||
            .circle {
 | 
			
		||||
                background-color: $dark-font-color;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .left-part {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            gap: 12px;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
 | 
			
		||||
            .circle {
 | 
			
		||||
                width: 25px;
 | 
			
		||||
                height: 25px;
 | 
			
		||||
                border-radius: 50rem;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .info {
 | 
			
		||||
                .title {
 | 
			
		||||
                    font-weight: bold;
 | 
			
		||||
                    font-size: 20px;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .status {
 | 
			
		||||
                    font-size: 14px;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .buttons {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            gap: 8px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dark {
 | 
			
		||||
        .item {
 | 
			
		||||
            &:hover {
 | 
			
		||||
                background-color: $dark-bg2;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -218,12 +218,29 @@
 | 
			
		||||
                        {{ $t("Degraded Service") }}
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-else-if="isMaintenance">
 | 
			
		||||
                        <font-awesome-icon icon="wrench" class="status-maintenance" />
 | 
			
		||||
                        {{ $t("maintenanceStatus-under-maintenance") }}
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-else>
 | 
			
		||||
                        <font-awesome-icon icon="question-circle" style="color: #efefef;" />
 | 
			
		||||
                    </div>
 | 
			
		||||
                </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" />
 | 
			
		||||
@@ -295,8 +312,9 @@ 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_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";
 | 
			
		||||
 | 
			
		||||
const toast = useToast();
 | 
			
		||||
 | 
			
		||||
@@ -316,6 +334,7 @@ export default {
 | 
			
		||||
        ImageCropUpload,
 | 
			
		||||
        Confirm,
 | 
			
		||||
        PrismEditor,
 | 
			
		||||
        MaintenanceTime,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // Leave Page for vue route change
 | 
			
		||||
@@ -356,6 +375,7 @@ export default {
 | 
			
		||||
            loadedData: false,
 | 
			
		||||
            baseURL: "",
 | 
			
		||||
            clickedEditButton: false,
 | 
			
		||||
            maintenanceList: [],
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
@@ -409,6 +429,10 @@ export default {
 | 
			
		||||
            return "bg-" + this.incident.style;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        maintenanceClass() {
 | 
			
		||||
            return "bg-maintenance";
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        overallStatus() {
 | 
			
		||||
 | 
			
		||||
            if (Object.keys(this.$root.publicLastHeartbeatList).length === 0) {
 | 
			
		||||
@@ -421,7 +445,9 @@ 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;
 | 
			
		||||
@@ -447,6 +473,10 @@ export default {
 | 
			
		||||
            return this.overallStatus === STATUS_PAGE_ALL_DOWN;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        isMaintenance() {
 | 
			
		||||
            return this.overallStatus === STATUS_PAGE_MAINTENANCE;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    watch: {
 | 
			
		||||
 | 
			
		||||
@@ -551,6 +581,7 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.incident = res.data.incident;
 | 
			
		||||
            this.maintenanceList = res.data.maintenanceList;
 | 
			
		||||
            this.$root.publicGroupList = res.data.publicGroupList;
 | 
			
		||||
        }).catch( function (error) {
 | 
			
		||||
            if (error.response.status === 404) {
 | 
			
		||||
@@ -946,6 +977,24 @@ footer {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.maintenance-bg-info {
 | 
			
		||||
    color: $maintenance;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.maintenance-icon {
 | 
			
		||||
    font-size: 35px;
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark .shadow-box {
 | 
			
		||||
    background-color: #0d1117;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status-maintenance {
 | 
			
		||||
    color: $maintenance;
 | 
			
		||||
    margin-right: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mobile {
 | 
			
		||||
    h1 {
 | 
			
		||||
        font-size: 22px;
 | 
			
		||||
@@ -1007,4 +1056,10 @@ footer {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bg-maintenance {
 | 
			
		||||
    .alert-heading {
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,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";
 | 
			
		||||
@@ -14,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";
 | 
			
		||||
@@ -25,7 +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";
 | 
			
		||||
 | 
			
		||||
const routes = [
 | 
			
		||||
    {
 | 
			
		||||
@@ -126,6 +129,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,
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										80
									
								
								src/util.js
									
									
									
									
									
								
							
							
						
						
									
										80
									
								
								src/util.js
									
									
									
									
									
								
							@@ -7,17 +7,21 @@
 | 
			
		||||
// 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.log = 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;
 | 
			
		||||
const _dayjs = require("dayjs");
 | 
			
		||||
const dayjs = _dayjs;
 | 
			
		||||
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";
 | 
			
		||||
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;
 | 
			
		||||
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) {
 | 
			
		||||
@@ -100,7 +104,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);
 | 
			
		||||
@@ -303,3 +307,71 @@ function getMonitorRelativeURL(id) {
 | 
			
		||||
    return "/dashboard/" + id;
 | 
			
		||||
}
 | 
			
		||||
exports.getMonitorRelativeURL = getMonitorRelativeURL;
 | 
			
		||||
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 parseTimeObject(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.parseTimeObject = parseTimeObject;
 | 
			
		||||
/**
 | 
			
		||||
 * @returns string e.g. 12:00
 | 
			
		||||
 */
 | 
			
		||||
function parseTimeFromTimeObject(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.parseTimeFromTimeObject = parseTimeFromTimeObject;
 | 
			
		||||
function isoToUTCDateTime(input) {
 | 
			
		||||
    return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
 | 
			
		||||
}
 | 
			
		||||
exports.isoToUTCDateTime = isoToUTCDateTime;
 | 
			
		||||
/**
 | 
			
		||||
 * @param input
 | 
			
		||||
 */
 | 
			
		||||
function utcToISODateTime(input) {
 | 
			
		||||
    return dayjs.utc(input).toISOString();
 | 
			
		||||
}
 | 
			
		||||
exports.utcToISODateTime = utcToISODateTime;
 | 
			
		||||
/**
 | 
			
		||||
 * For SQL_DATETIME_FORMAT
 | 
			
		||||
 */
 | 
			
		||||
function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
 | 
			
		||||
    return dayjs.utc(input).local().format(format);
 | 
			
		||||
}
 | 
			
		||||
exports.utcToLocal = utcToLocal;
 | 
			
		||||
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
 | 
			
		||||
    return dayjs(input).utc().format(format);
 | 
			
		||||
}
 | 
			
		||||
exports.localToUTC = localToUTC;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										89
									
								
								src/util.ts
									
									
									
									
									
								
							
							
						
						
									
										89
									
								
								src/util.ts
									
									
									
									
									
								
							@@ -6,18 +6,25 @@
 | 
			
		||||
// 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";
 | 
			
		||||
 | 
			
		||||
export const isDev = process.env.NODE_ENV === "development";
 | 
			
		||||
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 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) {
 | 
			
		||||
@@ -112,7 +119,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") {
 | 
			
		||||
@@ -336,3 +343,79 @@ export function genSecret(length = 64) {
 | 
			
		||||
export function getMonitorRelativeURL(id: string) {
 | 
			
		||||
    return "/dashboard/" + id;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 parseTimeObject(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 parseTimeFromTimeObject(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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export function isoToUTCDateTime(input : string) {
 | 
			
		||||
    return dayjs(input).utc().format(SQL_DATETIME_FORMAT);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param input
 | 
			
		||||
 */
 | 
			
		||||
export function utcToISODateTime(input : string) {
 | 
			
		||||
    return dayjs.utc(input).toISOString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * For SQL_DATETIME_FORMAT
 | 
			
		||||
 */
 | 
			
		||||
export function utcToLocal(input : string, format = SQL_DATETIME_FORMAT) {
 | 
			
		||||
    return dayjs.utc(input).local().format(format);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function localToUTC(input : string, format = SQL_DATETIME_FORMAT) {
 | 
			
		||||
    return dayjs(input).utc().format(format);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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");
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user