This commit is contained in:
Louis Lam 2023-03-31 04:04:17 +08:00
parent 4ae437dd61
commit 02291730fe
12 changed files with 266 additions and 260 deletions

View File

@ -0,0 +1,9 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
-- 999 characters. https://stackoverflow.com/questions/46134830/maximum-length-for-cron-job
ALTER TABLE maintenance ADD cron TEXT;
ALTER TABLE maintenance ADD timezone VARCHAR(255);
ALTER TABLE maintenance ADD duration INTEGER;
COMMIT;

9
package-lock.json generated
View File

@ -26,6 +26,7 @@
"command-exists": "~1.2.9", "command-exists": "~1.2.9",
"compare-versions": "~3.6.0", "compare-versions": "~3.6.0",
"compression": "~1.7.4", "compression": "~1.7.4",
"croner": "^6.0.3",
"dayjs": "~1.11.5", "dayjs": "~1.11.5",
"dotenv": "~16.0.3", "dotenv": "~16.0.3",
"express": "~4.17.3", "express": "~4.17.3",
@ -7243,6 +7244,14 @@
"yup": "0.32.9" "yup": "0.32.9"
} }
}, },
"node_modules/croner": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/croner/-/croner-6.0.3.tgz",
"integrity": "sha512-Go+s9AaI+MeZUDJ6Kp7OYXCbM3svJ0qZ3IpkGoPetZLnP5wpX8MBTEiJOTYDFokP0Ph85GFZEUTBL9fo1e4DtQ==",
"engines": {
"node": ">=6.0"
}
},
"node_modules/cross-env": { "node_modules/cross-env": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",

View File

@ -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",

View File

@ -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,
}; };
/** /**

View File

@ -3,9 +3,15 @@ const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log }
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server"); const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const Cron = require("croner");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const apicache = require("../modules/apicache");
class Maintenance extends BeanModel { class Maintenance extends BeanModel {
static statusList = {};
static jobList = {};
/** /**
* Return an object that ready to parse to JSON for public * Return an object that ready to parse to JSON for public
* Only show necessary data to public * Only show necessary data to public
@ -15,16 +21,16 @@ class Maintenance extends BeanModel {
let dateRange = []; let dateRange = [];
if (this.start_date) { if (this.start_date) {
dateRange.push(utcToLocal(this.start_date)); dateRange.push(this.start_date);
if (this.end_date) { if (this.end_date) {
dateRange.push(utcToLocal(this.end_date)); dateRange.push(this.end_date);
} }
} }
let timeRange = []; let timeRange = [];
let startTime = timeObjectToLocal(parseTimeObject(this.start_time)); let startTime = parseTimeObject(this.start_time);
timeRange.push(startTime); timeRange.push(startTime);
let endTime = timeObjectToLocal(parseTimeObject(this.end_time)); let endTime = parseTimeObject(this.end_time);
timeRange.push(endTime); timeRange.push(endTime);
let obj = { let obj = {
@ -39,12 +45,18 @@ class Maintenance extends BeanModel {
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [], weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [], daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
timeslotList: [], timeslotList: [],
cron: this.cron,
duration: this.duration,
timezone: await this.getTimezone(),
timezoneOffset: await this.getTimezoneOffset(),
status: await this.getStatus(),
}; };
const timeslotList = await this.getTimeslotList(); if (this.strategy === "single") {
obj.timeslotList.push({
for (let timeslot of timeslotList) { startDate: this.start_date,
obj.timeslotList.push(await timeslot.toPublicJSON()); endDate: this.end_date,
});
} }
if (!Array.isArray(obj.weekdays)) { if (!Array.isArray(obj.weekdays)) {
@ -55,54 +67,9 @@ class Maintenance extends BeanModel {
obj.daysOfMonth = []; obj.daysOfMonth = [];
} }
// Maintenance Status
if (!obj.active) {
obj.status = "inactive";
} else if (obj.strategy === "manual") {
obj.status = "under-maintenance";
} else if (obj.timeslotList.length > 0) {
let currentTimestamp = dayjs().unix();
for (let timeslot of obj.timeslotList) {
if (dayjs.utc(timeslot.startDate).unix() <= currentTimestamp && dayjs.utc(timeslot.endDate).unix() >= currentTimestamp) {
log.debug("timeslot", "Timeslot ID: " + timeslot.id);
log.debug("timeslot", "currentTimestamp:" + currentTimestamp);
log.debug("timeslot", "timeslot.start_date:" + dayjs.utc(timeslot.startDate).unix());
log.debug("timeslot", "timeslot.end_date:" + dayjs.utc(timeslot.endDate).unix());
obj.status = "under-maintenance";
break;
}
}
if (!obj.status) {
obj.status = "scheduled";
}
} else if (obj.timeslotList.length === 0) {
obj.status = "ended";
} else {
obj.status = "unknown";
}
return obj; return obj;
} }
/**
* Only get future or current timeslots only
* @returns {Promise<[]>}
*/
async getTimeslotList() {
return R.convertToBeans("maintenance_timeslot", await R.getAll(`
SELECT maintenance_timeslot.*
FROM maintenance_timeslot, maintenance
WHERE maintenance_timeslot.maintenance_id = maintenance.id
AND maintenance.id = ?
AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
`, [
this.id
]));
}
/** /**
* Return an object that ready to parse to JSON * Return an object that ready to parse to JSON
* @param {string} timezone If not specified, the timeRange will be in UTC * @param {string} timezone If not specified, the timeRange will be in UTC
@ -135,26 +102,10 @@ class Maintenance extends BeanModel {
} }
/** /**
* Get the start date and time for maintenance * Get the duration of maintenance in seconds
* @returns {dayjs.Dayjs} Start date and time
*/
getStartDateTime() {
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
// Start Time
let startTimeSecond = dayjs.utc(this.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
log.debug("timeslot", "startTime: " + startTimeSecond);
// Bake StartDate + StartTime = Start DateTime
return dayjs.utc(this.start_date).add(startTimeSecond, "second");
}
/**
* Get the duraction of maintenance in seconds
* @returns {number} Duration of maintenance * @returns {number} Duration of maintenance
*/ */
getDuration() { calcDuration() {
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second"); let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
// Add 24hours if it is across day // Add 24hours if it is across day
if (duration < 0) { if (duration < 0) {
@ -169,30 +120,24 @@ class Maintenance extends BeanModel {
* @param {Object} obj Data to fill bean with * @param {Object} obj Data to fill bean with
* @returns {Bean} Filled bean * @returns {Bean} Filled bean
*/ */
static jsonToBean(bean, obj) { static async jsonToBean(bean, obj) {
if (obj.id) { if (obj.id) {
bean.id = obj.id; bean.id = obj.id;
} }
// Apply timezone offset to timeRange, as it cannot apply automatically.
if (obj.timeRange[0]) {
timeObjectToUTC(obj.timeRange[0]);
if (obj.timeRange[1]) {
timeObjectToUTC(obj.timeRange[1]);
}
}
bean.title = obj.title; bean.title = obj.title;
bean.description = obj.description; bean.description = obj.description;
bean.strategy = obj.strategy; bean.strategy = obj.strategy;
bean.interval_day = obj.intervalDay; bean.interval_day = obj.intervalDay;
bean.timezone = obj.timezone;
bean.duration = obj.duration;
bean.active = obj.active; bean.active = obj.active;
if (obj.dateRange[0]) { if (obj.dateRange[0]) {
bean.start_date = localToUTC(obj.dateRange[0]); bean.start_date = obj.dateRange[0];
if (obj.dateRange[1]) { if (obj.dateRange[1]) {
bean.end_date = localToUTC(obj.dateRange[1]); bean.end_date = obj.dateRange[1];
} }
} }
@ -202,38 +147,111 @@ class Maintenance extends BeanModel {
bean.weekdays = JSON.stringify(obj.weekdays); bean.weekdays = JSON.stringify(obj.weekdays);
bean.days_of_month = JSON.stringify(obj.daysOfMonth); bean.days_of_month = JSON.stringify(obj.daysOfMonth);
await bean.generateCron();
return bean; return bean;
} }
/** /**
* SQL conditions for active maintenance * Run the cron
* @returns {string}
*/ */
static getActiveMaintenanceSQLCondition() { async run() {
return ` if (Maintenance.jobList[this.id]) {
( log.debug("maintenance", "Maintenance is already running, stop it first. id: " + this.id);
(maintenance_timeslot.start_date <= DATETIME('now') this.stop();
AND maintenance_timeslot.end_date >= DATETIME('now') }
AND maintenance.active = 1)
OR log.debug("maintenance", "Run maintenance id: " + this.id);
(maintenance.strategy = 'manual' AND active = 1)
) // 1.21.2 migration
`; if (!this.cron) {
//this.generateCron();
//this.timezone = "UTC";
// this.duration =
if (this.cron) {
//await R.store(this);
}
}
if (this.strategy === "single") {
Maintenance.jobList[this.id] = new Cron(this.start_date, { timezone: await this.getTimezone() }, () => {
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
apicache.clear();
});
}
}
stop() {
if (Maintenance.jobList[this.id]) {
Maintenance.jobList[this.id].stop();
delete Maintenance.jobList[this.id];
}
}
async isUnderMaintenance() {
return (await this.getStatus()) === "under-maintenance";
}
async getTimezone() {
if (!this.timezone) {
return await UptimeKumaServer.getInstance().getTimezone();
}
return this.timezone;
}
async getTimezoneOffset() {
return dayjs.tz(dayjs(), await this.getTimezone()).format("Z");
}
async getStatus() {
if (!this.active) {
return "inactive";
}
if (this.strategy === "manual") {
return "under-maintenance";
}
// Check if the maintenance is started
if (this.start_date && dayjs().isBefore(dayjs.tz(this.start_date, await this.getTimezone()))) {
return "scheduled";
}
// Check if the maintenance is ended
if (this.end_date && dayjs().isAfter(dayjs.tz(this.end_date, await this.getTimezone()))) {
return "ended";
}
if (this.strategy === "single") {
return "under-maintenance";
}
if (!Maintenance.statusList[this.id]) {
Maintenance.statusList[this.id] = "unknown";
}
return Maintenance.statusList[this.id];
}
setStatus(status) {
Maintenance.statusList[this.id] = status;
}
async generateCron() {
log.info("maintenance", "Generate cron for maintenance id: " + this.id);
if (this.strategy === "recurring-interval") {
let array = this.start_time.split(":");
let hour = parseInt(array[0]);
let minute = parseInt(array[1]);
this.cron = minute + " " + hour + " */" + this.interval_day + " * *";
this.duration = this.calcDuration();
log.debug("maintenance", "Cron: " + this.cron);
log.debug("maintenance", "Duration: " + this.duration);
} }
/**
* SQL conditions for active and future maintenance
* @returns {string}
*/
static getActiveAndFutureMaintenanceSQLCondition() {
return `
(
((maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1))
)
`;
} }
} }

View File

@ -151,73 +151,6 @@ class MaintenanceTimeslot extends BeanModel {
} }
} }
static async isDuplicateTimeslot(timeslot) {
let bean = await R.findOne("maintenance_timeslot", "maintenance_id = ? AND start_date = ? AND end_date = ?", [
timeslot.maintenance_id,
timeslot.start_date,
timeslot.end_date
]);
return bean !== null;
}
/**
* Generate a next timeslot for all recurring types
* @param maintenance
* @param minDate
* @param {function} nextDayCallback The logic how to get the next possible day
* @param {function} isValidCallback Check the day whether is matched the current strategy
* @returns {Promise<null|MaintenanceTimeslot>}
*/
static async handleRecurringType(maintenance, minDate, nextDayCallback, isValidCallback) {
let bean = R.dispense("maintenance_timeslot");
let duration = maintenance.getDuration();
let startDateTime = maintenance.getStartDateTime();
let endDateTime;
// Keep generating from the first possible date, until it is ok
while (true) {
//log.debug("timeslot", "startDateTime: " + startDateTime.format());
// Handling out of effective date range
if (startDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
log.debug("timeslot", "Out of effective date range");
return null;
}
endDateTime = startDateTime.add(duration, "second");
// If endDateTime is out of effective date range, use the end datetime from effective date range
if (endDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
endDateTime = dayjs.utc(maintenance.end_date);
}
// If minDate is set, the endDateTime must be bigger than it.
// And the endDateTime must be bigger current time
// Is valid under current recurring strategy
if (
(!minDate || endDateTime.diff(minDate) > 0) &&
endDateTime.diff(dayjs()) > 0 &&
isValidCallback(startDateTime)
) {
break;
}
startDateTime = nextDayCallback(startDateTime);
}
bean.maintenance_id = maintenance.id;
bean.start_date = localToUTC(startDateTime);
bean.end_date = localToUTC(endDateTime);
bean.generated_next = false;
if (!await this.isDuplicateTimeslot(bean)) {
await R.store(bean);
return bean;
} else {
log.debug("maintenance", "Duplicate timeslot, skip");
return null;
}
}
} }
module.exports = MaintenanceTimeslot; module.exports = MaintenanceTimeslot;

View File

@ -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 */

View File

@ -5,7 +5,6 @@ const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server"); const { UptimeKumaServer } = require("../uptime-kuma-server");
const Maintenance = require("../model/maintenance"); const Maintenance = require("../model/maintenance");
const server = UptimeKumaServer.getInstance(); const server = UptimeKumaServer.getInstance();
const MaintenanceTimeslot = require("../model/maintenance_timeslot");
/** /**
* Handlers for Maintenance * Handlers for Maintenance
@ -19,10 +18,12 @@ module.exports.maintenanceSocketHandler = (socket) => {
log.debug("maintenance", maintenance); log.debug("maintenance", maintenance);
let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance); let bean = await Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
bean.user_id = socket.userID; bean.user_id = socket.userID;
let maintenanceID = await R.store(bean); let maintenanceID = await R.store(bean);
await MaintenanceTimeslot.generateTimeslot(bean);
server.maintenanceList[maintenanceID] = bean;
bean.run();
await server.sendMaintenanceList(socket); await server.sendMaintenanceList(socket);
@ -45,17 +46,15 @@ module.exports.maintenanceSocketHandler = (socket) => {
try { try {
checkLogin(socket); checkLogin(socket);
let bean = await R.findOne("maintenance", " id = ? ", [ maintenance.id ]); let bean = server.getMaintenance(maintenance.id);
if (bean.user_id !== socket.userID) { if (bean.user_id !== socket.userID) {
throw new Error("Permission denied."); throw new Error("Permission denied.");
} }
Maintenance.jsonToBean(bean, maintenance); await Maintenance.jsonToBean(bean, maintenance);
await R.store(bean); await R.store(bean);
await MaintenanceTimeslot.generateTimeslot(bean, null, true); await bean.run();
await server.sendMaintenanceList(socket); await server.sendMaintenanceList(socket);
callback({ callback({
@ -236,6 +235,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`); log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
if (maintenanceID in server.maintenanceList) { if (maintenanceID in server.maintenanceList) {
server.maintenanceList[maintenanceID].stop();
delete server.maintenanceList[maintenanceID]; delete server.maintenanceList[maintenanceID];
} }
@ -267,9 +267,16 @@ module.exports.maintenanceSocketHandler = (socket) => {
log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`); log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
await R.exec("UPDATE maintenance SET active = 0 WHERE id = ? ", [ let maintenance = server.getMaintenance(maintenanceID);
maintenanceID,
]); if (!maintenance) {
throw new Error("Maintenance not found");
}
maintenance.active = false;
maintenance.setStatus("inactive");
await R.store(maintenance);
maintenance.stop();
apicache.clear(); apicache.clear();
@ -294,9 +301,15 @@ module.exports.maintenanceSocketHandler = (socket) => {
log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`); log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
await R.exec("UPDATE maintenance SET active = 1 WHERE id = ? ", [ let maintenance = server.getMaintenance(maintenanceID);
maintenanceID,
]); if (!maintenance) {
throw new Error("Maintenance not found");
}
maintenance.active = true;
await R.store(maintenance);
await maintenance.run();
apicache.clear(); apicache.clear();

View File

@ -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");

View File

@ -4,10 +4,10 @@
{{ $t("Manual") }} {{ $t("Manual") }}
</div> </div>
<div v-else-if="maintenance.timeslotList.length > 0" class="timeslot"> <div v-else-if="maintenance.timeslotList.length > 0" class="timeslot">
{{ maintenance.timeslotList[0].startDateServerTimezone }} {{ maintenance.timeslotList[0].startDate }}
<span class="to">-</span> <span class="to">-</span>
{{ maintenance.timeslotList[0].endDateServerTimezone }} {{ maintenance.timeslotList[0].endDate }}
(UTC{{ maintenance.timeslotList[0].serverTimezoneOffset }}) (UTC{{ maintenance.timezoneOffset }})
</div> </div>
</div> </div>
</template> </template>

View File

@ -394,6 +394,10 @@
"backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.", "backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.",
"Optional": "Optional", "Optional": "Optional",
"or": "or", "or": "or",
"sameAsServerTimezone": "Same as Server Timezone",
"startDateTime": "Start Date/Time",
"endDateTime": "End Date/Time",
"cronExpression": "Cron Expression",
"recurringInterval": "Interval", "recurringInterval": "Interval",
"Recurring": "Recurring", "Recurring": "Recurring",
"strategyManual": "Active/Inactive Manually", "strategyManual": "Active/Inactive Manually",

View File

@ -85,14 +85,13 @@
<h2 class="mt-5">{{ $t("Date and Time") }}</h2> <h2 class="mt-5">{{ $t("Date and Time") }}</h2>
<div> {{ $t("warningTimezone") }}: <mark>{{ $root.info.serverTimezone }} ({{ $root.info.serverTimezoneOffset }})</mark></div>
<!-- Strategy --> <!-- Strategy -->
<div class="my-3"> <div class="my-3">
<label for="strategy" class="form-label">{{ $t("Strategy") }}</label> <label for="strategy" class="form-label">{{ $t("Strategy") }}</label>
<select id="strategy" v-model="maintenance.strategy" class="form-select"> <select id="strategy" v-model="maintenance.strategy" class="form-select">
<option value="manual">{{ $t("strategyManual") }}</option> <option value="manual">{{ $t("strategyManual") }}</option>
<option value="single">{{ $t("Single Maintenance Window") }}</option> <option value="single">{{ $t("Single Maintenance Window") }}</option>
<option value="cron">{{ $t("cronExpression") }}</option>
<option value="recurring-interval">{{ $t("Recurring") }} - {{ $t("recurringInterval") }}</option> <option value="recurring-interval">{{ $t("Recurring") }} - {{ $t("recurringInterval") }}</option>
<option value="recurring-weekday">{{ $t("Recurring") }} - {{ $t("dayOfWeek") }}</option> <option value="recurring-weekday">{{ $t("Recurring") }} - {{ $t("dayOfWeek") }}</option>
<option value="recurring-day-of-month">{{ $t("Recurring") }} - {{ $t("dayOfMonth") }}</option> <option value="recurring-day-of-month">{{ $t("Recurring") }} - {{ $t("dayOfMonth") }}</option>
@ -102,19 +101,6 @@
<!-- Single Maintenance Window --> <!-- Single Maintenance Window -->
<template v-if="maintenance.strategy === 'single'"> <template v-if="maintenance.strategy === 'single'">
<!-- DateTime Range -->
<div class="my-3">
<label class="form-label">{{ $t("DateTime Range") }}</label>
<Datepicker
v-model="maintenance.dateRange"
:dark="$root.isDark"
range
:monthChangeOnScroll="false"
:minDate="minDate"
format="yyyy-MM-dd HH:mm"
modelType="yyyy-MM-dd HH:mm:ss"
/>
</div>
</template> </template>
<!-- Recurring - Interval --> <!-- Recurring - Interval -->
@ -180,7 +166,6 @@
</div> </div>
</template> </template>
<!-- For any recurring types -->
<template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month'"> <template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month'">
<!-- Maintenance Time Window of a Day --> <!-- Maintenance Time Window of a Day -->
<div class="my-3"> <div class="my-3">
@ -192,21 +177,57 @@
disableTimeRangeValidation range disableTimeRangeValidation range
/> />
</div> </div>
</template>
<template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month' || maintenance.strategy === 'cron' || maintenance.strategy === 'single'">
<!-- Timezone -->
<div class="mb-4">
<label for="timezone" class="form-label">
{{ $t("Timezone") }}
</label>
<select id="timezone" v-model="maintenance.timezone" class="form-select">
<option :value="null">{{ $t("sameAsServerTimezone") }}</option>
<option value="UTC">UTC</option>
<option
v-for="(timezone, index) in timezoneList"
:key="index"
:value="timezone.value"
>
{{ timezone.name }}
</option>
</select>
</div>
<!-- Date Range --> <!-- Date Range -->
<div class="my-3"> <div class="my-3">
<label class="form-label">{{ $t("Effective Date Range") }}</label> <label class="form-label">{{ $t("Effective Date Range") }}</label>
<div class="row">
<div class="col">
<div class="mb-2">{{ $t("startDateTime") }}</div>
<Datepicker <Datepicker
v-model="maintenance.dateRange" v-model="maintenance.dateRange[0]"
:dark="$root.isDark" :dark="$root.isDark"
range datePicker datePicker
:monthChangeOnScroll="false" :monthChangeOnScroll="false"
:minDate="minDate"
format="yyyy-MM-dd HH:mm:ss" format="yyyy-MM-dd HH:mm:ss"
modelType="yyyy-MM-dd HH:mm:ss" modelType="yyyy-MM-dd HH:mm:ss"
required
/> />
</div> </div>
<div class="col">
<div class="mb-2">{{ $t("endDateTime") }}</div>
<Datepicker
v-model="maintenance.dateRange[1]"
:dark="$root.isDark"
datePicker
:monthChangeOnScroll="false"
format="yyyy-MM-dd HH:mm:ss"
modelType="yyyy-MM-dd HH:mm:ss"
/>
</div>
</div>
</div>
</template> </template>
<div class="mt-4 mb-1"> <div class="mt-4 mb-1">
@ -231,6 +252,7 @@ import { useToast } from "vue-toastification";
import VueMultiselect from "vue-multiselect"; import VueMultiselect from "vue-multiselect";
import dayjs from "dayjs"; import dayjs from "dayjs";
import Datepicker from "@vuepic/vue-datepicker"; import Datepicker from "@vuepic/vue-datepicker";
import { timezoneList } from "../util-frontend";
const toast = useToast(); const toast = useToast();
@ -242,6 +264,7 @@ export default {
data() { data() {
return { return {
timezoneList: timezoneList(),
processing: false, processing: false,
maintenance: {}, maintenance: {},
affectedMonitors: [], affectedMonitors: [],
@ -381,6 +404,7 @@ export default {
}], }],
weekdays: [], weekdays: [],
daysOfMonth: [], daysOfMonth: [],
timezone: null,
}; };
} else if (this.isEdit) { } else if (this.isEdit) {
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => { this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {