WIP
This commit is contained in:
parent
4ae437dd61
commit
02291730fe
|
@ -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;
|
|
@ -26,6 +26,7 @@
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
"compare-versions": "~3.6.0",
|
"compare-versions": "~3.6.0",
|
||||||
"compression": "~1.7.4",
|
"compression": "~1.7.4",
|
||||||
|
"croner": "^6.0.3",
|
||||||
"dayjs": "~1.11.5",
|
"dayjs": "~1.11.5",
|
||||||
"dotenv": "~16.0.3",
|
"dotenv": "~16.0.3",
|
||||||
"express": "~4.17.3",
|
"express": "~4.17.3",
|
||||||
|
@ -7243,6 +7244,14 @@
|
||||||
"yup": "0.32.9"
|
"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": {
|
"node_modules/cross-env": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||||
|
|
|
@ -85,6 +85,7 @@
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
"compare-versions": "~3.6.0",
|
"compare-versions": "~3.6.0",
|
||||||
"compression": "~1.7.4",
|
"compression": "~1.7.4",
|
||||||
|
"croner": "^6.0.3",
|
||||||
"dayjs": "~1.11.5",
|
"dayjs": "~1.11.5",
|
||||||
"dotenv": "~16.0.3",
|
"dotenv": "~16.0.3",
|
||||||
"express": "~4.17.3",
|
"express": "~4.17.3",
|
||||||
|
|
|
@ -74,6 +74,7 @@ class Database {
|
||||||
"patch-add-description-monitor.sql": true,
|
"patch-add-description-monitor.sql": true,
|
||||||
"patch-api-key-table.sql": true,
|
"patch-api-key-table.sql": true,
|
||||||
"patch-monitor-tls.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 { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
|
const Cron = require("croner");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
const apicache = require("../modules/apicache");
|
||||||
|
|
||||||
class Maintenance extends BeanModel {
|
class Maintenance extends BeanModel {
|
||||||
|
|
||||||
|
static statusList = {};
|
||||||
|
static jobList = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an object that ready to parse to JSON for public
|
* Return an object that ready to parse to JSON for public
|
||||||
* Only show necessary data to public
|
* Only show necessary data to public
|
||||||
|
@ -15,16 +21,16 @@ class Maintenance extends BeanModel {
|
||||||
|
|
||||||
let dateRange = [];
|
let dateRange = [];
|
||||||
if (this.start_date) {
|
if (this.start_date) {
|
||||||
dateRange.push(utcToLocal(this.start_date));
|
dateRange.push(this.start_date);
|
||||||
if (this.end_date) {
|
if (this.end_date) {
|
||||||
dateRange.push(utcToLocal(this.end_date));
|
dateRange.push(this.end_date);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeRange = [];
|
let timeRange = [];
|
||||||
let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
|
let startTime = parseTimeObject(this.start_time);
|
||||||
timeRange.push(startTime);
|
timeRange.push(startTime);
|
||||||
let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
|
let endTime = parseTimeObject(this.end_time);
|
||||||
timeRange.push(endTime);
|
timeRange.push(endTime);
|
||||||
|
|
||||||
let obj = {
|
let obj = {
|
||||||
|
@ -39,12 +45,18 @@ class Maintenance extends BeanModel {
|
||||||
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
|
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
|
||||||
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
|
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
|
||||||
timeslotList: [],
|
timeslotList: [],
|
||||||
|
cron: this.cron,
|
||||||
|
duration: this.duration,
|
||||||
|
timezone: await this.getTimezone(),
|
||||||
|
timezoneOffset: await this.getTimezoneOffset(),
|
||||||
|
status: await this.getStatus(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeslotList = await this.getTimeslotList();
|
if (this.strategy === "single") {
|
||||||
|
obj.timeslotList.push({
|
||||||
for (let timeslot of timeslotList) {
|
startDate: this.start_date,
|
||||||
obj.timeslotList.push(await timeslot.toPublicJSON());
|
endDate: this.end_date,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(obj.weekdays)) {
|
if (!Array.isArray(obj.weekdays)) {
|
||||||
|
@ -55,54 +67,9 @@ class Maintenance extends BeanModel {
|
||||||
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;
|
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
|
* Return an object that ready to parse to JSON
|
||||||
* @param {string} timezone If not specified, the timeRange will be in UTC
|
* @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
|
* Get the duration of maintenance in seconds
|
||||||
* @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
|
|
||||||
* @returns {number} Duration of maintenance
|
* @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");
|
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
|
// Add 24hours if it is across day
|
||||||
if (duration < 0) {
|
if (duration < 0) {
|
||||||
|
@ -169,30 +120,24 @@ class Maintenance extends BeanModel {
|
||||||
* @param {Object} obj Data to fill bean with
|
* @param {Object} obj Data to fill bean with
|
||||||
* @returns {Bean} Filled bean
|
* @returns {Bean} Filled bean
|
||||||
*/
|
*/
|
||||||
static jsonToBean(bean, obj) {
|
static async jsonToBean(bean, obj) {
|
||||||
if (obj.id) {
|
if (obj.id) {
|
||||||
bean.id = 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.title = obj.title;
|
||||||
bean.description = obj.description;
|
bean.description = obj.description;
|
||||||
bean.strategy = obj.strategy;
|
bean.strategy = obj.strategy;
|
||||||
bean.interval_day = obj.intervalDay;
|
bean.interval_day = obj.intervalDay;
|
||||||
|
bean.timezone = obj.timezone;
|
||||||
|
bean.duration = obj.duration;
|
||||||
bean.active = obj.active;
|
bean.active = obj.active;
|
||||||
|
|
||||||
if (obj.dateRange[0]) {
|
if (obj.dateRange[0]) {
|
||||||
bean.start_date = localToUTC(obj.dateRange[0]);
|
bean.start_date = obj.dateRange[0];
|
||||||
|
|
||||||
if (obj.dateRange[1]) {
|
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.weekdays = JSON.stringify(obj.weekdays);
|
||||||
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
||||||
|
|
||||||
|
await bean.generateCron();
|
||||||
|
|
||||||
return bean;
|
return bean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SQL conditions for active maintenance
|
* Run the cron
|
||||||
* @returns {string}
|
|
||||||
*/
|
*/
|
||||||
static getActiveMaintenanceSQLCondition() {
|
async run() {
|
||||||
return `
|
if (Maintenance.jobList[this.id]) {
|
||||||
(
|
log.debug("maintenance", "Maintenance is already running, stop it first. id: " + this.id);
|
||||||
(maintenance_timeslot.start_date <= DATETIME('now')
|
this.stop();
|
||||||
AND maintenance_timeslot.end_date >= DATETIME('now')
|
}
|
||||||
AND maintenance.active = 1)
|
|
||||||
OR
|
log.debug("maintenance", "Run maintenance id: " + this.id);
|
||||||
(maintenance.strategy = 'manual' AND active = 1)
|
|
||||||
)
|
// 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;
|
module.exports = MaintenanceTimeslot;
|
||||||
|
|
|
@ -16,7 +16,6 @@ const apicache = require("../modules/apicache");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||||
const { DockerHost } = require("../docker");
|
const { DockerHost } = require("../docker");
|
||||||
const Maintenance = require("./maintenance");
|
|
||||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
const { UptimeCacheList } = require("../uptime-cache-list");
|
||||||
const Gamedig = require("gamedig");
|
const Gamedig = require("gamedig");
|
||||||
|
|
||||||
|
@ -1303,18 +1302,19 @@ class Monitor extends BeanModel {
|
||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
static async isUnderMaintenance(monitorID) {
|
static async isUnderMaintenance(monitorID) {
|
||||||
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
|
const maintenanceIDList = await R.getCol(`
|
||||||
const maintenance = await R.getRow(`
|
SELECT maintenance_id FROM monitor_maintenance
|
||||||
SELECT COUNT(*) AS count
|
WHERE monitor_id = ?
|
||||||
FROM monitor_maintenance mm
|
`, [ monitorID ]);
|
||||||
JOIN maintenance
|
|
||||||
ON mm.maintenance_id = maintenance.id
|
for (const maintenanceID of maintenanceIDList) {
|
||||||
AND mm.monitor_id = ?
|
const maintenance = await UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
|
||||||
LEFT JOIN maintenance_timeslot
|
if (maintenance && await maintenance.isUnderMaintenance()) {
|
||||||
ON maintenance_timeslot.maintenance_id = maintenance.id
|
return true;
|
||||||
WHERE ${activeCondition}
|
}
|
||||||
LIMIT 1`, [ monitorID ]);
|
}
|
||||||
return maintenance.count !== 0;
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Make sure monitor interval is between bounds */
|
/** Make sure monitor interval is between bounds */
|
||||||
|
|
|
@ -5,7 +5,6 @@ const apicache = require("../modules/apicache");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const Maintenance = require("../model/maintenance");
|
const Maintenance = require("../model/maintenance");
|
||||||
const server = UptimeKumaServer.getInstance();
|
const server = UptimeKumaServer.getInstance();
|
||||||
const MaintenanceTimeslot = require("../model/maintenance_timeslot");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handlers for Maintenance
|
* Handlers for Maintenance
|
||||||
|
@ -19,10 +18,12 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||||
|
|
||||||
log.debug("maintenance", maintenance);
|
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;
|
bean.user_id = socket.userID;
|
||||||
let maintenanceID = await R.store(bean);
|
let maintenanceID = await R.store(bean);
|
||||||
await MaintenanceTimeslot.generateTimeslot(bean);
|
|
||||||
|
server.maintenanceList[maintenanceID] = bean;
|
||||||
|
bean.run();
|
||||||
|
|
||||||
await server.sendMaintenanceList(socket);
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
@ -45,17 +46,15 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
let bean = await R.findOne("maintenance", " id = ? ", [ maintenance.id ]);
|
let bean = server.getMaintenance(maintenance.id);
|
||||||
|
|
||||||
if (bean.user_id !== socket.userID) {
|
if (bean.user_id !== socket.userID) {
|
||||||
throw new Error("Permission denied.");
|
throw new Error("Permission denied.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Maintenance.jsonToBean(bean, maintenance);
|
await Maintenance.jsonToBean(bean, maintenance);
|
||||||
|
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
await MaintenanceTimeslot.generateTimeslot(bean, null, true);
|
await bean.run();
|
||||||
|
|
||||||
await server.sendMaintenanceList(socket);
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
|
@ -236,6 +235,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||||
log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
if (maintenanceID in server.maintenanceList) {
|
if (maintenanceID in server.maintenanceList) {
|
||||||
|
server.maintenanceList[maintenanceID].stop();
|
||||||
delete server.maintenanceList[maintenanceID];
|
delete server.maintenanceList[maintenanceID];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,9 +267,16 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||||
|
|
||||||
log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
await R.exec("UPDATE maintenance SET active = 0 WHERE id = ? ", [
|
let maintenance = server.getMaintenance(maintenanceID);
|
||||||
maintenanceID,
|
|
||||||
]);
|
if (!maintenance) {
|
||||||
|
throw new Error("Maintenance not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
maintenance.active = false;
|
||||||
|
maintenance.setStatus("inactive");
|
||||||
|
await R.store(maintenance);
|
||||||
|
maintenance.stop();
|
||||||
|
|
||||||
apicache.clear();
|
apicache.clear();
|
||||||
|
|
||||||
|
@ -294,9 +301,15 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||||
|
|
||||||
log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
await R.exec("UPDATE maintenance SET active = 1 WHERE id = ? ", [
|
let maintenance = server.getMaintenance(maintenanceID);
|
||||||
maintenanceID,
|
|
||||||
]);
|
if (!maintenance) {
|
||||||
|
throw new Error("Maintenance not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
maintenance.active = true;
|
||||||
|
await R.store(maintenance);
|
||||||
|
await maintenance.run();
|
||||||
|
|
||||||
apicache.clear();
|
apicache.clear();
|
||||||
|
|
||||||
|
|
|
@ -47,8 +47,6 @@ class UptimeKumaServer {
|
||||||
*/
|
*/
|
||||||
indexHTML = "";
|
indexHTML = "";
|
||||||
|
|
||||||
generateMaintenanceTimeslotsInterval = undefined;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugins Manager
|
* Plugins Manager
|
||||||
* @type {PluginsManager}
|
* @type {PluginsManager}
|
||||||
|
@ -112,8 +110,7 @@ class UptimeKumaServer {
|
||||||
log.debug("DEBUG", "Timezone: " + process.env.TZ);
|
log.debug("DEBUG", "Timezone: " + process.env.TZ);
|
||||||
log.debug("DEBUG", "Current Time: " + dayjs.tz().format());
|
log.debug("DEBUG", "Current Time: " + dayjs.tz().format());
|
||||||
|
|
||||||
await this.generateMaintenanceTimeslots();
|
await this.loadMaintenanceList();
|
||||||
this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -175,16 +172,33 @@ class UptimeKumaServer {
|
||||||
*/
|
*/
|
||||||
async getMaintenanceJSONList(userID) {
|
async getMaintenanceJSONList(userID) {
|
||||||
let result = {};
|
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) {
|
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
|
* Attempt to get the current server timezone
|
||||||
* If this fails, fall back to environment variables and then make a
|
* If this fails, fall back to environment variables and then make a
|
||||||
* guess.
|
* guess.
|
||||||
* @returns {string}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
async getTimezone() {
|
async getTimezone() {
|
||||||
let timezone = await Settings.get("serverTimezone");
|
let timezone = await Settings.get("serverTimezone");
|
||||||
|
@ -271,28 +285,9 @@ class UptimeKumaServer {
|
||||||
dayjs.tz.setDefault(timezone);
|
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 */
|
/** Stop the server */
|
||||||
async stop() {
|
async stop() {
|
||||||
clearTimeout(this.generateMaintenanceTimeslotsInterval);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPlugins() {
|
loadPlugins() {
|
||||||
|
@ -341,5 +336,4 @@ module.exports = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Must be at the end
|
// Must be at the end
|
||||||
const MaintenanceTimeslot = require("./model/maintenance_timeslot");
|
|
||||||
const { MonitorType } = require("./monitor-types/monitor-type");
|
const { MonitorType } = require("./monitor-types/monitor-type");
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
{{ $t("Manual") }}
|
{{ $t("Manual") }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="maintenance.timeslotList.length > 0" class="timeslot">
|
<div v-else-if="maintenance.timeslotList.length > 0" class="timeslot">
|
||||||
{{ maintenance.timeslotList[0].startDateServerTimezone }}
|
{{ maintenance.timeslotList[0].startDate }}
|
||||||
<span class="to">-</span>
|
<span class="to">-</span>
|
||||||
{{ maintenance.timeslotList[0].endDateServerTimezone }}
|
{{ maintenance.timeslotList[0].endDate }}
|
||||||
(UTC{{ maintenance.timeslotList[0].serverTimezoneOffset }})
|
(UTC{{ maintenance.timezoneOffset }})
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -394,6 +394,10 @@
|
||||||
"backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.",
|
"backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.",
|
||||||
"Optional": "Optional",
|
"Optional": "Optional",
|
||||||
"or": "or",
|
"or": "or",
|
||||||
|
"sameAsServerTimezone": "Same as Server Timezone",
|
||||||
|
"startDateTime": "Start Date/Time",
|
||||||
|
"endDateTime": "End Date/Time",
|
||||||
|
"cronExpression": "Cron Expression",
|
||||||
"recurringInterval": "Interval",
|
"recurringInterval": "Interval",
|
||||||
"Recurring": "Recurring",
|
"Recurring": "Recurring",
|
||||||
"strategyManual": "Active/Inactive Manually",
|
"strategyManual": "Active/Inactive Manually",
|
||||||
|
|
|
@ -85,14 +85,13 @@
|
||||||
|
|
||||||
<h2 class="mt-5">{{ $t("Date and Time") }}</h2>
|
<h2 class="mt-5">{{ $t("Date and Time") }}</h2>
|
||||||
|
|
||||||
<div>⚠️ {{ $t("warningTimezone") }}: <mark>{{ $root.info.serverTimezone }} ({{ $root.info.serverTimezoneOffset }})</mark></div>
|
|
||||||
|
|
||||||
<!-- Strategy -->
|
<!-- Strategy -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="strategy" class="form-label">{{ $t("Strategy") }}</label>
|
<label for="strategy" class="form-label">{{ $t("Strategy") }}</label>
|
||||||
<select id="strategy" v-model="maintenance.strategy" class="form-select">
|
<select id="strategy" v-model="maintenance.strategy" class="form-select">
|
||||||
<option value="manual">{{ $t("strategyManual") }}</option>
|
<option value="manual">{{ $t("strategyManual") }}</option>
|
||||||
<option value="single">{{ $t("Single Maintenance Window") }}</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-interval">{{ $t("Recurring") }} - {{ $t("recurringInterval") }}</option>
|
||||||
<option value="recurring-weekday">{{ $t("Recurring") }} - {{ $t("dayOfWeek") }}</option>
|
<option value="recurring-weekday">{{ $t("Recurring") }} - {{ $t("dayOfWeek") }}</option>
|
||||||
<option value="recurring-day-of-month">{{ $t("Recurring") }} - {{ $t("dayOfMonth") }}</option>
|
<option value="recurring-day-of-month">{{ $t("Recurring") }} - {{ $t("dayOfMonth") }}</option>
|
||||||
|
@ -102,19 +101,6 @@
|
||||||
|
|
||||||
<!-- Single Maintenance Window -->
|
<!-- Single Maintenance Window -->
|
||||||
<template v-if="maintenance.strategy === 'single'">
|
<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>
|
</template>
|
||||||
|
|
||||||
<!-- Recurring - Interval -->
|
<!-- Recurring - Interval -->
|
||||||
|
@ -180,7 +166,6 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- For any recurring types -->
|
|
||||||
<template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month'">
|
<template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month'">
|
||||||
<!-- Maintenance Time Window of a Day -->
|
<!-- Maintenance Time Window of a Day -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
|
@ -192,21 +177,57 @@
|
||||||
disableTimeRangeValidation range
|
disableTimeRangeValidation range
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 -->
|
<!-- Date Range -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label class="form-label">{{ $t("Effective Date Range") }}</label>
|
<label class="form-label">{{ $t("Effective Date Range") }}</label>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="mb-2">{{ $t("startDateTime") }}</div>
|
||||||
<Datepicker
|
<Datepicker
|
||||||
v-model="maintenance.dateRange"
|
v-model="maintenance.dateRange[0]"
|
||||||
:dark="$root.isDark"
|
:dark="$root.isDark"
|
||||||
range datePicker
|
datePicker
|
||||||
:monthChangeOnScroll="false"
|
:monthChangeOnScroll="false"
|
||||||
:minDate="minDate"
|
|
||||||
format="yyyy-MM-dd HH:mm:ss"
|
format="yyyy-MM-dd HH:mm:ss"
|
||||||
modelType="yyyy-MM-dd HH:mm:ss"
|
modelType="yyyy-MM-dd HH:mm:ss"
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<div class="mt-4 mb-1">
|
<div class="mt-4 mb-1">
|
||||||
|
@ -231,6 +252,7 @@ import { useToast } from "vue-toastification";
|
||||||
import VueMultiselect from "vue-multiselect";
|
import VueMultiselect from "vue-multiselect";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import Datepicker from "@vuepic/vue-datepicker";
|
import Datepicker from "@vuepic/vue-datepicker";
|
||||||
|
import { timezoneList } from "../util-frontend";
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
@ -242,6 +264,7 @@ export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
timezoneList: timezoneList(),
|
||||||
processing: false,
|
processing: false,
|
||||||
maintenance: {},
|
maintenance: {},
|
||||||
affectedMonitors: [],
|
affectedMonitors: [],
|
||||||
|
@ -381,6 +404,7 @@ export default {
|
||||||
}],
|
}],
|
||||||
weekdays: [],
|
weekdays: [],
|
||||||
daysOfMonth: [],
|
daysOfMonth: [],
|
||||||
|
timezone: null,
|
||||||
};
|
};
|
||||||
} else if (this.isEdit) {
|
} else if (this.isEdit) {
|
||||||
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
|
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
|
||||||
|
|
Loading…
Reference in New Issue