Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>pull/2151/head
commit
2052fa175f
@ -0,0 +1,25 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_url VARCHAR(255) default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_protobuf TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_body TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_metadata TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_method VARCHAR(255) default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_service_name VARCHAR(255) default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_enable_tls BOOLEAN default 0 not null;
|
||||||
|
|
||||||
|
COMMIT;
|
@ -0,0 +1,83 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Just for someone who tested maintenance before (patch-maintenance-table.sql)
|
||||||
|
DROP TABLE IF EXISTS maintenance_status_page;
|
||||||
|
DROP TABLE IF EXISTS monitor_maintenance;
|
||||||
|
DROP TABLE IF EXISTS maintenance;
|
||||||
|
DROP TABLE IF EXISTS maintenance_timeslot;
|
||||||
|
|
||||||
|
-- maintenance
|
||||||
|
CREATE TABLE [maintenance] (
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[title] VARCHAR(150) NOT NULL,
|
||||||
|
[description] TEXT NOT NULL,
|
||||||
|
[user_id] INTEGER REFERENCES [user]([id]) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
[active] BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
[strategy] VARCHAR(50) NOT NULL DEFAULT 'single',
|
||||||
|
[start_date] DATETIME,
|
||||||
|
[end_date] DATETIME,
|
||||||
|
[start_time] TIME,
|
||||||
|
[end_time] TIME,
|
||||||
|
[weekdays] VARCHAR2(250) DEFAULT '[]',
|
||||||
|
[days_of_month] TEXT DEFAULT '[]',
|
||||||
|
[interval_day] INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [manual_active] ON [maintenance] (
|
||||||
|
[strategy],
|
||||||
|
[active]
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [active] ON [maintenance] ([active]);
|
||||||
|
|
||||||
|
CREATE INDEX [maintenance_user_id] ON [maintenance] ([user_id]);
|
||||||
|
|
||||||
|
-- maintenance_status_page
|
||||||
|
CREATE TABLE maintenance_status_page (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
status_page_id INTEGER NOT NULL,
|
||||||
|
maintenance_id INTEGER NOT NULL,
|
||||||
|
CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT FK_status_page FOREIGN KEY (status_page_id) REFERENCES status_page (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [status_page_id_index]
|
||||||
|
ON [maintenance_status_page]([status_page_id]);
|
||||||
|
|
||||||
|
CREATE INDEX [maintenance_id_index]
|
||||||
|
ON [maintenance_status_page]([maintenance_id]);
|
||||||
|
|
||||||
|
-- maintenance_timeslot
|
||||||
|
CREATE TABLE [maintenance_timeslot] (
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[maintenance_id] INTEGER NOT NULL CONSTRAINT [FK_maintenance] REFERENCES [maintenance]([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
[start_date] DATETIME NOT NULL,
|
||||||
|
[end_date] DATETIME,
|
||||||
|
[generated_next] BOOLEAN DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [maintenance_id] ON [maintenance_timeslot] ([maintenance_id] DESC);
|
||||||
|
|
||||||
|
CREATE INDEX [active_timeslot_index] ON [maintenance_timeslot] (
|
||||||
|
[maintenance_id] DESC,
|
||||||
|
[start_date] DESC,
|
||||||
|
[end_date] DESC
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [generated_next_index] ON [maintenance_timeslot] ([generated_next]);
|
||||||
|
|
||||||
|
-- monitor_maintenance
|
||||||
|
CREATE TABLE monitor_maintenance (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
monitor_id INTEGER NOT NULL,
|
||||||
|
maintenance_id INTEGER NOT NULL,
|
||||||
|
CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [maintenance_id_index2] ON [monitor_maintenance]([maintenance_id]);
|
||||||
|
|
||||||
|
CREATE INDEX [monitor_id_index] ON [monitor_maintenance]([monitor_id]);
|
||||||
|
|
||||||
|
COMMIT;
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,215 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
|
||||||
|
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
|
||||||
|
class Maintenance extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toPublicJSON() {
|
||||||
|
|
||||||
|
let dateRange = [];
|
||||||
|
if (this.start_date) {
|
||||||
|
dateRange.push(utcToLocal(this.start_date));
|
||||||
|
if (this.end_date) {
|
||||||
|
dateRange.push(utcToLocal(this.end_date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeRange = [];
|
||||||
|
let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
|
||||||
|
timeRange.push(startTime);
|
||||||
|
let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
|
||||||
|
timeRange.push(endTime);
|
||||||
|
|
||||||
|
let obj = {
|
||||||
|
id: this.id,
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
strategy: this.strategy,
|
||||||
|
intervalDay: this.interval_day,
|
||||||
|
active: !!this.active,
|
||||||
|
dateRange: dateRange,
|
||||||
|
timeRange: timeRange,
|
||||||
|
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
|
||||||
|
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
|
||||||
|
timeslotList: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeslotList = await this.getTimeslotList();
|
||||||
|
|
||||||
|
for (let timeslot of timeslotList) {
|
||||||
|
obj.timeslotList.push(await timeslot.toPublicJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(obj.weekdays)) {
|
||||||
|
obj.weekdays = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only get future or current timeslots only
|
||||||
|
* @returns {Promise<[]>}
|
||||||
|
*/
|
||||||
|
async getTimeslotList() {
|
||||||
|
return R.convertToBeans("maintenance_timeslot", await R.getAll(`
|
||||||
|
SELECT maintenance_timeslot.*
|
||||||
|
FROM maintenance_timeslot, maintenance
|
||||||
|
WHERE maintenance_timeslot.maintenance_id = maintenance.id
|
||||||
|
AND maintenance.id = ?
|
||||||
|
AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
|
||||||
|
`, [
|
||||||
|
this.id
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @param {string} timezone If not specified, the timeRange will be in UTC
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toJSON(timezone = null) {
|
||||||
|
return this.toPublicJSON(timezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDayOfWeekList() {
|
||||||
|
log.debug("timeslot", "List: " + this.weekdays);
|
||||||
|
return JSON.parse(this.weekdays).sort(function (a, b) {
|
||||||
|
return a - b;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getDayOfMonthList() {
|
||||||
|
return JSON.parse(this.days_of_month).sort(function (a, b) {
|
||||||
|
return a - b;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
getDuration() {
|
||||||
|
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
|
||||||
|
// Add 24hours if it is across day
|
||||||
|
if (duration < 0) {
|
||||||
|
duration += 24 * 3600;
|
||||||
|
}
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
static jsonToBean(bean, obj) {
|
||||||
|
if (obj.id) {
|
||||||
|
bean.id = obj.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply timezone offset to timeRange, as it cannot apply automatically.
|
||||||
|
if (obj.timeRange[0]) {
|
||||||
|
timeObjectToUTC(obj.timeRange[0]);
|
||||||
|
if (obj.timeRange[1]) {
|
||||||
|
timeObjectToUTC(obj.timeRange[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.title = obj.title;
|
||||||
|
bean.description = obj.description;
|
||||||
|
bean.strategy = obj.strategy;
|
||||||
|
bean.interval_day = obj.intervalDay;
|
||||||
|
bean.active = obj.active;
|
||||||
|
|
||||||
|
if (obj.dateRange[0]) {
|
||||||
|
bean.start_date = localToUTC(obj.dateRange[0]);
|
||||||
|
|
||||||
|
if (obj.dateRange[1]) {
|
||||||
|
bean.end_date = localToUTC(obj.dateRange[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
||||||
|
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
||||||
|
|
||||||
|
bean.weekdays = JSON.stringify(obj.weekdays);
|
||||||
|
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
||||||
|
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL conditions for active maintenance
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static getActiveMaintenanceSQLCondition() {
|
||||||
|
return `
|
||||||
|
|
||||||
|
(maintenance_timeslot.start_date <= DATETIME('now')
|
||||||
|
AND maintenance_timeslot.end_date >= DATETIME('now')
|
||||||
|
AND maintenance.active = 1)
|
||||||
|
OR
|
||||||
|
(maintenance.strategy = 'manual' AND active = 1)
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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))
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Maintenance;
|
@ -0,0 +1,189 @@
|
|||||||
|
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 {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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;
|
||||||
|
return await R.store(bean);
|
||||||
|
|
||||||
|
} 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
return await R.store(bean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MaintenanceTimeslot;
|
@ -0,0 +1,71 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class SMSEagle extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "SMSEagle";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let postData;
|
||||||
|
let sendMethod;
|
||||||
|
let recipientType;
|
||||||
|
|
||||||
|
let encoding = (notification.smseagleEncoding) ? "1" : "0";
|
||||||
|
let priority = (notification.smseaglePriority) ? notification.smseaglePriority : "0";
|
||||||
|
|
||||||
|
if (notification.smseagleRecipientType === "smseagle-contact") {
|
||||||
|
recipientType = "contactname";
|
||||||
|
sendMethod = "sms.send_tocontact";
|
||||||
|
}
|
||||||
|
if (notification.smseagleRecipientType === "smseagle-group") {
|
||||||
|
recipientType = "groupname";
|
||||||
|
sendMethod = "sms.send_togroup";
|
||||||
|
}
|
||||||
|
if (notification.smseagleRecipientType === "smseagle-to") {
|
||||||
|
recipientType = "to";
|
||||||
|
sendMethod = "sms.send_sms";
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = {
|
||||||
|
access_token: notification.smseagleToken,
|
||||||
|
[recipientType]: notification.smseagleRecipient,
|
||||||
|
message: msg,
|
||||||
|
responsetype: "extended",
|
||||||
|
unicode: encoding,
|
||||||
|
highpriority: priority
|
||||||
|
};
|
||||||
|
|
||||||
|
postData = {
|
||||||
|
method: sendMethod,
|
||||||
|
params: params
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = await axios.post(notification.smseagleUrl + "/jsonrpc/sms", postData, config);
|
||||||
|
|
||||||
|
if ((JSON.stringify(resp.data)).indexOf("message_id") === -1) {
|
||||||
|
let error = "";
|
||||||
|
if (resp.data.result && resp.data.result.error_text) {
|
||||||
|
error = `SMSEagle API returned error: ${JSON.stringify(resp.data.result.error_text)}`;
|
||||||
|
} else {
|
||||||
|
error = "SMSEagle API returned an unexpected response";
|
||||||
|
}
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SMSEagle;
|
@ -0,0 +1,311 @@
|
|||||||
|
const { checkLogin } = require("../util-server");
|
||||||
|
const { log } = require("../../src/util");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const apicache = require("../modules/apicache");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
const Maintenance = require("../model/maintenance");
|
||||||
|
const server = UptimeKumaServer.getInstance();
|
||||||
|
const MaintenanceTimeslot = require("../model/maintenance_timeslot");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handlers for Maintenance
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
*/
|
||||||
|
module.exports.maintenanceSocketHandler = (socket) => {
|
||||||
|
// Add a new maintenance
|
||||||
|
socket.on("addMaintenance", async (maintenance, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", maintenance);
|
||||||
|
|
||||||
|
let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
|
||||||
|
bean.user_id = socket.userID;
|
||||||
|
let maintenanceID = await R.store(bean);
|
||||||
|
await MaintenanceTimeslot.generateTimeslot(bean);
|
||||||
|
|
||||||
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Added Successfully.",
|
||||||
|
maintenanceID,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit a maintenance
|
||||||
|
socket.on("editMaintenance", async (maintenance, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let bean = await R.findOne("maintenance", " id = ? ", [ maintenance.id ]);
|
||||||
|
|
||||||
|
if (bean.user_id !== socket.userID) {
|
||||||
|
throw new Error("Permission denied.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Maintenance.jsonToBean(bean, maintenance);
|
||||||
|
|
||||||
|
await R.store(bean);
|
||||||
|
await MaintenanceTimeslot.generateTimeslot(bean, null, true);
|
||||||
|
|
||||||
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Saved.",
|
||||||
|
maintenanceID: bean.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a new monitor_maintenance
|
||||||
|
socket.on("addMonitorMaintenance", async (maintenanceID, monitors, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM monitor_maintenance WHERE maintenance_id = ?", [
|
||||||
|
maintenanceID
|
||||||
|
]);
|
||||||
|
|
||||||
|
for await (const monitor of monitors) {
|
||||||
|
let bean = R.dispense("monitor_maintenance");
|
||||||
|
|
||||||
|
bean.import({
|
||||||
|
monitor_id: monitor.id,
|
||||||
|
maintenance_id: maintenanceID
|
||||||
|
});
|
||||||
|
await R.store(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
apicache.clear();
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Added Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a new monitor_maintenance
|
||||||
|
socket.on("addMaintenanceStatusPage", async (maintenanceID, statusPages, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM maintenance_status_page WHERE maintenance_id = ?", [
|
||||||
|
maintenanceID
|
||||||
|
]);
|
||||||
|
|
||||||
|
for await (const statusPage of statusPages) {
|
||||||
|
let bean = R.dispense("maintenance_status_page");
|
||||||
|
|
||||||
|
bean.import({
|
||||||
|
status_page_id: statusPage.id,
|
||||||
|
maintenance_id: maintenanceID
|
||||||
|
});
|
||||||
|
await R.store(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
apicache.clear();
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Added Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("getMaintenance", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", `Get Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
let bean = await R.findOne("maintenance", " id = ? AND user_id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
maintenance: await bean.toJSON(),
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("getMaintenanceList", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
await server.sendMaintenanceList(socket);
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("getMonitorMaintenance", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", `Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
let monitors = await R.getAll("SELECT monitor.id, monitor.name FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
monitors,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("getMaintenanceStatusPage", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", `Get Status Pages for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
let statusPages = await R.getAll("SELECT status_page.id, status_page.title FROM maintenance_status_page msp JOIN status_page ON msp.status_page_id = status_page.id WHERE msp.maintenance_id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
statusPages,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("deleteMaintenance", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
if (maintenanceID in server.maintenanceList) {
|
||||||
|
delete server.maintenanceList[maintenanceID];
|
||||||
|
}
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM maintenance WHERE id = ? AND user_id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Deleted Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("pauseMaintenance", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
await R.exec("UPDATE maintenance SET active = 0 WHERE id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Paused Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("resumeMaintenance", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
await R.exec("UPDATE maintenance SET active = 1 WHERE id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Resume Successfully",
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,39 @@
|
|||||||
|
@import "@vuepic/vue-datepicker/dist/main.css";
|
||||||
|
@import "vars.scss";
|
||||||
|
|
||||||
|
// Must use #{ }
|
||||||
|
// Remark: https://stackoverflow.com/questions/50202991/unable-to-set-scss-variable-to-css-variable
|
||||||
|
.dp__theme_dark {
|
||||||
|
--dp-background-color: #{$dark-bg2};
|
||||||
|
--dp-text-color: #{$dark-font-color};
|
||||||
|
--dp-hover-color: #484848;
|
||||||
|
--dp-hover-text-color: #ffffff;
|
||||||
|
--dp-hover-icon-color: #959595;
|
||||||
|
--dp-primary-color: #{#5cdd8b};
|
||||||
|
--dp-primary-text-color: #ffffff;
|
||||||
|
--dp-secondary-color: #494949;
|
||||||
|
--dp-border-color: #{$dark-border-color};
|
||||||
|
--dp-menu-border-color: #2d2d2d;
|
||||||
|
--dp-border-color-hover: #{$dark-border-color};
|
||||||
|
--dp-disabled-color: #212121;
|
||||||
|
--dp-scroll-bar-background: #212121;
|
||||||
|
--dp-scroll-bar-color: #484848;
|
||||||
|
--dp-success-color: #{$primary};
|
||||||
|
--dp-success-color-disabled: #428f59;
|
||||||
|
--dp-icon-color: #959595;
|
||||||
|
--dp-danger-color: #e53935;
|
||||||
|
--dp-highlight-color: rgba(0, 92, 178, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__input {
|
||||||
|
border-radius: $border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix: Full width of text input when using "inline textInput inlineWithInput" mode
|
||||||
|
.dp__main > div[aria-label="Datepicker input"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__main > div[aria-label="Datepicker menu"]:nth-child(2) {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="maintenance.strategy === 'manual'" class="timeslot">
|
||||||
|
{{ $t("Manual") }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="maintenance.timeslotList.length > 0" class="timeslot">
|
||||||
|
{{ maintenance.timeslotList[0].startDateServerTimezone }}
|
||||||
|
<span class="to">-</span>
|
||||||
|
{{ maintenance.timeslotList[0].endDateServerTimezone }}
|
||||||
|
(UTC{{ maintenance.timeslotList[0].serverTimezoneOffset }})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
maintenance: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.timeslot {
|
||||||
|
margin-top: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
.to {
|
||||||
|
margin: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smseagle-url" class="form-label">{{ $t("smseagleUrl") }}</label>
|
||||||
|
<input id="smseagle-url" v-model="$parent.notification.smseagleUrl" type="text" minlength="7" class="form-control" placeholder="http://127.0.0.1" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smseagle-token" class="form-label">{{ $t("smseagleToken") }}</label>
|
||||||
|
<HiddenInput id="smseagle-token" v-model="$parent.notification.smseagleToken" :required="true"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smseagle-recipient-type" class="form-label">{{ $t("smseagleRecipientType") }}</label>
|
||||||
|
<select id="smseagle-recipient-type" v-model="$parent.notification.smseagleRecipientType" class="form-select">
|
||||||
|
<option value="smseagle-to" selected>{{ $t("smseagleTo") }}</option>
|
||||||
|
<option value="smseagle-group">{{ $t("smseagleGroup") }}</option>
|
||||||
|
<option value="smseagle-contact">{{ $t("smseagleContact") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smseagle-recipient" class="form-label">{{ $t("smseagleRecipient") }}</label>
|
||||||
|
<input id="smseagle-recipient" v-model="$parent.notification.smseagleRecipient" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smseagle-priority" class="form-label">{{ $t("smseaglePriority") }}</label>
|
||||||
|
<input id="smseagle-priority" v-model="$parent.notification.smseaglePriority" type="number" class="form-control" min="0" max="9" step="1" placeholder="0">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check form-switch">
|
||||||
|
<label for="smseagle-encoding" class="form-label">{{ $t("smseagleEncoding") }}</label>
|
||||||
|
<input id="smseagle-encoding" v-model="$parent.notification.smseagleEncoding" type="checkbox" class="form-check-input">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -1,13 +1,14 @@
|
|||||||
# How to translate
|
# How to translate
|
||||||
|
|
||||||
1. Fork this repo.
|
1. Fork this repo.
|
||||||
2. Run `npm run update-language-files --language=<code>` where `<code>`
|
2. Run `npm install`
|
||||||
|
3. Run `npm run update-language-files --language=<code>` where `<code>`
|
||||||
is a valid ISO language code:
|
is a valid ISO language code:
|
||||||
http://www.lingoes.net/en/translator/langcode.htm. You can also use
|
http://www.lingoes.net/en/translator/langcode.htm. You can also use
|
||||||
this command to check if there are new strings to
|
this command to check if there are new strings to
|
||||||
translate for your language.
|
translate for your language.
|
||||||
3. Your language file should be filled in. You can translate now.
|
4. Your language file should be filled in. You can translate now.
|
||||||
4. Add it into `languageList` constant.
|
5. Add it into `languageList` constant.
|
||||||
5. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
|
6. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
|
||||||
|
|
||||||
If you do not have programming skills, let me know in [the issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏
|
If you do not have programming skills, let me know in [the issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏
|
||||||
|
@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="slide-fade" appear>
|
||||||
|
<div v-if="maintenance">
|
||||||
|
<h1>{{ maintenance.title }}</h1>
|
||||||
|
<p class="url">
|
||||||
|
<span>{{ $t("Start") }}: {{ $root.datetimeMaintenance(maintenance.start_date) }}</span>
|
||||||
|
<br>
|
||||||
|
<span>{{ $t("End") }}: {{ $root.datetimeMaintenance(maintenance.end_date) }}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="functions" style="margin-top: 10px;">
|
||||||
|
<router-link :to=" '/maintenance/edit/' + maintenance.id " class="btn btn-secondary">
|
||||||
|
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
||||||
|
</router-link>
|
||||||
|
<button class="btn btn-danger" @click="deleteDialog">
|
||||||
|
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="description" class="form-label" style="margin-top: 20px;">{{ $t("Description") }}</label>
|
||||||
|
<textarea id="description" v-model="maintenance.description" class="form-control" disabled></textarea>
|
||||||
|
|
||||||
|
<label for="affected_monitors" class="form-label" style="margin-top: 20px;">{{ $t("Affected Monitors") }}</label>
|
||||||
|
<br>
|
||||||
|
<button v-for="monitor in affectedMonitors" :key="monitor.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: 500;">
|
||||||
|
{{ monitor }}
|
||||||
|
</button>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<label for="selected_status_pages" class="form-label" style="margin-top: 20px;">{{ $t("Show this Maintenance Message on which Status Pages") }}</label>
|
||||||
|
<br>
|
||||||
|
<button v-for="statusPage in selectedStatusPages" :key="statusPage.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: 500;">
|
||||||
|
{{ statusPage }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMaintenance">
|
||||||
|
{{ $t("deleteMaintenanceMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
const toast = useToast();
|
||||||
|
import Confirm from "../components/Confirm.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
affectedMonitors: [],
|
||||||
|
selectedStatusPages: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
maintenance() {
|
||||||
|
let id = this.$route.params.id;
|
||||||
|
return this.$root.maintenanceList[id];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.affectedMonitors = Object.values(res.monitors).map(monitor => monitor.name);
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("getMaintenanceStatusPage", this.$route.params.id, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.selectedStatusPages = Object.values(res.statusPages).map(statusPage => statusPage.title);
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteDialog() {
|
||||||
|
this.$refs.confirmDelete.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteMaintenance() {
|
||||||
|
this.$root.deleteMaintenance(this.maintenance.id, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success(res.msg);
|
||||||
|
this.$router.push("/maintenance");
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
@media (max-width: 550px) {
|
||||||
|
.functions {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
button, a {
|
||||||
|
margin-left: 10px !important;
|
||||||
|
margin-right: 10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.btn {
|
||||||
|
padding-left: 25px;
|
||||||
|
padding-right: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.url {
|
||||||
|
color: $primary;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.functions {
|
||||||
|
button, a {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-monitor {
|
||||||
|
background-color: #5cdd8b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .btn-monitor {
|
||||||
|
color: #020b05 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -0,0 +1,280 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="slide-fade" appear>
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-3">
|
||||||
|
{{ $t("Maintenance") }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<router-link to="/add-maintenance" class="btn btn-primary mb-3">
|
||||||
|
<font-awesome-icon icon="plus" /> {{ $t("Schedule Maintenance") }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shadow-box">
|
||||||
|
<span v-if="Object.keys(sortedMaintenanceList).length === 0" class="d-flex align-items-center justify-content-center my-3">
|
||||||
|
{{ $t("No Maintenance") }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in sortedMaintenanceList"
|
||||||
|
:key="index"
|
||||||
|
class="item"
|
||||||
|
:class="item.status"
|
||||||
|
>
|
||||||
|
<div class="left-part">
|
||||||
|
<div
|
||||||
|
class="circle"
|
||||||
|
></div>
|
||||||
|
<div class="info">
|
||||||
|
<div class="title">{{ item.title }}</div>
|
||||||
|
<div v-if="false">{{ item.description }}</div>
|
||||||
|
<div class="status">
|
||||||
|
{{ $t("maintenanceStatus-" + item.status) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MaintenanceTime :maintenance="item" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<router-link v-if="false" :to="maintenanceURL(item.id)" class="btn btn-light">{{ $t("Details") }}</router-link>
|
||||||
|
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button v-if="item.active" class="btn btn-normal" @click="pauseDialog(item.id)">
|
||||||
|
<font-awesome-icon icon="pause" /> {{ $t("Pause") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="!item.active" class="btn btn-primary" @click="resumeMaintenance(item.id)">
|
||||||
|
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<router-link :to="'/maintenance/edit/' + item.id" class="btn btn-normal">
|
||||||
|
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<button class="btn btn-danger" @click="deleteDialog(item.id)">
|
||||||
|
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-3" style="font-size: 13px;">
|
||||||
|
<a href="https://github.com/louislam/uptime-kuma/wiki/Maintenance" target="_blank">Learn More</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseMaintenance">
|
||||||
|
{{ $t("pauseMaintenanceMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
|
||||||
|
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMaintenance">
|
||||||
|
{{ $t("deleteMaintenanceMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getResBaseURL } from "../util-frontend";
|
||||||
|
import { getMaintenanceRelativeURL } from "../util.ts";
|
||||||
|
import Confirm from "../components/Confirm.vue";
|
||||||
|
import MaintenanceTime from "../components/MaintenanceTime.vue";
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MaintenanceTime,
|
||||||
|
Confirm,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedMaintenanceID: undefined,
|
||||||
|
statusOrderList: {
|
||||||
|
"under-maintenance": 1000,
|
||||||
|
"scheduled": 900,
|
||||||
|
"inactive": 800,
|
||||||
|
"ended": 700,
|
||||||
|
"unknown": 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sortedMaintenanceList() {
|
||||||
|
let result = Object.values(this.$root.maintenanceList);
|
||||||
|
|
||||||
|
result.sort((m1, m2) => {
|
||||||
|
if (this.statusOrderList[m1.status] === this.statusOrderList[m2.status]) {
|
||||||
|
return m1.title.localeCompare(m2.title);
|
||||||
|
} else {
|
||||||
|
return this.statusOrderList[m1.status] < this.statusOrderList[m2.status];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Get the correct URL for the icon
|
||||||
|
* @param {string} icon Path for icon
|
||||||
|
* @returns {string} Correctly formatted path including port numbers
|
||||||
|
*/
|
||||||
|
icon(icon) {
|
||||||
|
if (icon === "/icon.svg") {
|
||||||
|
return icon;
|
||||||
|
} else {
|
||||||
|
return getResBaseURL() + icon;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
maintenanceURL(id) {
|
||||||
|
return getMaintenanceRelativeURL(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteDialog(maintenanceID) {
|
||||||
|
this.selectedMaintenanceID = maintenanceID;
|
||||||
|
this.$refs.confirmDelete.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteMaintenance() {
|
||||||
|
this.$root.deleteMaintenance(this.selectedMaintenanceID, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success(res.msg);
|
||||||
|
this.$router.push("/maintenance");
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show dialog to confirm pause
|
||||||
|
*/
|
||||||
|
pauseDialog(maintenanceID) {
|
||||||
|
this.selectedMaintenanceID = maintenanceID;
|
||||||
|
this.$refs.confirmPause.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause maintenance
|
||||||
|
*/
|
||||||
|
pauseMaintenance() {
|
||||||
|
this.$root.getSocket().emit("pauseMaintenance", this.selectedMaintenanceID, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume maintenance
|
||||||
|
*/
|
||||||
|
resumeMaintenance(id) {
|
||||||
|
this.$root.getSocket().emit("resumeMaintenance", id, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all ease-in-out 0.15s;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 90px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $highlight-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.under-maintenance {
|
||||||
|
background-color: rgba(23, 71, 245, 0.16);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(23, 71, 245, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
background-color: $maintenance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.scheduled {
|
||||||
|
.circle {
|
||||||
|
background-color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inactive {
|
||||||
|
.circle {
|
||||||
|
background-color: $danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ended {
|
||||||
|
.left-part {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
background-color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unknown {
|
||||||
|
.circle {
|
||||||
|
background-color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-part {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
border-radius: 50rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.item {
|
||||||
|
&:hover {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in new issue