mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-10-25 07:39:22 +08:00 
			
		
		
		
	WIP
This commit is contained in:
		
							
								
								
									
										9
									
								
								db/patch-maintenance-cron.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								db/patch-maintenance-cron.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| -- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | ||||
| BEGIN TRANSACTION; | ||||
|  | ||||
| -- 999 characters. https://stackoverflow.com/questions/46134830/maximum-length-for-cron-job | ||||
| ALTER TABLE maintenance ADD cron TEXT; | ||||
| ALTER TABLE maintenance ADD timezone VARCHAR(255); | ||||
| ALTER TABLE maintenance ADD duration INTEGER; | ||||
|  | ||||
| COMMIT; | ||||
							
								
								
									
										9
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -26,6 +26,7 @@ | ||||
|                 "command-exists": "~1.2.9", | ||||
|                 "compare-versions": "~3.6.0", | ||||
|                 "compression": "~1.7.4", | ||||
|                 "croner": "^6.0.3", | ||||
|                 "dayjs": "~1.11.5", | ||||
|                 "dotenv": "~16.0.3", | ||||
|                 "express": "~4.17.3", | ||||
| @@ -7243,6 +7244,14 @@ | ||||
|                 "yup": "0.32.9" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/croner": { | ||||
|             "version": "6.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/croner/-/croner-6.0.3.tgz", | ||||
|             "integrity": "sha512-Go+s9AaI+MeZUDJ6Kp7OYXCbM3svJ0qZ3IpkGoPetZLnP5wpX8MBTEiJOTYDFokP0Ph85GFZEUTBL9fo1e4DtQ==", | ||||
|             "engines": { | ||||
|                 "node": ">=6.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/cross-env": { | ||||
|             "version": "7.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", | ||||
|   | ||||
| @@ -85,6 +85,7 @@ | ||||
|         "command-exists": "~1.2.9", | ||||
|         "compare-versions": "~3.6.0", | ||||
|         "compression": "~1.7.4", | ||||
|         "croner": "^6.0.3", | ||||
|         "dayjs": "~1.11.5", | ||||
|         "dotenv": "~16.0.3", | ||||
|         "express": "~4.17.3", | ||||
|   | ||||
| @@ -74,6 +74,7 @@ class Database { | ||||
|         "patch-add-description-monitor.sql": true, | ||||
|         "patch-api-key-table.sql": true, | ||||
|         "patch-monitor-tls.sql": true, | ||||
|         "patch-maintenance-cron.sql": true, | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -3,9 +3,15 @@ const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } | ||||
| const { timeObjectToUTC, timeObjectToLocal } = require("../util-server"); | ||||
| const { R } = require("redbean-node"); | ||||
| const dayjs = require("dayjs"); | ||||
| const Cron = require("croner"); | ||||
| const { UptimeKumaServer } = require("../uptime-kuma-server"); | ||||
| const apicache = require("../modules/apicache"); | ||||
|  | ||||
| class Maintenance extends BeanModel { | ||||
|  | ||||
|     static statusList = {}; | ||||
|     static jobList = {}; | ||||
|  | ||||
|     /** | ||||
|      * Return an object that ready to parse to JSON for public | ||||
|      * Only show necessary data to public | ||||
| @@ -15,16 +21,16 @@ class Maintenance extends BeanModel { | ||||
|  | ||||
|         let dateRange = []; | ||||
|         if (this.start_date) { | ||||
|             dateRange.push(utcToLocal(this.start_date)); | ||||
|             dateRange.push(this.start_date); | ||||
|             if (this.end_date) { | ||||
|                 dateRange.push(utcToLocal(this.end_date)); | ||||
|                 dateRange.push(this.end_date); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let timeRange = []; | ||||
|         let startTime = timeObjectToLocal(parseTimeObject(this.start_time)); | ||||
|         let startTime = parseTimeObject(this.start_time); | ||||
|         timeRange.push(startTime); | ||||
|         let endTime = timeObjectToLocal(parseTimeObject(this.end_time)); | ||||
|         let endTime = parseTimeObject(this.end_time); | ||||
|         timeRange.push(endTime); | ||||
|  | ||||
|         let obj = { | ||||
| @@ -39,12 +45,18 @@ class Maintenance extends BeanModel { | ||||
|             weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [], | ||||
|             daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [], | ||||
|             timeslotList: [], | ||||
|             cron: this.cron, | ||||
|             duration: this.duration, | ||||
|             timezone: await this.getTimezone(), | ||||
|             timezoneOffset: await this.getTimezoneOffset(), | ||||
|             status: await this.getStatus(), | ||||
|         }; | ||||
|  | ||||
|         const timeslotList = await this.getTimeslotList(); | ||||
|  | ||||
|         for (let timeslot of timeslotList) { | ||||
|             obj.timeslotList.push(await timeslot.toPublicJSON()); | ||||
|         if (this.strategy === "single") { | ||||
|             obj.timeslotList.push({ | ||||
|                 startDate: this.start_date, | ||||
|                 endDate: this.end_date, | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (!Array.isArray(obj.weekdays)) { | ||||
| @@ -55,54 +67,9 @@ class Maintenance extends BeanModel { | ||||
|             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 | ||||
| @@ -135,26 +102,10 @@ class Maintenance extends BeanModel { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the start date and time for maintenance | ||||
|      * @returns {dayjs.Dayjs} Start date and time | ||||
|      */ | ||||
|     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"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the duraction of maintenance in seconds | ||||
|      * Get the duration of maintenance in seconds | ||||
|      * @returns {number} Duration of maintenance | ||||
|      */ | ||||
|     getDuration() { | ||||
|     calcDuration() { | ||||
|         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) { | ||||
| @@ -169,30 +120,24 @@ class Maintenance extends BeanModel { | ||||
|      * @param {Object} obj Data to fill bean with | ||||
|      * @returns {Bean} Filled bean | ||||
|      */ | ||||
|     static jsonToBean(bean, obj) { | ||||
|     static async 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.timezone = obj.timezone; | ||||
|         bean.duration = obj.duration; | ||||
|         bean.active = obj.active; | ||||
|  | ||||
|         if (obj.dateRange[0]) { | ||||
|             bean.start_date = localToUTC(obj.dateRange[0]); | ||||
|             bean.start_date = obj.dateRange[0]; | ||||
|  | ||||
|             if (obj.dateRange[1]) { | ||||
|                 bean.end_date = localToUTC(obj.dateRange[1]); | ||||
|                 bean.end_date = obj.dateRange[1]; | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -202,38 +147,111 @@ class Maintenance extends BeanModel { | ||||
|         bean.weekdays = JSON.stringify(obj.weekdays); | ||||
|         bean.days_of_month = JSON.stringify(obj.daysOfMonth); | ||||
|  | ||||
|         await bean.generateCron(); | ||||
|  | ||||
|         return bean; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * SQL conditions for active maintenance | ||||
|      * @returns {string} | ||||
|      * Run the cron | ||||
|      */ | ||||
|     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) | ||||
|             ) | ||||
|         `; | ||||
|     async run() { | ||||
|         if (Maintenance.jobList[this.id]) { | ||||
|             log.debug("maintenance", "Maintenance is already running, stop it first. id: " + this.id); | ||||
|             this.stop(); | ||||
|         } | ||||
|  | ||||
|         log.debug("maintenance", "Run maintenance id: " + this.id); | ||||
|  | ||||
|         // 1.21.2 migration | ||||
|         if (!this.cron) { | ||||
|             //this.generateCron(); | ||||
|             //this.timezone = "UTC"; | ||||
|             // this.duration = | ||||
|             if (this.cron) { | ||||
|                 //await R.store(this); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (this.strategy === "single") { | ||||
|             Maintenance.jobList[this.id] = new Cron(this.start_date, { timezone: await this.getTimezone() }, () => { | ||||
|                 log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now"); | ||||
|                 UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id); | ||||
|                 apicache.clear(); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     stop() { | ||||
|         if (Maintenance.jobList[this.id]) { | ||||
|             Maintenance.jobList[this.id].stop(); | ||||
|             delete Maintenance.jobList[this.id]; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async isUnderMaintenance() { | ||||
|         return (await this.getStatus()) === "under-maintenance"; | ||||
|     } | ||||
|  | ||||
|     async getTimezone() { | ||||
|         if (!this.timezone) { | ||||
|             return await UptimeKumaServer.getInstance().getTimezone(); | ||||
|         } | ||||
|         return this.timezone; | ||||
|     } | ||||
|  | ||||
|     async getTimezoneOffset() { | ||||
|         return dayjs.tz(dayjs(), await this.getTimezone()).format("Z"); | ||||
|     } | ||||
|  | ||||
|     async getStatus() { | ||||
|         if (!this.active) { | ||||
|             return "inactive"; | ||||
|         } | ||||
|  | ||||
|         if (this.strategy === "manual") { | ||||
|             return "under-maintenance"; | ||||
|         } | ||||
|  | ||||
|         // Check if the maintenance is started | ||||
|         if (this.start_date && dayjs().isBefore(dayjs.tz(this.start_date, await this.getTimezone()))) { | ||||
|             return "scheduled"; | ||||
|         } | ||||
|  | ||||
|         // Check if the maintenance is ended | ||||
|         if (this.end_date && dayjs().isAfter(dayjs.tz(this.end_date, await this.getTimezone()))) { | ||||
|             return "ended"; | ||||
|         } | ||||
|  | ||||
|         if (this.strategy === "single") { | ||||
|             return "under-maintenance"; | ||||
|         } | ||||
|  | ||||
|         if (!Maintenance.statusList[this.id]) { | ||||
|             Maintenance.statusList[this.id] = "unknown"; | ||||
|         } | ||||
|  | ||||
|         return Maintenance.statusList[this.id]; | ||||
|     } | ||||
|  | ||||
|     setStatus(status) { | ||||
|         Maintenance.statusList[this.id] = status; | ||||
|     } | ||||
|  | ||||
|     async generateCron() { | ||||
|         log.info("maintenance", "Generate cron for maintenance id: " + this.id); | ||||
|  | ||||
|         if (this.strategy === "recurring-interval") { | ||||
|             let array = this.start_time.split(":"); | ||||
|             let hour = parseInt(array[0]); | ||||
|             let minute = parseInt(array[1]); | ||||
|             this.cron = minute + " " + hour + " */" + this.interval_day + " * *"; | ||||
|             this.duration = this.calcDuration(); | ||||
|             log.debug("maintenance", "Cron: " + this.cron); | ||||
|             log.debug("maintenance", "Duration: " + this.duration); | ||||
|         } | ||||
|  | ||||
|     /** | ||||
|      * 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)) | ||||
|             ) | ||||
|         `; | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -151,73 +151,6 @@ class MaintenanceTimeslot extends BeanModel { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static async isDuplicateTimeslot(timeslot) { | ||||
|         let bean = await R.findOne("maintenance_timeslot", "maintenance_id = ? AND start_date = ? AND end_date = ?", [ | ||||
|             timeslot.maintenance_id, | ||||
|             timeslot.start_date, | ||||
|             timeslot.end_date | ||||
|         ]); | ||||
|         return bean !== null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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; | ||||
|  | ||||
|         if (!await this.isDuplicateTimeslot(bean)) { | ||||
|             await R.store(bean); | ||||
|             return bean; | ||||
|         } else { | ||||
|             log.debug("maintenance", "Duplicate timeslot, skip"); | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = MaintenanceTimeslot; | ||||
|   | ||||
| @@ -16,7 +16,6 @@ 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"); | ||||
| const { UptimeCacheList } = require("../uptime-cache-list"); | ||||
| const Gamedig = require("gamedig"); | ||||
|  | ||||
| @@ -1303,18 +1302,19 @@ class Monitor extends BeanModel { | ||||
|      * @returns {Promise<boolean>} | ||||
|      */ | ||||
|     static async isUnderMaintenance(monitorID) { | ||||
|         let activeCondition = Maintenance.getActiveMaintenanceSQLCondition(); | ||||
|         const maintenance = await R.getRow(` | ||||
|             SELECT COUNT(*) AS count | ||||
|             FROM monitor_maintenance mm | ||||
|             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; | ||||
|         const maintenanceIDList = await R.getCol(` | ||||
|             SELECT maintenance_id FROM monitor_maintenance | ||||
|             WHERE monitor_id = ? | ||||
|         `, [ monitorID ]); | ||||
|  | ||||
|         for (const maintenanceID of maintenanceIDList) { | ||||
|             const maintenance = await UptimeKumaServer.getInstance().getMaintenance(maintenanceID); | ||||
|             if (maintenance && await maintenance.isUnderMaintenance()) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     /** Make sure monitor interval is between bounds */ | ||||
|   | ||||
| @@ -5,7 +5,6 @@ const apicache = require("../modules/apicache"); | ||||
| const { UptimeKumaServer } = require("../uptime-kuma-server"); | ||||
| const Maintenance = require("../model/maintenance"); | ||||
| const server = UptimeKumaServer.getInstance(); | ||||
| const MaintenanceTimeslot = require("../model/maintenance_timeslot"); | ||||
|  | ||||
| /** | ||||
|  * Handlers for Maintenance | ||||
| @@ -19,10 +18,12 @@ module.exports.maintenanceSocketHandler = (socket) => { | ||||
|  | ||||
|             log.debug("maintenance", maintenance); | ||||
|  | ||||
|             let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance); | ||||
|             let bean = await Maintenance.jsonToBean(R.dispense("maintenance"), maintenance); | ||||
|             bean.user_id = socket.userID; | ||||
|             let maintenanceID = await R.store(bean); | ||||
|             await MaintenanceTimeslot.generateTimeslot(bean); | ||||
|  | ||||
|             server.maintenanceList[maintenanceID] = bean; | ||||
|             bean.run(); | ||||
|  | ||||
|             await server.sendMaintenanceList(socket); | ||||
|  | ||||
| @@ -45,17 +46,15 @@ module.exports.maintenanceSocketHandler = (socket) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
|  | ||||
|             let bean = await R.findOne("maintenance", " id = ? ", [ maintenance.id ]); | ||||
|             let bean = server.getMaintenance(maintenance.id); | ||||
|  | ||||
|             if (bean.user_id !== socket.userID) { | ||||
|                 throw new Error("Permission denied."); | ||||
|             } | ||||
|  | ||||
|             Maintenance.jsonToBean(bean, maintenance); | ||||
|  | ||||
|             await Maintenance.jsonToBean(bean, maintenance); | ||||
|             await R.store(bean); | ||||
|             await MaintenanceTimeslot.generateTimeslot(bean, null, true); | ||||
|  | ||||
|             await bean.run(); | ||||
|             await server.sendMaintenanceList(socket); | ||||
|  | ||||
|             callback({ | ||||
| @@ -236,6 +235,7 @@ module.exports.maintenanceSocketHandler = (socket) => { | ||||
|             log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`); | ||||
|  | ||||
|             if (maintenanceID in server.maintenanceList) { | ||||
|                 server.maintenanceList[maintenanceID].stop(); | ||||
|                 delete server.maintenanceList[maintenanceID]; | ||||
|             } | ||||
|  | ||||
| @@ -267,9 +267,16 @@ module.exports.maintenanceSocketHandler = (socket) => { | ||||
|  | ||||
|             log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`); | ||||
|  | ||||
|             await R.exec("UPDATE maintenance SET active = 0 WHERE id = ? ", [ | ||||
|                 maintenanceID, | ||||
|             ]); | ||||
|             let maintenance = server.getMaintenance(maintenanceID); | ||||
|  | ||||
|             if (!maintenance) { | ||||
|                 throw new Error("Maintenance not found"); | ||||
|             } | ||||
|  | ||||
|             maintenance.active = false; | ||||
|             maintenance.setStatus("inactive"); | ||||
|             await R.store(maintenance); | ||||
|             maintenance.stop(); | ||||
|  | ||||
|             apicache.clear(); | ||||
|  | ||||
| @@ -294,9 +301,15 @@ module.exports.maintenanceSocketHandler = (socket) => { | ||||
|  | ||||
|             log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`); | ||||
|  | ||||
|             await R.exec("UPDATE maintenance SET active = 1 WHERE id = ? ", [ | ||||
|                 maintenanceID, | ||||
|             ]); | ||||
|             let maintenance = server.getMaintenance(maintenanceID); | ||||
|  | ||||
|             if (!maintenance) { | ||||
|                 throw new Error("Maintenance not found"); | ||||
|             } | ||||
|  | ||||
|             maintenance.active = true; | ||||
|             await R.store(maintenance); | ||||
|             await maintenance.run(); | ||||
|  | ||||
|             apicache.clear(); | ||||
|  | ||||
|   | ||||
| @@ -47,8 +47,6 @@ class UptimeKumaServer { | ||||
|      */ | ||||
|     indexHTML = ""; | ||||
|  | ||||
|     generateMaintenanceTimeslotsInterval = undefined; | ||||
|  | ||||
|     /** | ||||
|      * Plugins Manager | ||||
|      * @type {PluginsManager} | ||||
| @@ -112,8 +110,7 @@ class UptimeKumaServer { | ||||
|         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); | ||||
|         await this.loadMaintenanceList(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -175,16 +172,33 @@ class UptimeKumaServer { | ||||
|      */ | ||||
|     async getMaintenanceJSONList(userID) { | ||||
|         let result = {}; | ||||
|         for (let maintenanceID in this.maintenanceList) { | ||||
|             result[maintenanceID] = await this.maintenanceList[maintenanceID].toJSON(); | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Load maintenance list and run | ||||
|      * @param userID | ||||
|      * @returns {Promise<void>} | ||||
|      */ | ||||
|     async loadMaintenanceList(userID) { | ||||
|         let maintenanceList = await R.findAll("maintenance", " ORDER BY end_date DESC, title", [ | ||||
|  | ||||
|         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(); | ||||
|             this.maintenanceList[maintenance.id] = maintenance; | ||||
|             maintenance.run(this); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|         return result; | ||||
|     getMaintenance(maintenanceID) { | ||||
|         if (this.maintenanceList[maintenanceID]) { | ||||
|             return this.maintenanceList[maintenanceID]; | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -240,7 +254,7 @@ class UptimeKumaServer { | ||||
|      * Attempt to get the current server timezone | ||||
|      * If this fails, fall back to environment variables and then make a | ||||
|      * guess. | ||||
|      * @returns {string} | ||||
|      * @returns {Promise<string>} | ||||
|      */ | ||||
|     async getTimezone() { | ||||
|         let timezone = await Settings.get("serverTimezone"); | ||||
| @@ -271,28 +285,9 @@ class UptimeKumaServer { | ||||
|         dayjs.tz.setDefault(timezone); | ||||
|     } | ||||
|  | ||||
|     /** Load the timeslots for maintenance */ | ||||
|     async generateMaintenanceTimeslots() { | ||||
|         log.debug("maintenance", "Routine: Generating Maintenance Timeslots"); | ||||
|  | ||||
|         // Prevent #2776 | ||||
|         // Remove duplicate maintenance_timeslot with same start_date, end_date and maintenance_id | ||||
|         await R.exec("DELETE FROM maintenance_timeslot WHERE id NOT IN (SELECT MIN(id) FROM maintenance_timeslot GROUP BY start_date, end_date, maintenance_id)"); | ||||
|  | ||||
|         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); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** Stop the server */ | ||||
|     async stop() { | ||||
|         clearTimeout(this.generateMaintenanceTimeslotsInterval); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     loadPlugins() { | ||||
| @@ -341,5 +336,4 @@ module.exports = { | ||||
| }; | ||||
|  | ||||
| // Must be at the end | ||||
| const MaintenanceTimeslot = require("./model/maintenance_timeslot"); | ||||
| const { MonitorType } = require("./monitor-types/monitor-type"); | ||||
|   | ||||
| @@ -4,10 +4,10 @@ | ||||
|             {{ $t("Manual") }} | ||||
|         </div> | ||||
|         <div v-else-if="maintenance.timeslotList.length > 0" class="timeslot"> | ||||
|             {{ maintenance.timeslotList[0].startDateServerTimezone }} | ||||
|             {{ maintenance.timeslotList[0].startDate }} | ||||
|             <span class="to">-</span> | ||||
|             {{ maintenance.timeslotList[0].endDateServerTimezone }} | ||||
|             (UTC{{ maintenance.timeslotList[0].serverTimezoneOffset }}) | ||||
|             {{ maintenance.timeslotList[0].endDate }} | ||||
|             (UTC{{ maintenance.timezoneOffset }}) | ||||
|         </div> | ||||
|     </div> | ||||
| </template> | ||||
|   | ||||
| @@ -394,6 +394,10 @@ | ||||
|     "backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.", | ||||
|     "Optional": "Optional", | ||||
|     "or": "or", | ||||
|     "sameAsServerTimezone": "Same as Server Timezone", | ||||
|     "startDateTime": "Start Date/Time", | ||||
|     "endDateTime": "End Date/Time", | ||||
|     "cronExpression": "Cron Expression", | ||||
|     "recurringInterval": "Interval", | ||||
|     "Recurring": "Recurring", | ||||
|     "strategyManual": "Active/Inactive Manually", | ||||
|   | ||||
| @@ -85,14 +85,13 @@ | ||||
|  | ||||
|                             <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">{{ $t("Single Maintenance Window") }}</option> | ||||
|                                     <option value="cron">{{ $t("cronExpression") }}</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> | ||||
| @@ -102,19 +101,6 @@ | ||||
|  | ||||
|                             <!-- 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 --> | ||||
| @@ -180,7 +166,6 @@ | ||||
|                                 </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"> | ||||
| @@ -192,21 +177,57 @@ | ||||
|                                         disableTimeRangeValidation range | ||||
|                                     /> | ||||
|                                 </div> | ||||
|                             </template> | ||||
|  | ||||
|                             <template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month' || maintenance.strategy === 'cron' || maintenance.strategy === 'single'"> | ||||
|                                 <!-- Timezone --> | ||||
|                                 <div class="mb-4"> | ||||
|                                     <label for="timezone" class="form-label"> | ||||
|                                         {{ $t("Timezone") }} | ||||
|                                     </label> | ||||
|                                     <select id="timezone" v-model="maintenance.timezone" class="form-select"> | ||||
|                                         <option :value="null">{{ $t("sameAsServerTimezone") }}</option> | ||||
|                                         <option value="UTC">UTC</option> | ||||
|                                         <option | ||||
|                                             v-for="(timezone, index) in timezoneList" | ||||
|                                             :key="index" | ||||
|                                             :value="timezone.value" | ||||
|                                         > | ||||
|                                             {{ timezone.name }} | ||||
|                                         </option> | ||||
|                                     </select> | ||||
|                                 </div> | ||||
|  | ||||
|                                 <!-- Date Range --> | ||||
|                                 <div class="my-3"> | ||||
|                                     <label class="form-label">{{ $t("Effective Date Range") }}</label> | ||||
|  | ||||
|                                     <div class="row"> | ||||
|                                         <div class="col"> | ||||
|                                             <div class="mb-2">{{ $t("startDateTime") }}</div> | ||||
|                                             <Datepicker | ||||
|                                         v-model="maintenance.dateRange" | ||||
|                                                 v-model="maintenance.dateRange[0]" | ||||
|                                                 :dark="$root.isDark" | ||||
|                                         range datePicker | ||||
|                                                 datePicker | ||||
|                                                 :monthChangeOnScroll="false" | ||||
|                                         :minDate="minDate" | ||||
|                                                 format="yyyy-MM-dd HH:mm:ss" | ||||
|                                                 modelType="yyyy-MM-dd HH:mm:ss" | ||||
|                                         required | ||||
|                                             /> | ||||
|                                         </div> | ||||
|  | ||||
|                                         <div class="col"> | ||||
|                                             <div class="mb-2">{{ $t("endDateTime") }}</div> | ||||
|                                             <Datepicker | ||||
|                                                 v-model="maintenance.dateRange[1]" | ||||
|                                                 :dark="$root.isDark" | ||||
|                                                 datePicker | ||||
|                                                 :monthChangeOnScroll="false" | ||||
|                                                 format="yyyy-MM-dd HH:mm:ss" | ||||
|                                                 modelType="yyyy-MM-dd HH:mm:ss" | ||||
|                                             /> | ||||
|                                         </div> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </template> | ||||
|  | ||||
|                             <div class="mt-4 mb-1"> | ||||
| @@ -231,6 +252,7 @@ import { useToast } from "vue-toastification"; | ||||
| import VueMultiselect from "vue-multiselect"; | ||||
| import dayjs from "dayjs"; | ||||
| import Datepicker from "@vuepic/vue-datepicker"; | ||||
| import { timezoneList } from "../util-frontend"; | ||||
|  | ||||
| const toast = useToast(); | ||||
|  | ||||
| @@ -242,6 +264,7 @@ export default { | ||||
|  | ||||
|     data() { | ||||
|         return { | ||||
|             timezoneList: timezoneList(), | ||||
|             processing: false, | ||||
|             maintenance: {}, | ||||
|             affectedMonitors: [], | ||||
| @@ -381,6 +404,7 @@ export default { | ||||
|                     }], | ||||
|                     weekdays: [], | ||||
|                     daysOfMonth: [], | ||||
|                     timezone: null, | ||||
|                 }; | ||||
|             } else if (this.isEdit) { | ||||
|                 this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user