mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-23 23:04:04 +00:00
WIP
This commit is contained in:
parent
02291730fe
commit
227cec86a8
9 changed files with 241 additions and 227 deletions
|
@ -1,6 +1,8 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
BEGIN TRANSACTION;
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
DROP TABLE maintenance_timeslot;
|
||||||
|
|
||||||
-- 999 characters. https://stackoverflow.com/questions/46134830/maximum-length-for-cron-job
|
-- 999 characters. https://stackoverflow.com/questions/46134830/maximum-length-for-cron-job
|
||||||
ALTER TABLE maintenance ADD cron TEXT;
|
ALTER TABLE maintenance ADD cron TEXT;
|
||||||
ALTER TABLE maintenance ADD timezone VARCHAR(255);
|
ALTER TABLE maintenance ADD timezone VARCHAR(255);
|
||||||
|
|
10
package-lock.json
generated
10
package-lock.json
generated
|
@ -88,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",
|
||||||
|
@ -7252,6 +7253,15 @@
|
||||||
"node": ">=6.0"
|
"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",
|
||||||
|
|
|
@ -147,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",
|
||||||
|
|
|
@ -9,9 +9,6 @@ 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
|
||||||
|
@ -47,16 +44,41 @@ class Maintenance extends BeanModel {
|
||||||
timeslotList: [],
|
timeslotList: [],
|
||||||
cron: this.cron,
|
cron: this.cron,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
|
durationMinutes: parseInt(this.duration / 60),
|
||||||
timezone: await this.getTimezone(),
|
timezone: await this.getTimezone(),
|
||||||
timezoneOffset: await this.getTimezoneOffset(),
|
timezoneOffset: await this.getTimezoneOffset(),
|
||||||
status: await this.getStatus(),
|
status: await this.getStatus(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.strategy === "single") {
|
if (this.strategy === "manual") {
|
||||||
|
// Do nothing, no timeslots
|
||||||
|
} else if (this.strategy === "single") {
|
||||||
obj.timeslotList.push({
|
obj.timeslotList.push({
|
||||||
startDate: this.start_date,
|
startDate: this.start_date,
|
||||||
endDate: this.end_date,
|
endDate: this.end_date,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Should be cron or recurring here
|
||||||
|
if (this.beanMeta.job) {
|
||||||
|
let runningTimeslot = this.getRunningTimeslot();
|
||||||
|
|
||||||
|
if (runningTimeslot) {
|
||||||
|
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)) {
|
||||||
|
@ -93,7 +115,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) {
|
||||||
|
@ -130,7 +152,6 @@ class Maintenance extends BeanModel {
|
||||||
bean.strategy = obj.strategy;
|
bean.strategy = obj.strategy;
|
||||||
bean.interval_day = obj.intervalDay;
|
bean.interval_day = obj.intervalDay;
|
||||||
bean.timezone = obj.timezone;
|
bean.timezone = obj.timezone;
|
||||||
bean.duration = obj.duration;
|
|
||||||
bean.active = obj.active;
|
bean.active = obj.active;
|
||||||
|
|
||||||
if (obj.dateRange[0]) {
|
if (obj.dateRange[0]) {
|
||||||
|
@ -141,13 +162,18 @@ class Maintenance extends BeanModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
if (bean.strategy === "cron") {
|
||||||
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
bean.duration = obj.durationMinutes * 60;
|
||||||
|
bean.cron = obj.cron;
|
||||||
|
}
|
||||||
|
|
||||||
bean.weekdays = JSON.stringify(obj.weekdays);
|
if (bean.strategy.startsWith("recurring-")) {
|
||||||
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
||||||
|
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
||||||
await bean.generateCron();
|
bean.weekdays = JSON.stringify(obj.weekdays);
|
||||||
|
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
||||||
|
await bean.generateCron();
|
||||||
|
}
|
||||||
|
|
||||||
return bean;
|
return bean;
|
||||||
}
|
}
|
||||||
|
@ -155,8 +181,8 @@ class Maintenance extends BeanModel {
|
||||||
/**
|
/**
|
||||||
* Run the cron
|
* Run the cron
|
||||||
*/
|
*/
|
||||||
async run() {
|
async run(throwError = false) {
|
||||||
if (Maintenance.jobList[this.id]) {
|
if (this.beanMeta.job) {
|
||||||
log.debug("maintenance", "Maintenance is already running, stop it first. id: " + this.id);
|
log.debug("maintenance", "Maintenance is already running, stop it first. id: " + this.id);
|
||||||
this.stop();
|
this.stop();
|
||||||
}
|
}
|
||||||
|
@ -165,28 +191,106 @@ class Maintenance extends BeanModel {
|
||||||
|
|
||||||
// 1.21.2 migration
|
// 1.21.2 migration
|
||||||
if (!this.cron) {
|
if (!this.cron) {
|
||||||
//this.generateCron();
|
await this.generateCron();
|
||||||
//this.timezone = "UTC";
|
if (!this.timezone) {
|
||||||
// this.duration =
|
this.timezone = "UTC";
|
||||||
|
}
|
||||||
if (this.cron) {
|
if (this.cron) {
|
||||||
//await R.store(this);
|
await R.store(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.strategy === "single") {
|
if (this.strategy === "manual") {
|
||||||
Maintenance.jobList[this.id] = new Cron(this.start_date, { timezone: await this.getTimezone() }, () => {
|
// 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");
|
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
||||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||||
apicache.clear();
|
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() {
|
stop() {
|
||||||
if (Maintenance.jobList[this.id]) {
|
if (this.beanMeta.job) {
|
||||||
Maintenance.jobList[this.id].stop();
|
this.beanMeta.job.stop();
|
||||||
delete Maintenance.jobList[this.id];
|
delete this.beanMeta.job;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,21 +332,25 @@ class Maintenance extends BeanModel {
|
||||||
return "under-maintenance";
|
return "under-maintenance";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Maintenance.statusList[this.id]) {
|
if (!this.beanMeta.status) {
|
||||||
Maintenance.statusList[this.id] = "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
return Maintenance.statusList[this.id];
|
return this.beanMeta.status;
|
||||||
}
|
|
||||||
|
|
||||||
setStatus(status) {
|
|
||||||
Maintenance.statusList[this.id] = status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate Cron for recurring maintenance
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async generateCron() {
|
async generateCron() {
|
||||||
log.info("maintenance", "Generate cron for maintenance id: " + this.id);
|
log.info("maintenance", "Generate cron for maintenance id: " + this.id);
|
||||||
|
|
||||||
if (this.strategy === "recurring-interval") {
|
if (this.strategy === "cron") {
|
||||||
|
// Do nothing for cron
|
||||||
|
} else if (!this.strategy.startsWith("recurring-")) {
|
||||||
|
this.cron = "";
|
||||||
|
} else if (this.strategy === "recurring-interval") {
|
||||||
let array = this.start_time.split(":");
|
let array = this.start_time.split(":");
|
||||||
let hour = parseInt(array[0]);
|
let hour = parseInt(array[0]);
|
||||||
let minute = parseInt(array[1]);
|
let minute = parseInt(array[1]);
|
||||||
|
@ -250,6 +358,37 @@ class Maintenance extends BeanModel {
|
||||||
this.duration = this.calcDuration();
|
this.duration = this.calcDuration();
|
||||||
log.debug("maintenance", "Cron: " + this.cron);
|
log.debug("maintenance", "Cron: " + this.cron);
|
||||||
log.debug("maintenance", "Duration: " + this.duration);
|
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,156 +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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = MaintenanceTimeslot;
|
|
|
@ -23,7 +23,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||||
let maintenanceID = await R.store(bean);
|
let maintenanceID = await R.store(bean);
|
||||||
|
|
||||||
server.maintenanceList[maintenanceID] = bean;
|
server.maintenanceList[maintenanceID] = bean;
|
||||||
bean.run();
|
await bean.run(true);
|
||||||
|
|
||||||
await server.sendMaintenanceList(socket);
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||||
|
|
||||||
await Maintenance.jsonToBean(bean, maintenance);
|
await Maintenance.jsonToBean(bean, maintenance);
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
await bean.run();
|
await bean.run(true);
|
||||||
await server.sendMaintenanceList(socket);
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
|
@ -274,7 +274,6 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
maintenance.active = false;
|
maintenance.active = false;
|
||||||
maintenance.setStatus("inactive");
|
|
||||||
await R.store(maintenance);
|
await R.store(maintenance);
|
||||||
maintenance.stop();
|
maintenance.stop();
|
||||||
|
|
||||||
|
|
|
@ -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].startDate }}
|
<div class="timeslot">
|
||||||
<span class="to">-</span>
|
{{ startDateTime }}
|
||||||
{{ maintenance.timeslotList[0].endDate }}
|
<span class="to">-</span>
|
||||||
(UTC{{ maintenance.timezoneOffset }})
|
{{ endDateTime }}
|
||||||
|
</div>
|
||||||
|
<div class="timeslot">
|
||||||
|
UTC{{ maintenance.timezoneOffset }} {{ maintenance.timezone }}
|
||||||
|
</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;
|
||||||
|
|
|
@ -433,7 +433,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",
|
||||||
|
|
|
@ -95,7 +95,6 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
@ -103,6 +102,25 @@
|
||||||
<template v-if="maintenance.strategy === 'single'">
|
<template v-if="maintenance.strategy === 'single'">
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="maintenance.strategy === 'cron'">
|
||||||
|
<!-- Cron -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="cron" class="form-label">
|
||||||
|
{{ $t("cronExpression") }}
|
||||||
|
</label>
|
||||||
|
<p>Run: {{ cronDescription }}</p>
|
||||||
|
<input id="cron" v-model="maintenance.cron" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<!-- 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>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Recurring - Interval -->
|
<!-- Recurring - Interval -->
|
||||||
<template v-if="maintenance.strategy === 'recurring-interval'">
|
<template v-if="maintenance.strategy === 'recurring-interval'">
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
|
@ -205,26 +223,12 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="mb-2">{{ $t("startDateTime") }}</div>
|
<div class="mb-2">{{ $t("startDateTime") }}</div>
|
||||||
<Datepicker
|
<input v-model="maintenance.dateRange[0]" type="datetime-local" class="form-control">
|
||||||
v-model="maintenance.dateRange[0]"
|
|
||||||
:dark="$root.isDark"
|
|
||||||
datePicker
|
|
||||||
:monthChangeOnScroll="false"
|
|
||||||
format="yyyy-MM-dd HH:mm:ss"
|
|
||||||
modelType="yyyy-MM-dd HH:mm:ss"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="mb-2">{{ $t("endDateTime") }}</div>
|
<div class="mb-2">{{ $t("endDateTime") }}</div>
|
||||||
<Datepicker
|
<input v-model="maintenance.dateRange[1]" type="datetime-local" class="form-control">
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -247,12 +251,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 { timezoneList } from "../util-frontend";
|
||||||
|
import cronstrue from "cronstrue";
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
@ -279,18 +283,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: [
|
||||||
{
|
{
|
||||||
|
@ -334,6 +326,15 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
|
cronDescription() {
|
||||||
|
if (! this.maintenance.cron) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return cronstrue.toString(this.maintenance.cron, {
|
||||||
|
locale: "zh-TW",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
selectedStatusPagesOptions() {
|
selectedStatusPagesOptions() {
|
||||||
return Object.values(this.$root.statusPageList).map(statusPage => {
|
return Object.values(this.$root.statusPageList).map(statusPage => {
|
||||||
return {
|
return {
|
||||||
|
@ -393,6 +394,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: [{
|
||||||
|
|
Loading…
Reference in a new issue