mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-03 20:14:21 -04:00
Merge remote-tracking branch 'origin/master' into skaempfe#2593
# Conflicts: # server/model/monitor.js # src/pages/Details.vue
This commit is contained in:
commit
0b8dddba24
247 changed files with 34964 additions and 32467 deletions
76
server/model/api_key.js
Normal file
76
server/model/api_key.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { R } = require("redbean-node");
|
||||
const dayjs = require("dayjs");
|
||||
|
||||
class APIKey extends BeanModel {
|
||||
/**
|
||||
* Get the current status of this API key
|
||||
* @returns {string} active, inactive or expired
|
||||
*/
|
||||
getStatus() {
|
||||
let current = dayjs();
|
||||
let expiry = dayjs(this.expires);
|
||||
if (expiry.diff(current) < 0) {
|
||||
return "expired";
|
||||
}
|
||||
|
||||
return this.active ? "active" : "inactive";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
key: this.key,
|
||||
name: this.name,
|
||||
userID: this.user_id,
|
||||
createdDate: this.created_date,
|
||||
active: this.active,
|
||||
expires: this.expires,
|
||||
status: this.getStatus(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object that ready to parse to JSON with sensitive fields
|
||||
* removed
|
||||
* @returns {Object}
|
||||
*/
|
||||
toPublicJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
userID: this.user_id,
|
||||
createdDate: this.created_date,
|
||||
active: this.active,
|
||||
expires: this.expires,
|
||||
status: this.getStatus(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new API Key and store it in the database
|
||||
* @param {Object} key Object sent by client
|
||||
* @param {int} userID ID of socket user
|
||||
* @returns {Promise<bean>}
|
||||
*/
|
||||
static async save(key, userID) {
|
||||
let bean;
|
||||
bean = R.dispense("api_key");
|
||||
|
||||
bean.key = key.key;
|
||||
bean.name = key.name;
|
||||
bean.user_id = userID;
|
||||
bean.active = key.active;
|
||||
bean.expires = key.expires;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = APIKey;
|
|
@ -1,8 +1,10 @@
|
|||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
|
||||
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
|
||||
const { parseTimeObject, parseTimeFromTimeObject, log } = require("../../src/util");
|
||||
const { R } = require("redbean-node");
|
||||
const dayjs = require("dayjs");
|
||||
const Cron = require("croner");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const apicache = require("../modules/apicache");
|
||||
|
||||
class Maintenance extends BeanModel {
|
||||
|
||||
|
@ -15,16 +17,19 @@ class Maintenance extends BeanModel {
|
|||
|
||||
let dateRange = [];
|
||||
if (this.start_date) {
|
||||
dateRange.push(utcToLocal(this.start_date));
|
||||
if (this.end_date) {
|
||||
dateRange.push(utcToLocal(this.end_date));
|
||||
}
|
||||
dateRange.push(this.start_date);
|
||||
} else {
|
||||
dateRange.push(null);
|
||||
}
|
||||
|
||||
if (this.end_date) {
|
||||
dateRange.push(this.end_date);
|
||||
}
|
||||
|
||||
let timeRange = [];
|
||||
let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
|
||||
let startTime = parseTimeObject(this.start_time);
|
||||
timeRange.push(startTime);
|
||||
let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
|
||||
let endTime = parseTimeObject(this.end_time);
|
||||
timeRange.push(endTime);
|
||||
|
||||
let obj = {
|
||||
|
@ -39,12 +44,44 @@ class Maintenance extends BeanModel {
|
|||
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
|
||||
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
|
||||
timeslotList: [],
|
||||
cron: this.cron,
|
||||
duration: this.duration,
|
||||
durationMinutes: parseInt(this.duration / 60),
|
||||
timezone: await this.getTimezone(), // Only valid timezone
|
||||
timezoneOption: this.timezone, // Mainly for dropdown menu, because there is a option "SAME_AS_SERVER"
|
||||
timezoneOffset: await this.getTimezoneOffset(),
|
||||
status: await this.getStatus(),
|
||||
};
|
||||
|
||||
const timeslotList = await this.getTimeslotList();
|
||||
if (this.strategy === "manual") {
|
||||
// Do nothing, no timeslots
|
||||
} else if (this.strategy === "single") {
|
||||
obj.timeslotList.push({
|
||||
startDate: this.start_date,
|
||||
endDate: this.end_date,
|
||||
});
|
||||
} else {
|
||||
// Should be cron or recurring here
|
||||
if (this.beanMeta.job) {
|
||||
let runningTimeslot = this.getRunningTimeslot();
|
||||
|
||||
for (let timeslot of timeslotList) {
|
||||
obj.timeslotList.push(await timeslot.toPublicJSON());
|
||||
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)) {
|
||||
|
@ -55,54 +92,9 @@ class Maintenance extends BeanModel {
|
|||
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
|
||||
|
@ -112,6 +104,11 @@ class Maintenance extends BeanModel {
|
|||
return this.toPublicJSON(timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of weekdays that the maintenance is active for
|
||||
* Monday=1, Tuesday=2 etc.
|
||||
* @returns {number[]} Array of active weekdays
|
||||
*/
|
||||
getDayOfWeekList() {
|
||||
log.debug("timeslot", "List: " + this.weekdays);
|
||||
return JSON.parse(this.weekdays).sort(function (a, b) {
|
||||
|
@ -119,25 +116,21 @@ class Maintenance extends BeanModel {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of days in month that maintenance is active for
|
||||
* @returns {number[]|string[]} Array of active days in month
|
||||
*/
|
||||
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() {
|
||||
/**
|
||||
* Get the duration of maintenance in seconds
|
||||
* @returns {number} Duration of maintenance
|
||||
*/
|
||||
calcDuration() {
|
||||
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) {
|
||||
|
@ -146,71 +139,276 @@ class Maintenance extends BeanModel {
|
|||
return duration;
|
||||
}
|
||||
|
||||
static jsonToBean(bean, obj) {
|
||||
/**
|
||||
* Convert data from socket to bean
|
||||
* @param {Bean} bean Bean to fill in
|
||||
* @param {Object} obj Data to fill bean with
|
||||
* @returns {Bean} Filled bean
|
||||
*/
|
||||
static async 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.timezone = obj.timezoneOption;
|
||||
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_date = obj.dateRange[0];
|
||||
} else {
|
||||
bean.start_date = null;
|
||||
}
|
||||
|
||||
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
||||
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
||||
if (obj.dateRange[1]) {
|
||||
bean.end_date = obj.dateRange[1];
|
||||
} else {
|
||||
bean.end_date = null;
|
||||
}
|
||||
|
||||
bean.weekdays = JSON.stringify(obj.weekdays);
|
||||
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
||||
if (bean.strategy === "cron") {
|
||||
bean.duration = obj.durationMinutes * 60;
|
||||
bean.cron = obj.cron;
|
||||
this.validateCron(bean.cron);
|
||||
}
|
||||
|
||||
if (bean.strategy.startsWith("recurring-")) {
|
||||
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);
|
||||
await bean.generateCron();
|
||||
this.validateCron(bean.cron);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL conditions for active maintenance
|
||||
* @returns {string}
|
||||
* Throw error if cron is invalid
|
||||
* @param cron
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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)
|
||||
)
|
||||
`;
|
||||
static async validateCron(cron) {
|
||||
let job = new Cron(cron, () => {});
|
||||
job.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL conditions for active and future maintenance
|
||||
* @returns {string}
|
||||
* Run the cron
|
||||
*/
|
||||
static getActiveAndFutureMaintenanceSQLCondition() {
|
||||
return `
|
||||
(
|
||||
((maintenance_timeslot.end_date >= DATETIME('now')
|
||||
AND maintenance.active = 1)
|
||||
OR
|
||||
(maintenance.strategy = 'manual' AND active = 1))
|
||||
)
|
||||
`;
|
||||
async run(throwError = false) {
|
||||
if (this.beanMeta.job) {
|
||||
log.debug("maintenance", "Maintenance is already running, stop it first. id: " + this.id);
|
||||
this.stop();
|
||||
}
|
||||
|
||||
log.debug("maintenance", "Run maintenance id: " + this.id);
|
||||
|
||||
// 1.21.2 migration
|
||||
if (!this.cron) {
|
||||
await this.generateCron();
|
||||
if (!this.timezone) {
|
||||
this.timezone = "UTC";
|
||||
}
|
||||
if (this.cron) {
|
||||
await R.store(this);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.strategy === "manual") {
|
||||
// Do nothing, because it is controlled by the user
|
||||
} else if (this.strategy === "single") {
|
||||
this.beanMeta.job = new Cron(this.start_date, { timezone: await this.getTimezone() }, () => {
|
||||
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
apicache.clear();
|
||||
});
|
||||
} else if (this.cron != null) {
|
||||
// Here should be cron or recurring
|
||||
try {
|
||||
this.beanMeta.status = "scheduled";
|
||||
|
||||
let startEvent = (customDuration = 0) => {
|
||||
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
||||
|
||||
this.beanMeta.status = "under-maintenance";
|
||||
clearTimeout(this.beanMeta.durationTimeout);
|
||||
|
||||
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
|
||||
let duration;
|
||||
|
||||
if (customDuration > 0) {
|
||||
duration = customDuration;
|
||||
} else if (this.end_date) {
|
||||
let d = dayjs(this.end_date).diff(dayjs(), "second");
|
||||
if (d < this.duration) {
|
||||
duration = d * 1000;
|
||||
}
|
||||
} else {
|
||||
duration = this.duration * 1000;
|
||||
}
|
||||
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
|
||||
this.beanMeta.durationTimeout = setTimeout(() => {
|
||||
// End of maintenance for this timeslot
|
||||
this.beanMeta.status = "scheduled";
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
}, duration);
|
||||
};
|
||||
|
||||
// Create Cron
|
||||
this.beanMeta.job = new Cron(this.cron, {
|
||||
timezone: await this.getTimezone(),
|
||||
}, startEvent);
|
||||
|
||||
// Continue if the maintenance is still in the window
|
||||
let runningTimeslot = this.getRunningTimeslot();
|
||||
let current = dayjs();
|
||||
|
||||
if (runningTimeslot) {
|
||||
let duration = dayjs(runningTimeslot.endDate).diff(current, "second") * 1000;
|
||||
log.debug("maintenance", "Maintenance id: " + this.id + " Remaining duration: " + duration + "ms");
|
||||
startEvent(duration);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
log.error("maintenance", "Error in maintenance id: " + this.id);
|
||||
log.error("maintenance", "Cron: " + this.cron);
|
||||
log.error("maintenance", e);
|
||||
|
||||
if (throwError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
log.error("maintenance", "Maintenance id: " + this.id + " has no cron");
|
||||
}
|
||||
}
|
||||
|
||||
getRunningTimeslot() {
|
||||
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").toDate()));
|
||||
let end = start.add(this.duration, "second");
|
||||
let current = dayjs();
|
||||
|
||||
if (current.isAfter(start) && current.isBefore(end)) {
|
||||
return {
|
||||
startDate: start.toISOString(),
|
||||
endDate: end.toISOString(),
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.beanMeta.job) {
|
||||
this.beanMeta.job.stop();
|
||||
delete this.beanMeta.job;
|
||||
}
|
||||
}
|
||||
|
||||
async isUnderMaintenance() {
|
||||
return (await this.getStatus()) === "under-maintenance";
|
||||
}
|
||||
|
||||
async getTimezone() {
|
||||
if (!this.timezone || this.timezone === "SAME_AS_SERVER") {
|
||||
return await UptimeKumaServer.getInstance().getTimezone();
|
||||
}
|
||||
return this.timezone;
|
||||
}
|
||||
|
||||
async getTimezoneOffset() {
|
||||
return dayjs.tz(dayjs(), await this.getTimezone()).format("Z");
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
if (!this.active) {
|
||||
return "inactive";
|
||||
}
|
||||
|
||||
if (this.strategy === "manual") {
|
||||
return "under-maintenance";
|
||||
}
|
||||
|
||||
// Check if the maintenance is started
|
||||
if (this.start_date && dayjs().isBefore(dayjs.tz(this.start_date, await this.getTimezone()))) {
|
||||
return "scheduled";
|
||||
}
|
||||
|
||||
// Check if the maintenance is ended
|
||||
if (this.end_date && dayjs().isAfter(dayjs.tz(this.end_date, await this.getTimezone()))) {
|
||||
return "ended";
|
||||
}
|
||||
|
||||
if (this.strategy === "single") {
|
||||
return "under-maintenance";
|
||||
}
|
||||
|
||||
if (!this.beanMeta.status) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
return this.beanMeta.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Cron for recurring maintenance
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async generateCron() {
|
||||
log.info("maintenance", "Generate cron for maintenance id: " + this.id);
|
||||
|
||||
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 hour = parseInt(array[0]);
|
||||
let minute = parseInt(array[1]);
|
||||
this.cron = minute + " " + hour + " */" + this.interval_day + " * *";
|
||||
this.duration = this.calcDuration();
|
||||
log.debug("maintenance", "Cron: " + this.cron);
|
||||
log.debug("maintenance", "Duration: " + this.duration);
|
||||
} else if (this.strategy === "recurring-weekday") {
|
||||
let list = this.getDayOfWeekList();
|
||||
let array = this.start_time.split(":");
|
||||
let hour = parseInt(array[0]);
|
||||
let minute = parseInt(array[1]);
|
||||
this.cron = minute + " " + hour + " * * " + list.join(",");
|
||||
this.duration = this.calcDuration();
|
||||
} else if (this.strategy === "recurring-day-of-month") {
|
||||
let list = this.getDayOfMonthList();
|
||||
let array = this.start_time.split(":");
|
||||
let hour = parseInt(array[0]);
|
||||
let minute = parseInt(array[1]);
|
||||
|
||||
let dayList = [];
|
||||
|
||||
for (let day of list) {
|
||||
if (typeof day === "string" && day.startsWith("lastDay")) {
|
||||
if (day === "lastDay1") {
|
||||
dayList.push("L");
|
||||
}
|
||||
// Unfortunately, lastDay2-4 is not supported by cron
|
||||
} else {
|
||||
dayList.push(day);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicate
|
||||
dayList = [ ...new Set(dayList) ];
|
||||
|
||||
this.cron = minute + " " + hour + " " + dayList.join(",") + " * *";
|
||||
this.duration = this.calcDuration();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,189 +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 {
|
||||
|
||||
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;
|
|
@ -2,8 +2,12 @@ const https = require("https");
|
|||
const dayjs = require("dayjs");
|
||||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery } = require("../util-server");
|
||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
||||
SQL_DATETIME_FORMAT
|
||||
} = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
||||
redisPingAsync, mongodbPing,
|
||||
} = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { Notification } = require("../notification");
|
||||
|
@ -14,8 +18,8 @@ const apicache = require("../modules/apicache");
|
|||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||
const { DockerHost } = require("../docker");
|
||||
const Maintenance = require("./maintenance");
|
||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
||||
const Gamedig = require("gamedig");
|
||||
|
||||
/**
|
||||
* status:
|
||||
|
@ -36,7 +40,6 @@ class Monitor extends BeanModel {
|
|||
id: this.id,
|
||||
name: this.name,
|
||||
sendUrl: this.sendUrl,
|
||||
maintenance: await Monitor.isUnderMaintenance(this.id),
|
||||
};
|
||||
|
||||
if (this.sendUrl) {
|
||||
|
@ -70,6 +73,7 @@ class Monitor extends BeanModel {
|
|||
let data = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
url: this.url,
|
||||
method: this.method,
|
||||
hostname: this.hostname,
|
||||
|
@ -85,6 +89,7 @@ class Monitor extends BeanModel {
|
|||
expiryNotification: this.isEnabledExpiryNotification(),
|
||||
ignoreTls: this.getIgnoreTls(),
|
||||
upsideDown: this.isUpsideDown(),
|
||||
packetSize: this.packetSize,
|
||||
maxredirects: this.maxredirects,
|
||||
accepted_statuscodes: this.getAcceptedStatuscodes(),
|
||||
dns_resolve_type: this.dns_resolve_type,
|
||||
|
@ -107,6 +112,8 @@ class Monitor extends BeanModel {
|
|||
grpcEnableTls: this.getGrpcEnableTls(),
|
||||
radiusCalledStationId: this.radiusCalledStationId,
|
||||
radiusCallingStationId: this.radiusCallingStationId,
|
||||
game: this.game,
|
||||
httpBodyEncoding: this.httpBodyEncoding
|
||||
};
|
||||
|
||||
if (includeSensitiveData) {
|
||||
|
@ -127,6 +134,9 @@ class Monitor extends BeanModel {
|
|||
mqttPassword: this.mqttPassword,
|
||||
authWorkstation: this.authWorkstation,
|
||||
authDomain: this.authDomain,
|
||||
tlsCa: this.tlsCa,
|
||||
tlsCert: this.tlsCert,
|
||||
tlsKey: this.tlsKey,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -139,7 +149,7 @@ class Monitor extends BeanModel {
|
|||
* @returns {Promise<LooseObject<any>[]>}
|
||||
*/
|
||||
async getTags() {
|
||||
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [ this.id ]);
|
||||
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ? ORDER BY tag.name", [ this.id ]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -199,7 +209,7 @@ class Monitor extends BeanModel {
|
|||
let previousBeat = null;
|
||||
let retries = 0;
|
||||
|
||||
let prometheus = new Prometheus(this);
|
||||
this.prometheus = new Prometheus(this);
|
||||
|
||||
const beat = async () => {
|
||||
|
||||
|
@ -268,17 +278,34 @@ class Monitor extends BeanModel {
|
|||
|
||||
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
||||
|
||||
let contentType = null;
|
||||
let bodyValue = null;
|
||||
|
||||
if (this.body && (typeof this.body === "string" && this.body.trim().length > 0)) {
|
||||
if (!this.httpBodyEncoding || this.httpBodyEncoding === "json") {
|
||||
try {
|
||||
bodyValue = JSON.parse(this.body);
|
||||
contentType = "application/json";
|
||||
} catch (e) {
|
||||
throw new Error("Your JSON body is invalid. " + e.message);
|
||||
}
|
||||
} else if (this.httpBodyEncoding === "xml") {
|
||||
bodyValue = this.body;
|
||||
contentType = "text/xml; charset=utf-8";
|
||||
}
|
||||
}
|
||||
|
||||
// Axios Options
|
||||
const options = {
|
||||
url: this.url,
|
||||
method: (this.method || "get").toLowerCase(),
|
||||
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
headers: {
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
...(this.headers ? JSON.parse(this.headers) : {}),
|
||||
...(contentType ? { "Content-Type": contentType } : {}),
|
||||
...(basicAuthHeader),
|
||||
...(this.headers ? JSON.parse(this.headers) : {})
|
||||
},
|
||||
maxRedirects: this.maxredirects,
|
||||
validateStatus: (status) => {
|
||||
|
@ -286,6 +313,10 @@ class Monitor extends BeanModel {
|
|||
},
|
||||
};
|
||||
|
||||
if (bodyValue) {
|
||||
options.data = bodyValue;
|
||||
}
|
||||
|
||||
if (this.proxy_id) {
|
||||
const proxy = await R.load("proxy", this.proxy_id);
|
||||
|
||||
|
@ -304,6 +335,18 @@ class Monitor extends BeanModel {
|
|||
options.httpsAgent = new https.Agent(httpsAgentOptions);
|
||||
}
|
||||
|
||||
if (this.auth_method === "mtls") {
|
||||
if (this.tlsCert !== null && this.tlsCert !== "") {
|
||||
options.httpsAgent.options.cert = Buffer.from(this.tlsCert);
|
||||
}
|
||||
if (this.tlsCa !== null && this.tlsCa !== "") {
|
||||
options.httpsAgent.options.ca = Buffer.from(this.tlsCa);
|
||||
}
|
||||
if (this.tlsKey !== null && this.tlsKey !== "") {
|
||||
options.httpsAgent.options.key = Buffer.from(this.tlsKey);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||
|
||||
|
@ -357,7 +400,7 @@ class Monitor extends BeanModel {
|
|||
bean.msg += ", keyword is found";
|
||||
bean.status = UP;
|
||||
} else {
|
||||
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
|
||||
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
|
||||
if (data.length > 50) {
|
||||
data = data.substring(0, 47) + "...";
|
||||
}
|
||||
|
@ -372,7 +415,7 @@ class Monitor extends BeanModel {
|
|||
bean.status = UP;
|
||||
|
||||
} else if (this.type === "ping") {
|
||||
bean.ping = await ping(this.hostname);
|
||||
bean.ping = await ping(this.hostname, this.packetSize);
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
} else if (this.type === "dns") {
|
||||
|
@ -482,25 +525,44 @@ class Monitor extends BeanModel {
|
|||
bean.msg = res.data.response.servers[0].name;
|
||||
|
||||
try {
|
||||
bean.ping = await ping(this.hostname);
|
||||
bean.ping = await ping(this.hostname, this.packetSize);
|
||||
} catch (_) { }
|
||||
} else {
|
||||
throw new Error("Server not found on Steam");
|
||||
}
|
||||
} else if (this.type === "gamedig") {
|
||||
try {
|
||||
const state = await Gamedig.query({
|
||||
type: this.game,
|
||||
host: this.hostname,
|
||||
port: this.port,
|
||||
givenPortOnly: true,
|
||||
});
|
||||
|
||||
bean.msg = state.name;
|
||||
bean.status = UP;
|
||||
bean.ping = state.ping;
|
||||
} catch (e) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
} else if (this.type === "docker") {
|
||||
log.debug(`[${this.name}] Prepare Options for Axios`);
|
||||
log.debug("monitor", `[${this.name}] Prepare Options for Axios`);
|
||||
|
||||
const dockerHost = await R.load("docker_host", this.docker_host);
|
||||
|
||||
const options = {
|
||||
url: `/containers/${this.docker_container}/json`,
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: ! this.getIgnoreTls(),
|
||||
rejectUnauthorized: !this.getIgnoreTls(),
|
||||
}),
|
||||
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||
maxCachedSessions: 0,
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -510,7 +572,7 @@ class Monitor extends BeanModel {
|
|||
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
||||
}
|
||||
|
||||
log.debug(`[${this.name}] Axios Request`);
|
||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||
let res = await axios.request(options);
|
||||
if (res.data.State.Running) {
|
||||
bean.status = UP;
|
||||
|
@ -576,11 +638,18 @@ class Monitor extends BeanModel {
|
|||
} else if (this.type === "mysql") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "mongodb") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
await mongodbPing(this.databaseConnectionString);
|
||||
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
} else if (this.type === "radius") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
|
@ -617,9 +686,23 @@ class Monitor extends BeanModel {
|
|||
}
|
||||
}
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "redis") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
bean.msg = await redisPingAsync(this.databaseConnectionString);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
} else if (this.type in UptimeKumaServer.monitorTypeList) {
|
||||
let startTime = dayjs().valueOf();
|
||||
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
|
||||
await monitorType.check(this, bean);
|
||||
if (!bean.ping) {
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
}
|
||||
|
||||
} else {
|
||||
bean.msg = "Unknown Monitor Type";
|
||||
bean.status = PENDING;
|
||||
throw new Error("Unknown Monitor Type");
|
||||
}
|
||||
|
||||
if (this.isUpsideDown()) {
|
||||
|
@ -709,7 +792,7 @@ class Monitor extends BeanModel {
|
|||
await R.store(bean);
|
||||
|
||||
log.debug("monitor", `[${this.name}] prometheus.update`);
|
||||
prometheus.update(bean, tlsInfo);
|
||||
this.prometheus?.update(bean, tlsInfo);
|
||||
|
||||
previousBeat = bean;
|
||||
|
||||
|
@ -748,6 +831,13 @@ class Monitor extends BeanModel {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request using axios
|
||||
* @param {Object} options Options for Axios
|
||||
* @param {boolean} finalCall Should this be the final call i.e
|
||||
* don't retry on faliure
|
||||
* @returns {Object} Axios response
|
||||
*/
|
||||
async makeAxiosRequest(options, finalCall = false) {
|
||||
try {
|
||||
let res;
|
||||
|
@ -760,7 +850,6 @@ class Monitor extends BeanModel {
|
|||
domain: this.authDomain,
|
||||
workstation: this.authWorkstation ? this.authWorkstation : undefined
|
||||
});
|
||||
|
||||
} else {
|
||||
res = await axios.request(options);
|
||||
}
|
||||
|
@ -787,15 +876,15 @@ class Monitor extends BeanModel {
|
|||
clearTimeout(this.heartbeatInterval);
|
||||
this.isStop = true;
|
||||
|
||||
this.prometheus().remove();
|
||||
this.prometheus?.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new prometheus instance
|
||||
* @returns {Prometheus}
|
||||
* Get prometheus instance
|
||||
* @returns {Prometheus|undefined}
|
||||
*/
|
||||
prometheus() {
|
||||
return new Prometheus(this);
|
||||
getPrometheus() {
|
||||
return this.prometheus;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1089,12 +1178,18 @@ class Monitor extends BeanModel {
|
|||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||
const heartbeatJSON = bean.toJSON();
|
||||
|
||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||
if (!heartbeatJSON["msg"]) {
|
||||
heartbeatJSON["msg"] = "N/A";
|
||||
}
|
||||
|
||||
// Also provide the time in server timezone
|
||||
heartbeatJSON["timezone"] = await UptimeKumaServer.getInstance().getTimezone();
|
||||
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
|
||||
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
|
||||
|
||||
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
|
||||
} catch (e) {
|
||||
log.error("monitor", "Cannot send notification to " + notification.name);
|
||||
|
@ -1167,11 +1262,11 @@ class Monitor extends BeanModel {
|
|||
*/
|
||||
async sendCertNotificationByTargetDays(certCN, certType, daysRemaining, targetDays, notificationList) {
|
||||
|
||||
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days = ?", [
|
||||
"certificate",
|
||||
this.id,
|
||||
targetDays,
|
||||
]);
|
||||
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [
|
||||
"certificate",
|
||||
this.id,
|
||||
targetDays,
|
||||
]);
|
||||
|
||||
// Sent already, no need to send again
|
||||
if (row) {
|
||||
|
@ -1209,7 +1304,7 @@ class Monitor extends BeanModel {
|
|||
*/
|
||||
static async getPreviousHeartbeat(monitorID) {
|
||||
return await R.getRow(`
|
||||
SELECT status, time FROM heartbeat
|
||||
SELECT ping, status, time FROM heartbeat
|
||||
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
||||
`, [
|
||||
monitorID
|
||||
|
@ -1222,20 +1317,22 @@ class Monitor extends BeanModel {
|
|||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
static async isUnderMaintenance(monitorID) {
|
||||
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
|
||||
const maintenance = await R.getRow(`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM monitor_maintenance mm
|
||||
JOIN maintenance
|
||||
ON mm.maintenance_id = maintenance.id
|
||||
AND mm.monitor_id = ?
|
||||
LEFT JOIN maintenance_timeslot
|
||||
ON maintenance_timeslot.maintenance_id = maintenance.id
|
||||
WHERE ${activeCondition}
|
||||
LIMIT 1`, [ monitorID ]);
|
||||
return maintenance.count !== 0;
|
||||
const maintenanceIDList = await R.getCol(`
|
||||
SELECT maintenance_id FROM monitor_maintenance
|
||||
WHERE monitor_id = ?
|
||||
`, [ monitorID ]);
|
||||
|
||||
for (const maintenanceID of maintenanceIDList) {
|
||||
const maintenance = await UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
|
||||
if (maintenance && await maintenance.isUnderMaintenance()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Make sure monitor interval is between bounds */
|
||||
validate() {
|
||||
if (this.interval > MAX_INTERVAL_SECOND) {
|
||||
throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`);
|
||||
|
|
|
@ -3,7 +3,7 @@ const { R } = require("redbean-node");
|
|||
const cheerio = require("cheerio");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const jsesc = require("jsesc");
|
||||
const Maintenance = require("./maintenance");
|
||||
const googleAnalytics = require("../google-analytics");
|
||||
|
||||
class StatusPage extends BeanModel {
|
||||
|
||||
|
@ -53,9 +53,17 @@ class StatusPage extends BeanModel {
|
|||
|
||||
const head = $("head");
|
||||
|
||||
if (statusPage.googleAnalyticsTagId) {
|
||||
let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.googleAnalyticsTagId);
|
||||
head.append($(escapedGoogleAnalyticsScript));
|
||||
}
|
||||
|
||||
// OG Meta Tags
|
||||
head.append(`<meta property="og:title" content="${statusPage.title}" />`);
|
||||
head.append(`<meta property="og:description" content="${description155}" />`);
|
||||
let ogTitle = $("<meta property=\"og:title\" content=\"\" />").attr("content", statusPage.title);
|
||||
head.append(ogTitle);
|
||||
|
||||
let ogDescription = $("<meta property=\"og:description\" content=\"\" />").attr("content", description155);
|
||||
head.append(ogDescription);
|
||||
|
||||
// Preload data
|
||||
// Add jsesc, fix https://github.com/louislam/uptime-kuma/issues/2186
|
||||
|
@ -225,6 +233,7 @@ class StatusPage extends BeanModel {
|
|||
customCSS: this.custom_css,
|
||||
footerText: this.footer_text,
|
||||
showPoweredBy: !!this.show_powered_by,
|
||||
googleAnalyticsId: this.google_analytics_tag_id,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -245,6 +254,7 @@ class StatusPage extends BeanModel {
|
|||
customCSS: this.custom_css,
|
||||
footerText: this.footer_text,
|
||||
showPoweredBy: !!this.show_powered_by,
|
||||
googleAnalyticsId: this.google_analytics_tag_id,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -279,21 +289,17 @@ class StatusPage extends BeanModel {
|
|||
try {
|
||||
const publicMaintenanceList = [];
|
||||
|
||||
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
|
||||
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
|
||||
SELECT DISTINCT maintenance.*
|
||||
FROM maintenance
|
||||
JOIN maintenance_status_page
|
||||
ON maintenance_status_page.maintenance_id = maintenance.id
|
||||
AND maintenance_status_page.status_page_id = ?
|
||||
LEFT JOIN maintenance_timeslot
|
||||
ON maintenance_timeslot.maintenance_id = maintenance.id
|
||||
WHERE ${activeCondition}
|
||||
ORDER BY maintenance.end_date
|
||||
`, [ statusPageId ]));
|
||||
let maintenanceIDList = await R.getCol(`
|
||||
SELECT DISTINCT maintenance_id
|
||||
FROM maintenance_status_page
|
||||
WHERE status_page_id = ?
|
||||
`, [ statusPageId ]);
|
||||
|
||||
for (const bean of maintenanceBeanList) {
|
||||
publicMaintenanceList.push(await bean.toPublicJSON());
|
||||
for (const maintenanceID of maintenanceIDList) {
|
||||
let maintenance = UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
|
||||
if (maintenance && await maintenance.isUnderMaintenance()) {
|
||||
publicMaintenanceList.push(await maintenance.toPublicJSON());
|
||||
}
|
||||
}
|
||||
|
||||
return publicMaintenanceList;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue