mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-28 01:04:05 +00:00
Merge pull request #3003 from louislam/maintenance-fix
Maintenance Core Bug Fix & Improvements
This commit is contained in:
commit
0f5a243450
16 changed files with 521 additions and 490 deletions
11
db/patch-maintenance-cron.sql
Normal file
11
db/patch-maintenance-cron.sql
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
DROP TABLE maintenance_timeslot;
|
||||||
|
|
||||||
|
-- 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;
|
19
package-lock.json
generated
19
package-lock.json
generated
|
@ -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",
|
||||||
|
@ -87,6 +88,7 @@
|
||||||
"chartjs-adapter-dayjs": "~1.0.0",
|
"chartjs-adapter-dayjs": "~1.0.0",
|
||||||
"concurrently": "^7.1.0",
|
"concurrently": "^7.1.0",
|
||||||
"core-js": "~3.26.1",
|
"core-js": "~3.26.1",
|
||||||
|
"cronstrue": "~2.24.0",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"cypress": "^10.1.0",
|
"cypress": "^10.1.0",
|
||||||
"delay": "^5.0.0",
|
"delay": "^5.0.0",
|
||||||
|
@ -7243,6 +7245,23 @@
|
||||||
"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/cronstrue": {
|
||||||
|
"version": "2.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.24.0.tgz",
|
||||||
|
"integrity": "sha512-A1of24mAGz+OWrdGsxT9BOnDqn2ba182hie8Jx0UcEC2t+ZKtfAJxaFntKUgR7sIisU297fgHBSlNhMIfvAkSA==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"cronstrue": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"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",
|
||||||
|
@ -146,6 +147,7 @@
|
||||||
"chartjs-adapter-dayjs": "~1.0.0",
|
"chartjs-adapter-dayjs": "~1.0.0",
|
||||||
"concurrently": "^7.1.0",
|
"concurrently": "^7.1.0",
|
||||||
"core-js": "~3.26.1",
|
"core-js": "~3.26.1",
|
||||||
|
"cronstrue": "~2.24.0",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"cypress": "^10.1.0",
|
"cypress": "^10.1.0",
|
||||||
"delay": "^5.0.0",
|
"delay": "^5.0.0",
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
|
const { parseTimeObject, parseTimeFromTimeObject, log } = require("../../src/util");
|
||||||
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 {
|
||||||
|
|
||||||
|
@ -15,16 +17,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 +41,43 @@ 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,
|
||||||
|
durationMinutes: parseInt(this.duration / 60),
|
||||||
|
timezone: await this.getTimezone(),
|
||||||
|
timezoneOffset: await this.getTimezoneOffset(),
|
||||||
|
status: await this.getStatus(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeslotList = await this.getTimeslotList();
|
if (this.strategy === "manual") {
|
||||||
|
// Do nothing, no timeslots
|
||||||
|
} else if (this.strategy === "single") {
|
||||||
|
obj.timeslotList.push({
|
||||||
|
startDate: this.start_date,
|
||||||
|
endDate: this.end_date,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Should be cron or recurring here
|
||||||
|
if (this.beanMeta.job) {
|
||||||
|
let runningTimeslot = this.getRunningTimeslot();
|
||||||
|
|
||||||
for (let timeslot of timeslotList) {
|
if (runningTimeslot) {
|
||||||
obj.timeslotList.push(await timeslot.toPublicJSON());
|
obj.timeslotList.push(runningTimeslot);
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextRunDate = this.beanMeta.job.nextRun();
|
||||||
|
if (nextRunDate) {
|
||||||
|
let startDateDayjs = dayjs(nextRunDate);
|
||||||
|
|
||||||
|
let startDate = startDateDayjs.toISOString();
|
||||||
|
let endDate = startDateDayjs.add(this.duration, "second").toISOString();
|
||||||
|
|
||||||
|
obj.timeslotList.push({
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(obj.weekdays)) {
|
if (!Array.isArray(obj.weekdays)) {
|
||||||
|
@ -55,54 +88,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
|
||||||
|
@ -126,7 +114,7 @@ class Maintenance extends BeanModel {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of days in month that maintenance is active for
|
* Get a list of days in month that maintenance is active for
|
||||||
* @returns {number[]} Array of active days in month
|
* @returns {number[]|string[]} Array of active days in month
|
||||||
*/
|
*/
|
||||||
getDayOfMonthList() {
|
getDayOfMonthList() {
|
||||||
return JSON.parse(this.days_of_month).sort(function (a, b) {
|
return JSON.parse(this.days_of_month).sort(function (a, b) {
|
||||||
|
@ -135,26 +123,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,71 +141,255 @@ 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.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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bean.strategy === "cron") {
|
||||||
|
bean.duration = obj.durationMinutes * 60;
|
||||||
|
bean.cron = obj.cron;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bean.strategy.startsWith("recurring-")) {
|
||||||
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
||||||
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
||||||
|
|
||||||
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(throwError = false) {
|
||||||
return `
|
if (this.beanMeta.job) {
|
||||||
(
|
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) {
|
||||||
|
await this.generateCron();
|
||||||
|
if (!this.timezone) {
|
||||||
|
this.timezone = "UTC";
|
||||||
|
}
|
||||||
|
if (this.cron) {
|
||||||
|
await R.store(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.strategy === "manual") {
|
||||||
|
// Do nothing, because it is controlled by the user
|
||||||
|
} else if (this.strategy === "single") {
|
||||||
|
this.beanMeta.job = 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();
|
||||||
|
});
|
||||||
|
} else if (this.cron != null) {
|
||||||
|
// Here should be cron or recurring
|
||||||
|
try {
|
||||||
|
this.beanMeta.status = "scheduled";
|
||||||
|
|
||||||
|
let startEvent = (customDuration = 0) => {
|
||||||
|
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
||||||
|
|
||||||
|
this.beanMeta.status = "under-maintenance";
|
||||||
|
clearTimeout(this.beanMeta.durationTimeout);
|
||||||
|
|
||||||
|
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
|
||||||
|
let duration;
|
||||||
|
|
||||||
|
if (customDuration > 0) {
|
||||||
|
duration = customDuration;
|
||||||
|
} else if (this.end_date) {
|
||||||
|
let d = dayjs(this.end_date).diff(dayjs(), "second");
|
||||||
|
if (d < this.duration) {
|
||||||
|
duration = d * 1000;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
duration = this.duration * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||||
|
|
||||||
|
this.beanMeta.durationTimeout = setTimeout(() => {
|
||||||
|
// End of maintenance for this timeslot
|
||||||
|
this.beanMeta.status = "scheduled";
|
||||||
|
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||||
|
}, duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create Cron
|
||||||
|
this.beanMeta.job = new Cron(this.cron, {
|
||||||
|
timezone: await this.getTimezone(),
|
||||||
|
}, startEvent);
|
||||||
|
|
||||||
|
// Continue if the maintenance is still in the window
|
||||||
|
let runningTimeslot = this.getRunningTimeslot();
|
||||||
|
let current = dayjs();
|
||||||
|
|
||||||
|
if (runningTimeslot) {
|
||||||
|
let duration = dayjs(runningTimeslot.endDate).diff(current, "second") * 1000;
|
||||||
|
log.debug("maintenance", "Maintenance id: " + this.id + " Remaining duration: " + duration + "ms");
|
||||||
|
startEvent(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
log.error("maintenance", "Error in maintenance id: " + this.id);
|
||||||
|
log.error("maintenance", "Cron: " + this.cron);
|
||||||
|
log.error("maintenance", e);
|
||||||
|
|
||||||
|
if (throwError) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.error("maintenance", "Maintenance id: " + this.id + " has no cron");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRunningTimeslot() {
|
||||||
|
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").format("YYYY-MM-DD HH:mm:ss")));
|
||||||
|
let end = start.add(this.duration, "second");
|
||||||
|
let current = dayjs();
|
||||||
|
|
||||||
|
if (current.isAfter(start) && current.isBefore(end)) {
|
||||||
|
return {
|
||||||
|
startDate: start.toISOString(),
|
||||||
|
endDate: end.toISOString(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.beanMeta.job) {
|
||||||
|
this.beanMeta.job.stop();
|
||||||
|
delete this.beanMeta.job;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (!this.beanMeta.status) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.beanMeta.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SQL conditions for active and future maintenance
|
* Generate Cron for recurring maintenance
|
||||||
* @returns {string}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static getActiveAndFutureMaintenanceSQLCondition() {
|
async generateCron() {
|
||||||
return `
|
log.info("maintenance", "Generate cron for maintenance id: " + this.id);
|
||||||
(
|
|
||||||
((maintenance_timeslot.end_date >= DATETIME('now')
|
if (this.strategy === "cron") {
|
||||||
AND maintenance.active = 1)
|
// Do nothing for cron
|
||||||
OR
|
} else if (!this.strategy.startsWith("recurring-")) {
|
||||||
(maintenance.strategy = 'manual' AND active = 1))
|
this.cron = "";
|
||||||
)
|
} else 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);
|
||||||
|
} else if (this.strategy === "recurring-weekday") {
|
||||||
|
let list = this.getDayOfWeekList();
|
||||||
|
let array = this.start_time.split(":");
|
||||||
|
let hour = parseInt(array[0]);
|
||||||
|
let minute = parseInt(array[1]);
|
||||||
|
this.cron = minute + " " + hour + " * * " + list.join(",");
|
||||||
|
this.duration = this.calcDuration();
|
||||||
|
} else if (this.strategy === "recurring-day-of-month") {
|
||||||
|
let list = this.getDayOfMonthList();
|
||||||
|
let array = this.start_time.split(":");
|
||||||
|
let hour = parseInt(array[0]);
|
||||||
|
let minute = parseInt(array[1]);
|
||||||
|
|
||||||
|
let dayList = [];
|
||||||
|
|
||||||
|
for (let day of list) {
|
||||||
|
if (typeof day === "string" && day.startsWith("lastDay")) {
|
||||||
|
if (day === "lastDay1") {
|
||||||
|
dayList.push("L");
|
||||||
|
}
|
||||||
|
// Unfortunately, lastDay2-4 is not supported by cron
|
||||||
|
} else {
|
||||||
|
dayList.push(day);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicate
|
||||||
|
dayList = [ ...new Set(dayList) ];
|
||||||
|
|
||||||
|
this.cron = minute + " " + hour + " " + dayList.join(",") + " * *";
|
||||||
|
this.duration = this.calcDuration();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,223 +0,0 @@
|
||||||
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 {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an object that ready to parse to JSON for public
|
|
||||||
* Only show necessary data to public
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an object that ready to parse to JSON
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
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) {
|
|
||||||
log.info("maintenance", "Generate Timeslot for maintenance id: " + maintenance.id);
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
if (!await this.isDuplicateTimeslot(bean)) {
|
|
||||||
await R.store(bean);
|
|
||||||
return bean;
|
|
||||||
} else {
|
|
||||||
log.debug("maintenance", "Duplicate timeslot, skip");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
} 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 { 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 */
|
||||||
|
|
|
@ -3,7 +3,6 @@ const { R } = require("redbean-node");
|
||||||
const cheerio = require("cheerio");
|
const cheerio = require("cheerio");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const jsesc = require("jsesc");
|
const jsesc = require("jsesc");
|
||||||
const Maintenance = require("./maintenance");
|
|
||||||
const googleAnalytics = require("../google-analytics");
|
const googleAnalytics = require("../google-analytics");
|
||||||
|
|
||||||
class StatusPage extends BeanModel {
|
class StatusPage extends BeanModel {
|
||||||
|
@ -290,21 +289,17 @@ class StatusPage extends BeanModel {
|
||||||
try {
|
try {
|
||||||
const publicMaintenanceList = [];
|
const publicMaintenanceList = [];
|
||||||
|
|
||||||
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
|
let maintenanceIDList = await R.getCol(`
|
||||||
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
|
SELECT DISTINCT maintenance_id
|
||||||
SELECT DISTINCT maintenance.*
|
FROM maintenance_status_page
|
||||||
FROM maintenance
|
WHERE status_page_id = ?
|
||||||
JOIN maintenance_status_page
|
`, [ statusPageId ]);
|
||||||
ON maintenance_status_page.maintenance_id = maintenance.id
|
|
||||||
AND maintenance_status_page.status_page_id = ?
|
|
||||||
LEFT JOIN maintenance_timeslot
|
|
||||||
ON maintenance_timeslot.maintenance_id = maintenance.id
|
|
||||||
WHERE ${activeCondition}
|
|
||||||
ORDER BY maintenance.end_date
|
|
||||||
`, [ statusPageId ]));
|
|
||||||
|
|
||||||
for (const bean of maintenanceBeanList) {
|
for (const maintenanceID of maintenanceIDList) {
|
||||||
publicMaintenanceList.push(await bean.toPublicJSON());
|
let maintenance = UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
|
||||||
|
if (maintenance && await maintenance.isUnderMaintenance()) {
|
||||||
|
publicMaintenanceList.push(await maintenance.toPublicJSON());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return publicMaintenanceList;
|
return publicMaintenanceList;
|
||||||
|
|
|
@ -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;
|
||||||
|
await bean.run(true);
|
||||||
|
|
||||||
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(true);
|
||||||
|
|
||||||
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,15 @@ 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;
|
||||||
|
await R.store(maintenance);
|
||||||
|
maintenance.stop();
|
||||||
|
|
||||||
apicache.clear();
|
apicache.clear();
|
||||||
|
|
||||||
|
@ -294,9 +300,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");
|
||||||
|
|
|
@ -556,6 +556,31 @@ h5.settings-subheading::after {
|
||||||
border-bottom: 1px solid $dark-border-color;
|
border-bottom: 1px solid $dark-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$shadow-box-padding: 20px;
|
||||||
|
|
||||||
|
.shadow-box-with-fixed-bottom-bar {
|
||||||
|
padding-top: $shadow-box-padding;
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-right: $shadow-box-padding;
|
||||||
|
padding-left: $shadow-box-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-bottom-bar {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
margin-left: -$shadow-box-padding;
|
||||||
|
margin-right: -$shadow-box-padding;
|
||||||
|
z-index: 100;
|
||||||
|
background-color: rgba(white, 0.2);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: rgba($dark-header-bg, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Localization
|
// Localization
|
||||||
|
|
||||||
@import "localization.scss";
|
@import "localization.scss";
|
||||||
|
|
|
@ -3,16 +3,23 @@
|
||||||
<div v-if="maintenance.strategy === 'manual'" class="timeslot">
|
<div v-if="maintenance.strategy === 'manual'" class="timeslot">
|
||||||
{{ $t("Manual") }}
|
{{ $t("Manual") }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="maintenance.timeslotList.length > 0" class="timeslot">
|
<div v-else-if="maintenance.timeslotList.length > 0">
|
||||||
{{ maintenance.timeslotList[0].startDateServerTimezone }}
|
<div class="timeslot">
|
||||||
|
{{ startDateTime }}
|
||||||
<span class="to">-</span>
|
<span class="to">-</span>
|
||||||
{{ maintenance.timeslotList[0].endDateServerTimezone }}
|
{{ endDateTime }}
|
||||||
(UTC{{ maintenance.timeslotList[0].serverTimezoneOffset }})
|
</div>
|
||||||
|
<div class="timeslot">
|
||||||
|
UTC{{ maintenance.timezoneOffset }} <span v-if="maintenance.timezone !== 'UTC'">{{ maintenance.timezone }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { SQL_DATETIME_FORMAT_WITHOUT_SECOND } from "../util.ts";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
maintenance: {
|
maintenance: {
|
||||||
|
@ -20,6 +27,14 @@ export default {
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
startDateTime() {
|
||||||
|
return dayjs(this.maintenance.timeslotList[0].startDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
|
||||||
|
},
|
||||||
|
endDateTime() {
|
||||||
|
return dayjs(this.maintenance.timeslotList[0].endDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -31,6 +46,7 @@ export default {
|
||||||
background-color: rgba(255, 255, 255, 0.5);
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
margin-right: 5px;
|
||||||
|
|
||||||
.to {
|
.to {
|
||||||
margin: 0 6px;
|
margin: 0 6px;
|
||||||
|
|
|
@ -394,6 +394,12 @@
|
||||||
"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",
|
||||||
|
"cronSchedule": "Schedule: ",
|
||||||
|
"invalidCronExpression": "Invalid Cron Expression: {0}",
|
||||||
"recurringInterval": "Interval",
|
"recurringInterval": "Interval",
|
||||||
"Recurring": "Recurring",
|
"Recurring": "Recurring",
|
||||||
"strategyManual": "Active/Inactive Manually",
|
"strategyManual": "Active/Inactive Manually",
|
||||||
|
@ -429,7 +435,7 @@
|
||||||
"dnsCacheDescription": "It may be not working in some IPv6 environments, disable it if you encounter any issues.",
|
"dnsCacheDescription": "It may be not working in some IPv6 environments, disable it if you encounter any issues.",
|
||||||
"Single Maintenance Window": "Single Maintenance Window",
|
"Single Maintenance Window": "Single Maintenance Window",
|
||||||
"Maintenance Time Window of a Day": "Maintenance Time Window of a Day",
|
"Maintenance Time Window of a Day": "Maintenance Time Window of a Day",
|
||||||
"Effective Date Range": "Effective Date Range",
|
"Effective Date Range": "Effective Date Range (Optional)",
|
||||||
"Schedule Maintenance": "Schedule Maintenance",
|
"Schedule Maintenance": "Schedule Maintenance",
|
||||||
"Date and Time": "Date and Time",
|
"Date and Time": "Date and Time",
|
||||||
"DateTime Range": "DateTime Range",
|
"DateTime Range": "DateTime Range",
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div>
|
<div>
|
||||||
<h1 class="mb-3">{{ pageName }}</h1>
|
<h1 class="mb-3">{{ pageName }}</h1>
|
||||||
<form @submit.prevent="submit">
|
<form @submit.prevent="submit">
|
||||||
<div class="shadow-box">
|
<div class="shadow-box shadow-box-with-fixed-bottom-bar">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-10">
|
<div class="col-xl-10">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
|
@ -85,35 +85,39 @@
|
||||||
|
|
||||||
<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>
|
||||||
<option v-if="false" value="recurring-day-of-year">{{ $t("Recurring") }} - Day of Year</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Single Maintenance Window -->
|
<!-- Single Maintenance Window -->
|
||||||
<template v-if="maintenance.strategy === 'single'">
|
<template v-if="maintenance.strategy === 'single'">
|
||||||
<!-- DateTime Range -->
|
</template>
|
||||||
|
|
||||||
|
<template v-if="maintenance.strategy === 'cron'">
|
||||||
|
<!-- Cron -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label class="form-label">{{ $t("DateTime Range") }}</label>
|
<label for="cron" class="form-label">
|
||||||
<Datepicker
|
{{ $t("cronExpression") }}
|
||||||
v-model="maintenance.dateRange"
|
</label>
|
||||||
:dark="$root.isDark"
|
<p>{{ $t("cronSchedule") }}{{ cronDescription }}</p>
|
||||||
range
|
<input id="cron" v-model="maintenance.cron" type="text" class="form-control" required>
|
||||||
:monthChangeOnScroll="false"
|
</div>
|
||||||
:minDate="minDate"
|
|
||||||
format="yyyy-MM-dd HH:mm"
|
<div class="my-3">
|
||||||
modelType="yyyy-MM-dd HH:mm:ss"
|
<!-- Duration -->
|
||||||
/>
|
<label for="duration" class="form-label">
|
||||||
|
{{ $t("Duration (Minutes)") }}
|
||||||
|
</label>
|
||||||
|
<input id="duration" v-model="maintenance.durationMinutes" type="number" class="form-control" required min="1" step="1">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -180,7 +184,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,32 +195,49 @@
|
||||||
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>
|
||||||
<Datepicker
|
|
||||||
v-model="maintenance.dateRange"
|
<div class="row">
|
||||||
:dark="$root.isDark"
|
<div class="col">
|
||||||
range datePicker
|
<div class="mb-2">{{ $t("startDateTime") }}</div>
|
||||||
:monthChangeOnScroll="false"
|
<input v-model="maintenance.dateRange[0]" type="datetime-local" class="form-control">
|
||||||
:minDate="minDate"
|
</div>
|
||||||
format="yyyy-MM-dd HH:mm:ss"
|
|
||||||
modelType="yyyy-MM-dd HH:mm:ss"
|
<div class="col">
|
||||||
required
|
<div class="mb-2">{{ $t("endDateTime") }}</div>
|
||||||
/>
|
<input v-model="maintenance.dateRange[1]" type="datetime-local" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 mb-1">
|
<div class="fixed-bottom-bar p-3">
|
||||||
<button
|
<button id="monitor-submit-btn" class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
|
||||||
id="monitor-submit-btn" class="btn btn-primary" type="submit"
|
|
||||||
:disabled="processing"
|
|
||||||
>
|
|
||||||
{{ $t("Save") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -226,11 +246,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import { useToast } from "vue-toastification";
|
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";
|
||||||
|
import cronstrue from "cronstrue/i18n";
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
@ -242,6 +263,7 @@ export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
timezoneList: timezoneList(),
|
||||||
processing: false,
|
processing: false,
|
||||||
maintenance: {},
|
maintenance: {},
|
||||||
affectedMonitors: [],
|
affectedMonitors: [],
|
||||||
|
@ -256,18 +278,6 @@ export default {
|
||||||
langKey: "lastDay1",
|
langKey: "lastDay1",
|
||||||
value: "lastDay1",
|
value: "lastDay1",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
langKey: "lastDay2",
|
|
||||||
value: "lastDay2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
langKey: "lastDay3",
|
|
||||||
value: "lastDay3",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
langKey: "lastDay4",
|
|
||||||
value: "lastDay4",
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
weekdays: [
|
weekdays: [
|
||||||
{
|
{
|
||||||
|
@ -311,6 +321,34 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
|
cronDescription() {
|
||||||
|
if (! this.maintenance.cron) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let locale = "";
|
||||||
|
|
||||||
|
if (this.$root.language) {
|
||||||
|
locale = this.$root.language.replace("-", "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling
|
||||||
|
// If locale is also not working in your language, you can map it here
|
||||||
|
// https://github.com/bradymholt/cRonstrue/tree/master/src/i18n/locales
|
||||||
|
if (locale === "zh_HK") {
|
||||||
|
locale = "zh_TW";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return cronstrue.toString(this.maintenance.cron, {
|
||||||
|
locale,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return this.$t("invalidCronExpression", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
selectedStatusPagesOptions() {
|
selectedStatusPagesOptions() {
|
||||||
return Object.values(this.$root.statusPageList).map(statusPage => {
|
return Object.values(this.$root.statusPageList).map(statusPage => {
|
||||||
return {
|
return {
|
||||||
|
@ -370,6 +408,8 @@ export default {
|
||||||
description: "",
|
description: "",
|
||||||
strategy: "single",
|
strategy: "single",
|
||||||
active: 1,
|
active: 1,
|
||||||
|
cron: "30 3 * * *",
|
||||||
|
durationMinutes: 60,
|
||||||
intervalDay: 1,
|
intervalDay: 1,
|
||||||
dateRange: [ this.minDate ],
|
dateRange: [ this.minDate ],
|
||||||
timeRange: [{
|
timeRange: [{
|
||||||
|
@ -381,6 +421,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) => {
|
||||||
|
@ -501,10 +542,6 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.shadow-box {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div>
|
<div>
|
||||||
<h1 class="mb-3">{{ pageName }}</h1>
|
<h1 class="mb-3">{{ pageName }}</h1>
|
||||||
<form @submit.prevent="submit">
|
<form @submit.prevent="submit">
|
||||||
<div class="shadow-box">
|
<div class="shadow-box shadow-box-with-fixed-bottom-bar">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2 class="mb-2">{{ $t("General") }}</h2>
|
<h2 class="mb-2">{{ $t("General") }}</h2>
|
||||||
|
@ -1102,31 +1102,7 @@ message HealthCheckResponse {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../assets/vars.scss";
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
$padding: 20px;
|
|
||||||
|
|
||||||
.shadow-box {
|
|
||||||
padding-top: $padding;
|
|
||||||
padding-bottom: 0;
|
|
||||||
padding-right: $padding;
|
|
||||||
padding-left: $padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fixed-bottom-bar {
|
|
||||||
position: sticky;
|
|
||||||
bottom: 0;
|
|
||||||
margin-left: -$padding;
|
|
||||||
margin-right: -$padding;
|
|
||||||
z-index: 100;
|
|
||||||
background-color: rgba(white, 0.2);
|
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
border-radius: 0 0 10px 10px;
|
|
||||||
|
|
||||||
.dark & {
|
|
||||||
background-color: rgba($dark-header-bg, 0.9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -923,7 +923,11 @@ export default {
|
||||||
* @returns {string} Sanitized HTML
|
* @returns {string} Sanitized HTML
|
||||||
*/
|
*/
|
||||||
maintenanceHTML(description) {
|
maintenanceHTML(description) {
|
||||||
|
if (description) {
|
||||||
return DOMPurify.sanitize(marked(description));
|
return DOMPurify.sanitize(marked(description));
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue