mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-10-01 01:25:45 -04:00
Merge branch 'master' into feature/#1817-add-mysql-monitor
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
This commit is contained in:
commit
2052fa175f
25
db/patch-grpc-monitor.sql
Normal file
25
db/patch-grpc-monitor.sql
Normal file
@ -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;
|
83
db/patch-maintenance-table2.sql
Normal file
83
db/patch-maintenance-table2.sql
Normal file
@ -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;
|
4390
package-lock.json
generated
4390
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.18.5",
|
"version": "1.19.0-beta.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -22,6 +22,7 @@
|
|||||||
"start": "npm run start-server",
|
"start": "npm run start-server",
|
||||||
"start-server": "node server/server.js",
|
"start-server": "node server/server.js",
|
||||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||||
|
"start-server-watch-dev": "cross-env NODE_ENV=development node --watch server/server.js",
|
||||||
"build": "vite build --config ./config/vite.config.js",
|
"build": "vite build --config ./config/vite.config.js",
|
||||||
"test": "node test/prepare-test-server.js && npm run jest-backend",
|
"test": "node test/prepare-test-server.js && npm run jest-backend",
|
||||||
"test-with-build": "npm run build && npm test",
|
"test-with-build": "npm run build && npm test",
|
||||||
@ -63,7 +64,8 @@
|
|||||||
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\""
|
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@louislam/sqlite3": "~15.0.6",
|
"@grpc/grpc-js": "^1.7.0",
|
||||||
|
"@louislam/sqlite3": "15.1.2",
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.27.0",
|
"axios": "~0.27.0",
|
||||||
"axios-ntlm": "~1.3.0",
|
"axios-ntlm": "~1.3.0",
|
||||||
@ -103,9 +105,10 @@
|
|||||||
"pg-connection-string": "~2.5.0",
|
"pg-connection-string": "~2.5.0",
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.1",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
|
"protobufjs": "~7.1.1",
|
||||||
"redbean-node": "0.1.4",
|
"redbean-node": "0.1.4",
|
||||||
"socket.io": "~4.4.1",
|
"socket.io": "~4.5.3",
|
||||||
"socket.io-client": "~4.4.1",
|
"socket.io-client": "~4.5.3",
|
||||||
"socks-proxy-agent": "6.1.1",
|
"socks-proxy-agent": "6.1.1",
|
||||||
"tar": "~6.1.11",
|
"tar": "~6.1.11",
|
||||||
"tcp-ping": "~0.1.1",
|
"tcp-ping": "~0.1.1",
|
||||||
@ -124,6 +127,7 @@
|
|||||||
"@vitejs/plugin-legacy": "~2.1.0",
|
"@vitejs/plugin-legacy": "~2.1.0",
|
||||||
"@vitejs/plugin-vue": "~3.1.0",
|
"@vitejs/plugin-vue": "~3.1.0",
|
||||||
"@vue/compiler-sfc": "~3.2.36",
|
"@vue/compiler-sfc": "~3.2.36",
|
||||||
|
"@vuepic/vue-datepicker": "~3.4.8",
|
||||||
"aedes": "^0.46.3",
|
"aedes": "^0.46.3",
|
||||||
"babel-plugin-rewire": "~1.2.0",
|
"babel-plugin-rewire": "~1.2.0",
|
||||||
"bootstrap": "5.1.3",
|
"bootstrap": "5.1.3",
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
const { TimeLogger } = require("../src/util");
|
const { TimeLogger } = require("../src/util");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||||
const io = UptimeKumaServer.getInstance().io;
|
const server = UptimeKumaServer.getInstance();
|
||||||
|
const io = server.io;
|
||||||
const { setting } = require("./util-server");
|
const { setting } = require("./util-server");
|
||||||
const checkVersion = require("./check-version");
|
const checkVersion = require("./check-version");
|
||||||
|
|
||||||
@ -121,7 +122,9 @@ async function sendInfo(socket) {
|
|||||||
socket.emit("info", {
|
socket.emit("info", {
|
||||||
version: checkVersion.version,
|
version: checkVersion.version,
|
||||||
latestVersion: checkVersion.latestVersion,
|
latestVersion: checkVersion.latestVersion,
|
||||||
primaryBaseURL: await setting("primaryBaseURL")
|
primaryBaseURL: await setting("primaryBaseURL"),
|
||||||
|
serverTimezone: await server.getTimezone(),
|
||||||
|
serverTimezoneOffset: server.getTimezoneOffset(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,8 +62,10 @@ class Database {
|
|||||||
"patch-add-clickable-status-page-link.sql": true,
|
"patch-add-clickable-status-page-link.sql": true,
|
||||||
"patch-add-sqlserver-monitor.sql": true,
|
"patch-add-sqlserver-monitor.sql": true,
|
||||||
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
|
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
|
||||||
|
"patch-grpc-monitor.sql": true,
|
||||||
"patch-add-radius-monitor.sql": true,
|
"patch-add-radius-monitor.sql": true,
|
||||||
"patch-monitor-add-resend-interval.sql": true,
|
"patch-monitor-add-resend-interval.sql": true,
|
||||||
|
"patch-maintenance-table2.sql": true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
const dayjs = require("dayjs");
|
|
||||||
const utc = require("dayjs/plugin/utc");
|
|
||||||
let timezone = require("dayjs/plugin/timezone");
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -10,6 +5,7 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
|||||||
* 0 = DOWN
|
* 0 = DOWN
|
||||||
* 1 = UP
|
* 1 = UP
|
||||||
* 2 = PENDING
|
* 2 = PENDING
|
||||||
|
* 3 = MAINTENANCE
|
||||||
*/
|
*/
|
||||||
class Heartbeat extends BeanModel {
|
class Heartbeat extends BeanModel {
|
||||||
|
|
||||||
|
215
server/model/maintenance.js
Normal file
215
server/model/maintenance.js
Normal file
@ -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;
|
189
server/model/maintenance_timeslot.js
Normal file
189
server/model/maintenance_timeslot.js
Normal file
@ -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;
|
@ -1,13 +1,9 @@
|
|||||||
const https = require("https");
|
const https = require("https");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const utc = require("dayjs/plugin/utc");
|
|
||||||
let timezone = require("dayjs/plugin/timezone");
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger } = require("../../src/util");
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server");
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { Notification } = require("../notification");
|
const { Notification } = require("../notification");
|
||||||
@ -18,12 +14,14 @@ const apicache = require("../modules/apicache");
|
|||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||||
const { DockerHost } = require("../docker");
|
const { DockerHost } = require("../docker");
|
||||||
|
const Maintenance = require("./maintenance");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
* 0 = DOWN
|
* 0 = DOWN
|
||||||
* 1 = UP
|
* 1 = UP
|
||||||
* 2 = PENDING
|
* 2 = PENDING
|
||||||
|
* 3 = MAINTENANCE
|
||||||
*/
|
*/
|
||||||
class Monitor extends BeanModel {
|
class Monitor extends BeanModel {
|
||||||
|
|
||||||
@ -37,6 +35,7 @@ class Monitor extends BeanModel {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
sendUrl: this.sendUrl,
|
sendUrl: this.sendUrl,
|
||||||
|
maintenance: await Monitor.isUnderMaintenance(this.id),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.sendUrl) {
|
if (this.sendUrl) {
|
||||||
@ -96,6 +95,7 @@ class Monitor extends BeanModel {
|
|||||||
proxyId: this.proxy_id,
|
proxyId: this.proxy_id,
|
||||||
notificationIDList,
|
notificationIDList,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
|
maintenance: await Monitor.isUnderMaintenance(this.id),
|
||||||
mqttUsername: this.mqttUsername,
|
mqttUsername: this.mqttUsername,
|
||||||
mqttPassword: this.mqttPassword,
|
mqttPassword: this.mqttPassword,
|
||||||
mqttTopic: this.mqttTopic,
|
mqttTopic: this.mqttTopic,
|
||||||
@ -105,6 +105,11 @@ class Monitor extends BeanModel {
|
|||||||
authMethod: this.authMethod,
|
authMethod: this.authMethod,
|
||||||
authWorkstation: this.authWorkstation,
|
authWorkstation: this.authWorkstation,
|
||||||
authDomain: this.authDomain,
|
authDomain: this.authDomain,
|
||||||
|
grpcUrl: this.grpcUrl,
|
||||||
|
grpcProtobuf: this.grpcProtobuf,
|
||||||
|
grpcMethod: this.grpcMethod,
|
||||||
|
grpcServiceName: this.grpcServiceName,
|
||||||
|
grpcEnableTls: this.getGrpcEnableTls(),
|
||||||
radiusUsername: this.radiusUsername,
|
radiusUsername: this.radiusUsername,
|
||||||
radiusPassword: this.radiusPassword,
|
radiusPassword: this.radiusPassword,
|
||||||
radiusCalledStationId: this.radiusCalledStationId,
|
radiusCalledStationId: this.radiusCalledStationId,
|
||||||
@ -117,6 +122,8 @@ class Monitor extends BeanModel {
|
|||||||
...data,
|
...data,
|
||||||
headers: this.headers,
|
headers: this.headers,
|
||||||
body: this.body,
|
body: this.body,
|
||||||
|
grpcBody: this.grpcBody,
|
||||||
|
grpcMetadata: this.grpcMetadata,
|
||||||
basic_auth_user: this.basic_auth_user,
|
basic_auth_user: this.basic_auth_user,
|
||||||
basic_auth_pass: this.basic_auth_pass,
|
basic_auth_pass: this.basic_auth_pass,
|
||||||
pushToken: this.pushToken,
|
pushToken: this.pushToken,
|
||||||
@ -167,6 +174,14 @@ class Monitor extends BeanModel {
|
|||||||
return Boolean(this.upsideDown);
|
return Boolean(this.upsideDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
getGrpcEnableTls() {
|
||||||
|
return Boolean(this.grpcEnableTls);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get accepted status codes
|
* Get accepted status codes
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
@ -230,7 +245,10 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.type === "http" || this.type === "keyword") {
|
if (await Monitor.isUnderMaintenance(this.id)) {
|
||||||
|
bean.msg = "Monitor under maintenance";
|
||||||
|
bean.status = MAINTENANCE;
|
||||||
|
} else if (this.type === "http" || this.type === "keyword") {
|
||||||
// Do not do any queries/high loading things before the "bean.ping"
|
// Do not do any queries/high loading things before the "bean.ping"
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
@ -524,6 +542,37 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg = "";
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
} else if (this.type === "grpc-keyword") {
|
||||||
|
let startTime = dayjs().valueOf();
|
||||||
|
const options = {
|
||||||
|
grpcUrl: this.grpcUrl,
|
||||||
|
grpcProtobufData: this.grpcProtobuf,
|
||||||
|
grpcServiceName: this.grpcServiceName,
|
||||||
|
grpcEnableTls: this.grpcEnableTls,
|
||||||
|
grpcMethod: this.grpcMethod,
|
||||||
|
grpcBody: this.grpcBody,
|
||||||
|
keyword: this.keyword
|
||||||
|
};
|
||||||
|
const response = await grpcQuery(options);
|
||||||
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
log.debug("monitor:", `gRPC response: ${JSON.stringify(response)}`);
|
||||||
|
let responseData = response.data;
|
||||||
|
if (responseData.length > 50) {
|
||||||
|
responseData = response.substring(0, 47) + "...";
|
||||||
|
}
|
||||||
|
if (response.code !== 1) {
|
||||||
|
bean.status = DOWN;
|
||||||
|
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
|
||||||
|
} else {
|
||||||
|
if (response.data.toString().includes(this.keyword)) {
|
||||||
|
bean.status = UP;
|
||||||
|
bean.msg = `${responseData}, keyword [${this.keyword}] is found`;
|
||||||
|
} else {
|
||||||
|
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is not in [" + ${response.data} + "]"`);
|
||||||
|
bean.status = DOWN;
|
||||||
|
bean.msg = `, but keyword [${this.keyword}] is not in [" + ${responseData} + "]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (this.type === "postgres") {
|
} else if (this.type === "postgres") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
@ -614,8 +663,12 @@ class Monitor extends BeanModel {
|
|||||||
if (isImportant) {
|
if (isImportant) {
|
||||||
bean.important = true;
|
bean.important = true;
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] sendNotification`);
|
if (Monitor.isImportantForNotification(isFirstBeat, previousBeat?.status, bean.status)) {
|
||||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
log.debug("monitor", `[${this.name}] sendNotification`);
|
||||||
|
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||||
|
} else {
|
||||||
|
log.debug("monitor", `[${this.name}] will not sendNotification because it is (or was) under maintenance`);
|
||||||
|
}
|
||||||
|
|
||||||
// Reset down count
|
// Reset down count
|
||||||
bean.downCount = 0;
|
bean.downCount = 0;
|
||||||
@ -624,6 +677,8 @@ class Monitor extends BeanModel {
|
|||||||
log.debug("monitor", `[${this.name}] apicache clear`);
|
log.debug("monitor", `[${this.name}] apicache clear`);
|
||||||
apicache.clear();
|
apicache.clear();
|
||||||
|
|
||||||
|
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
bean.important = false;
|
bean.important = false;
|
||||||
|
|
||||||
@ -647,6 +702,8 @@ class Monitor extends BeanModel {
|
|||||||
beatInterval = this.retryInterval;
|
beatInterval = this.retryInterval;
|
||||||
}
|
}
|
||||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||||
|
} else if (bean.status === MAINTENANCE) {
|
||||||
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
|
||||||
} else {
|
} else {
|
||||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||||
}
|
}
|
||||||
@ -857,7 +914,7 @@ class Monitor extends BeanModel {
|
|||||||
-- SUM all uptime duration, also trim off the beat out of time window
|
-- SUM all uptime duration, also trim off the beat out of time window
|
||||||
SUM(
|
SUM(
|
||||||
CASE
|
CASE
|
||||||
WHEN (status = 1)
|
WHEN (status = 1 OR status = 3)
|
||||||
THEN
|
THEN
|
||||||
CASE
|
CASE
|
||||||
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
||||||
@ -928,11 +985,49 @@ class Monitor extends BeanModel {
|
|||||||
// DOWN -> PENDING = this case not exists
|
// DOWN -> PENDING = this case not exists
|
||||||
// DOWN -> DOWN = not important
|
// DOWN -> DOWN = not important
|
||||||
// * DOWN -> UP = important
|
// * DOWN -> UP = important
|
||||||
let isImportant = isFirstBeat ||
|
// MAINTENANCE -> MAINTENANCE = not important
|
||||||
|
// * MAINTENANCE -> UP = important
|
||||||
|
// * MAINTENANCE -> DOWN = important
|
||||||
|
// * DOWN -> MAINTENANCE = important
|
||||||
|
// * UP -> MAINTENANCE = important
|
||||||
|
return isFirstBeat ||
|
||||||
|
(previousBeatStatus === DOWN && currentBeatStatus === MAINTENANCE) ||
|
||||||
|
(previousBeatStatus === UP && currentBeatStatus === MAINTENANCE) ||
|
||||||
|
(previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) ||
|
||||||
|
(previousBeatStatus === MAINTENANCE && currentBeatStatus === UP) ||
|
||||||
|
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
|
||||||
|
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
|
||||||
|
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this beat important for notifications?
|
||||||
|
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
|
||||||
|
* @param {const} previousBeatStatus Status of the previous beat
|
||||||
|
* @param {const} currentBeatStatus Status of the current beat
|
||||||
|
* @returns {boolean} True if is an important beat else false
|
||||||
|
*/
|
||||||
|
static isImportantForNotification(isFirstBeat, previousBeatStatus, currentBeatStatus) {
|
||||||
|
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||||
|
// UP -> PENDING = not important
|
||||||
|
// * UP -> DOWN = important
|
||||||
|
// UP -> UP = not important
|
||||||
|
// PENDING -> PENDING = not important
|
||||||
|
// * PENDING -> DOWN = important
|
||||||
|
// PENDING -> UP = not important
|
||||||
|
// DOWN -> PENDING = this case not exists
|
||||||
|
// DOWN -> DOWN = not important
|
||||||
|
// * DOWN -> UP = important
|
||||||
|
// MAINTENANCE -> MAINTENANCE = not important
|
||||||
|
// MAINTENANCE -> UP = not important
|
||||||
|
// * MAINTENANCE -> DOWN = important
|
||||||
|
// DOWN -> MAINTENANCE = not important
|
||||||
|
// UP -> MAINTENANCE = not important
|
||||||
|
return isFirstBeat ||
|
||||||
|
(previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) ||
|
||||||
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
|
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
|
||||||
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
|
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
|
||||||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
|
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
|
||||||
return isImportant;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1069,6 +1164,26 @@ class Monitor extends BeanModel {
|
|||||||
monitorID
|
monitorID
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if monitor is under maintenance
|
||||||
|
* @param {number} monitorID ID of monitor to check
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Monitor;
|
module.exports = Monitor;
|
||||||
|
@ -3,6 +3,7 @@ const { R } = require("redbean-node");
|
|||||||
const cheerio = require("cheerio");
|
const cheerio = require("cheerio");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const jsesc = require("jsesc");
|
const jsesc = require("jsesc");
|
||||||
|
const Maintenance = require("./maintenance");
|
||||||
|
|
||||||
class StatusPage extends BeanModel {
|
class StatusPage extends BeanModel {
|
||||||
|
|
||||||
@ -37,7 +38,7 @@ class StatusPage extends BeanModel {
|
|||||||
*/
|
*/
|
||||||
static async renderHTML(indexHTML, statusPage) {
|
static async renderHTML(indexHTML, statusPage) {
|
||||||
const $ = cheerio.load(indexHTML);
|
const $ = cheerio.load(indexHTML);
|
||||||
const description155 = statusPage.description?.substring(0, 155);
|
const description155 = statusPage.description?.substring(0, 155) ?? "";
|
||||||
|
|
||||||
$("title").text(statusPage.title);
|
$("title").text(statusPage.title);
|
||||||
$("meta[name=description]").attr("content", description155);
|
$("meta[name=description]").attr("content", description155);
|
||||||
@ -90,6 +91,8 @@ class StatusPage extends BeanModel {
|
|||||||
incident = incident.toPublicJSON();
|
incident = incident.toPublicJSON();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let maintenanceList = await StatusPage.getMaintenanceList(statusPage.id);
|
||||||
|
|
||||||
// Public Group List
|
// Public Group List
|
||||||
const publicGroupList = [];
|
const publicGroupList = [];
|
||||||
const showTags = !!statusPage.show_tags;
|
const showTags = !!statusPage.show_tags;
|
||||||
@ -107,7 +110,8 @@ class StatusPage extends BeanModel {
|
|||||||
return {
|
return {
|
||||||
config: await statusPage.toPublicJSON(),
|
config: await statusPage.toPublicJSON(),
|
||||||
incident,
|
incident,
|
||||||
publicGroupList
|
publicGroupList,
|
||||||
|
maintenanceList,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,6 +270,36 @@ class StatusPage extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of maintenances
|
||||||
|
* @param {number} statusPageId ID of status page to get maintenance for
|
||||||
|
* @returns {Object} Object representing maintenances sanitized for public
|
||||||
|
*/
|
||||||
|
static async getMaintenanceList(statusPageId) {
|
||||||
|
try {
|
||||||
|
const publicMaintenanceList = [];
|
||||||
|
|
||||||
|
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
|
||||||
|
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
|
||||||
|
SELECT maintenance.*
|
||||||
|
FROM maintenance, maintenance_status_page msp, maintenance_timeslot
|
||||||
|
WHERE msp.maintenance_id = maintenance.id
|
||||||
|
AND maintenance_timeslot.maintenance_id = maintenance.id
|
||||||
|
AND msp.status_page_id = ?
|
||||||
|
AND ${activeCondition}
|
||||||
|
ORDER BY maintenance.end_date
|
||||||
|
`, [ statusPageId ]));
|
||||||
|
|
||||||
|
for (const bean of maintenanceBeanList) {
|
||||||
|
publicMaintenanceList.push(await bean.toPublicJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
return publicMaintenanceList;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = StatusPage;
|
module.exports = StatusPage;
|
||||||
|
@ -20,6 +20,11 @@ class Ntfy extends NotificationProvider {
|
|||||||
"priority": notification.ntfyPriority || 4,
|
"priority": notification.ntfyPriority || 4,
|
||||||
"title": "Uptime-Kuma",
|
"title": "Uptime-Kuma",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (notification.ntfyIcon) {
|
||||||
|
data.icon = notification.ntfyIcon;
|
||||||
|
}
|
||||||
|
|
||||||
await axios.post(`${notification.ntfyserverurl}`, data, { headers: headers });
|
await axios.post(`${notification.ntfyserverurl}`, data, { headers: headers });
|
||||||
|
|
||||||
return okMsg;
|
return okMsg;
|
||||||
|
@ -19,26 +19,26 @@ class Pushbullet extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (heartbeatJSON == null) {
|
if (heartbeatJSON == null) {
|
||||||
let testdata = {
|
let data = {
|
||||||
"type": "note",
|
"type": "note",
|
||||||
"title": "Uptime Kuma Alert",
|
"title": "Uptime Kuma Alert",
|
||||||
"body": "Testing Successful.",
|
"body": msg,
|
||||||
};
|
};
|
||||||
await axios.post(pushbulletUrl, testdata, config);
|
await axios.post(pushbulletUrl, data, config);
|
||||||
} else if (heartbeatJSON["status"] === DOWN) {
|
} else if (heartbeatJSON["status"] === DOWN) {
|
||||||
let downdata = {
|
let downData = {
|
||||||
"type": "note",
|
"type": "note",
|
||||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||||
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||||
};
|
};
|
||||||
await axios.post(pushbulletUrl, downdata, config);
|
await axios.post(pushbulletUrl, downData, config);
|
||||||
} else if (heartbeatJSON["status"] === UP) {
|
} else if (heartbeatJSON["status"] === UP) {
|
||||||
let updata = {
|
let upData = {
|
||||||
"type": "note",
|
"type": "note",
|
||||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||||
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||||
};
|
};
|
||||||
await axios.post(pushbulletUrl, updata, config);
|
await axios.post(pushbulletUrl, upData, config);
|
||||||
}
|
}
|
||||||
return okMsg;
|
return okMsg;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
71
server/notification-providers/smseagle.js
Normal file
71
server/notification-providers/smseagle.js
Normal file
@ -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;
|
@ -32,6 +32,7 @@ const RocketChat = require("./notification-providers/rocket-chat");
|
|||||||
const SerwerSMS = require("./notification-providers/serwersms");
|
const SerwerSMS = require("./notification-providers/serwersms");
|
||||||
const Signal = require("./notification-providers/signal");
|
const Signal = require("./notification-providers/signal");
|
||||||
const Slack = require("./notification-providers/slack");
|
const Slack = require("./notification-providers/slack");
|
||||||
|
const SMSEagle = require("./notification-providers/smseagle");
|
||||||
const SMTP = require("./notification-providers/smtp");
|
const SMTP = require("./notification-providers/smtp");
|
||||||
const Squadcast = require("./notification-providers/squadcast");
|
const Squadcast = require("./notification-providers/squadcast");
|
||||||
const Stackfield = require("./notification-providers/stackfield");
|
const Stackfield = require("./notification-providers/stackfield");
|
||||||
@ -89,6 +90,7 @@ class Notification {
|
|||||||
new Signal(),
|
new Signal(),
|
||||||
new SMSManager(),
|
new SMSManager(),
|
||||||
new Slack(),
|
new Slack(),
|
||||||
|
new SMSEagle(),
|
||||||
new SMTP(),
|
new SMTP(),
|
||||||
new Squadcast(),
|
new Squadcast(),
|
||||||
new Stackfield(),
|
new Stackfield(),
|
||||||
|
@ -4,7 +4,7 @@ const { R } = require("redbean-node");
|
|||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
const Monitor = require("../model/monitor");
|
const Monitor = require("../model/monitor");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { UP, DOWN, flipStatus, log } = require("../../src/util");
|
const { UP, MAINTENANCE, DOWN, flipStatus, log } = require("../../src/util");
|
||||||
const StatusPage = require("../model/status_page");
|
const StatusPage = require("../model/status_page");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const { makeBadge } = require("badge-maker");
|
const { makeBadge } = require("badge-maker");
|
||||||
@ -67,6 +67,11 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await Monitor.isUnderMaintenance(monitor.id)) {
|
||||||
|
msg = "Monitor under maintenance";
|
||||||
|
status = MAINTENANCE;
|
||||||
|
}
|
||||||
|
|
||||||
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
||||||
log.debug("router", "PreviousStatus: " + previousStatus);
|
log.debug("router", "PreviousStatus: " + previousStatus);
|
||||||
log.debug("router", "Current Status: " + status);
|
log.debug("router", "Current Status: " + status);
|
||||||
@ -87,7 +92,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
ok: true,
|
ok: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (bean.important) {
|
if (Monitor.isImportantForNotification(isFirstBeat, previousStatus, status)) {
|
||||||
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,12 @@
|
|||||||
*/
|
*/
|
||||||
console.log("Welcome to Uptime Kuma");
|
console.log("Welcome to Uptime Kuma");
|
||||||
|
|
||||||
|
// As the log function need to use dayjs, it should be very top
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
dayjs.extend(require("dayjs/plugin/utc"));
|
||||||
|
dayjs.extend(require("dayjs/plugin/timezone"));
|
||||||
|
dayjs.extend(require("dayjs/plugin/customParseFormat"));
|
||||||
|
|
||||||
// Check Node.js Version
|
// Check Node.js Version
|
||||||
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
|
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
|
||||||
const requiredVersion = 14;
|
const requiredVersion = 14;
|
||||||
@ -33,6 +39,7 @@ log.info("server", "Importing Node libraries");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
|
||||||
log.info("server", "Importing 3rd-party libraries");
|
log.info("server", "Importing 3rd-party libraries");
|
||||||
|
|
||||||
log.debug("server", "Importing express");
|
log.debug("server", "Importing express");
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const expressStaticGzip = require("express-static-gzip");
|
const expressStaticGzip = require("express-static-gzip");
|
||||||
@ -127,6 +134,7 @@ const StatusPage = require("./model/status_page");
|
|||||||
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
|
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
|
||||||
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
|
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
|
||||||
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
|
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
|
||||||
|
const { maintenanceSocketHandler } = require("./socket-handlers/maintenance-socket-handler");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@ -155,6 +163,7 @@ let needSetup = false;
|
|||||||
(async () => {
|
(async () => {
|
||||||
Database.init(args);
|
Database.init(args);
|
||||||
await initDatabase(testMode);
|
await initDatabase(testMode);
|
||||||
|
await server.initAfterDatabaseReady();
|
||||||
|
|
||||||
server.entryPage = await Settings.get("entryPage");
|
server.entryPage = await Settings.get("entryPage");
|
||||||
await StatusPage.loadDomainMappingList();
|
await StatusPage.loadDomainMappingList();
|
||||||
@ -697,6 +706,12 @@ let needSetup = false;
|
|||||||
bean.authMethod = monitor.authMethod;
|
bean.authMethod = monitor.authMethod;
|
||||||
bean.authWorkstation = monitor.authWorkstation;
|
bean.authWorkstation = monitor.authWorkstation;
|
||||||
bean.authDomain = monitor.authDomain;
|
bean.authDomain = monitor.authDomain;
|
||||||
|
bean.grpcUrl = monitor.grpcUrl;
|
||||||
|
bean.grpcProtobuf = monitor.grpcProtobuf;
|
||||||
|
bean.grpcMethod = monitor.grpcMethod;
|
||||||
|
bean.grpcBody = monitor.grpcBody;
|
||||||
|
bean.grpcMetadata = monitor.grpcMetadata;
|
||||||
|
bean.grpcEnableTls = monitor.grpcEnableTls;
|
||||||
bean.radiusUsername = monitor.radiusUsername;
|
bean.radiusUsername = monitor.radiusUsername;
|
||||||
bean.radiusPassword = monitor.radiusPassword;
|
bean.radiusPassword = monitor.radiusPassword;
|
||||||
bean.radiusCalledStationId = monitor.radiusCalledStationId;
|
bean.radiusCalledStationId = monitor.radiusCalledStationId;
|
||||||
@ -1057,10 +1072,15 @@ let needSetup = false;
|
|||||||
socket.on("getSettings", async (callback) => {
|
socket.on("getSettings", async (callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
const data = await getSettings("general");
|
||||||
|
|
||||||
|
if (!data.serverTimezone) {
|
||||||
|
data.serverTimezone = await server.getTimezone();
|
||||||
|
}
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
data: await getSettings("general"),
|
data: data,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -1088,12 +1108,18 @@ let needSetup = false;
|
|||||||
await setSettings("general", data);
|
await setSettings("general", data);
|
||||||
server.entryPage = data.entryPage;
|
server.entryPage = data.entryPage;
|
||||||
|
|
||||||
|
// Also need to apply timezone globally
|
||||||
|
if (data.serverTimezone) {
|
||||||
|
await server.setTimezone(data.serverTimezone);
|
||||||
|
}
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Saved"
|
msg: "Saved"
|
||||||
});
|
});
|
||||||
|
|
||||||
sendInfo(socket);
|
sendInfo(socket);
|
||||||
|
server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
@ -1452,6 +1478,7 @@ let needSetup = false;
|
|||||||
databaseSocketHandler(socket);
|
databaseSocketHandler(socket);
|
||||||
proxySocketHandler(socket);
|
proxySocketHandler(socket);
|
||||||
dockerSocketHandler(socket);
|
dockerSocketHandler(socket);
|
||||||
|
maintenanceSocketHandler(socket);
|
||||||
|
|
||||||
log.debug("server", "added all socket handlers");
|
log.debug("server", "added all socket handlers");
|
||||||
|
|
||||||
@ -1554,6 +1581,7 @@ async function afterLogin(socket, user) {
|
|||||||
socket.join(user.id);
|
socket.join(user.id);
|
||||||
|
|
||||||
let monitorList = await server.sendMonitorList(socket);
|
let monitorList = await server.sendMonitorList(socket);
|
||||||
|
server.sendMaintenanceList(socket);
|
||||||
sendNotificationList(socket);
|
sendNotificationList(socket);
|
||||||
sendProxyList(socket);
|
sendProxyList(socket);
|
||||||
sendDockerHostList(socket);
|
sendDockerHostList(socket);
|
||||||
@ -1699,6 +1727,8 @@ async function shutdownFunction(signal) {
|
|||||||
log.info("server", "Shutdown requested");
|
log.info("server", "Shutdown requested");
|
||||||
log.info("server", "Called signal: " + signal);
|
log.info("server", "Called signal: " + signal);
|
||||||
|
|
||||||
|
await server.stop();
|
||||||
|
|
||||||
log.info("server", "Stopping all monitors");
|
log.info("server", "Stopping all monitors");
|
||||||
for (let id in server.monitorList) {
|
for (let id in server.monitorList) {
|
||||||
let monitor = server.monitorList[id];
|
let monitor = server.monitorList[id];
|
||||||
|
311
server/socket-handlers/maintenance-socket-handler.js
Normal file
311
server/socket-handlers/maintenance-socket-handler.js
Normal file
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -9,6 +9,8 @@ const Database = require("./database");
|
|||||||
const util = require("util");
|
const util = require("util");
|
||||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
||||||
@ -26,6 +28,13 @@ class UptimeKumaServer {
|
|||||||
* @type {{}}
|
* @type {{}}
|
||||||
*/
|
*/
|
||||||
monitorList = {};
|
monitorList = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main maintenance list
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
|
maintenanceList = {};
|
||||||
|
|
||||||
entryPage = "dashboard";
|
entryPage = "dashboard";
|
||||||
app = undefined;
|
app = undefined;
|
||||||
httpServer = undefined;
|
httpServer = undefined;
|
||||||
@ -37,6 +46,8 @@ class UptimeKumaServer {
|
|||||||
*/
|
*/
|
||||||
indexHTML = "";
|
indexHTML = "";
|
||||||
|
|
||||||
|
generateMaintenanceTimeslotsInterval = undefined;
|
||||||
|
|
||||||
static getInstance(args) {
|
static getInstance(args) {
|
||||||
if (UptimeKumaServer.instance == null) {
|
if (UptimeKumaServer.instance == null) {
|
||||||
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
||||||
@ -77,6 +88,16 @@ class UptimeKumaServer {
|
|||||||
this.io = new Server(this.httpServer);
|
this.io = new Server(this.httpServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async initAfterDatabaseReady() {
|
||||||
|
process.env.TZ = await this.getTimezone();
|
||||||
|
dayjs.tz.setDefault(process.env.TZ);
|
||||||
|
log.debug("DEBUG", "Timezone: " + process.env.TZ);
|
||||||
|
log.debug("DEBUG", "Current Time: " + dayjs.tz().format());
|
||||||
|
|
||||||
|
await this.generateMaintenanceTimeslots();
|
||||||
|
this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
async sendMonitorList(socket) {
|
async sendMonitorList(socket) {
|
||||||
let list = await this.getMonitorJSONList(socket.userID);
|
let list = await this.getMonitorJSONList(socket.userID);
|
||||||
this.io.to(socket.userID).emit("monitorList", list);
|
this.io.to(socket.userID).emit("monitorList", list);
|
||||||
@ -104,6 +125,40 @@ class UptimeKumaServer {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send maintenance list to client
|
||||||
|
* @param {Socket} socket Socket.io instance to send to
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async sendMaintenanceList(socket) {
|
||||||
|
return await this.sendMaintenanceListByUserID(socket.userID);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMaintenanceListByUserID(userID) {
|
||||||
|
let list = await this.getMaintenanceJSONList(userID);
|
||||||
|
this.io.to(userID).emit("maintenanceList", list);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of maintenances for the given user.
|
||||||
|
* @param {string} userID - The ID of the user to get maintenances for.
|
||||||
|
* @returns {Promise<Object>} A promise that resolves to an object with maintenance IDs as keys and maintenances objects as values.
|
||||||
|
*/
|
||||||
|
async getMaintenanceJSONList(userID) {
|
||||||
|
let result = {};
|
||||||
|
|
||||||
|
let maintenanceList = await R.find("maintenance", " user_id = ? ORDER BY end_date DESC, title", [
|
||||||
|
userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let maintenance of maintenanceList) {
|
||||||
|
result[maintenance.id] = await maintenance.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write error to log file
|
* Write error to log file
|
||||||
* @param {any} error The error to write
|
* @param {any} error The error to write
|
||||||
@ -147,8 +202,49 @@ class UptimeKumaServer {
|
|||||||
return clientIP.replace(/^.*:/, "");
|
return clientIP.replace(/^.*:/, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTimezone() {
|
||||||
|
let timezone = await Settings.get("serverTimezone");
|
||||||
|
if (timezone) {
|
||||||
|
return timezone;
|
||||||
|
} else if (process.env.TZ) {
|
||||||
|
return process.env.TZ;
|
||||||
|
} else {
|
||||||
|
return dayjs.tz.guess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTimezoneOffset() {
|
||||||
|
return dayjs().format("Z");
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTimezone(timezone) {
|
||||||
|
await Settings.set("serverTimezone", timezone, "general");
|
||||||
|
process.env.TZ = timezone;
|
||||||
|
dayjs.tz.setDefault(timezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateMaintenanceTimeslots() {
|
||||||
|
|
||||||
|
let list = await R.find("maintenance_timeslot", " generated_next = 0 AND start_date <= DATETIME('now') ");
|
||||||
|
|
||||||
|
for (let maintenanceTimeslot of list) {
|
||||||
|
let maintenance = await maintenanceTimeslot.maintenance;
|
||||||
|
await MaintenanceTimeslot.generateTimeslot(maintenance, maintenanceTimeslot.end_date, false);
|
||||||
|
maintenanceTimeslot.generated_next = true;
|
||||||
|
await R.store(maintenanceTimeslot);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
clearTimeout(this.generateMaintenanceTimeslotsInterval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
UptimeKumaServer
|
UptimeKumaServer
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Must be at the end
|
||||||
|
const MaintenanceTimeslot = require("./model/maintenance_timeslot");
|
||||||
|
@ -16,12 +16,15 @@ const postgresConParse = require("pg-connection-string").parse;
|
|||||||
const mysql = require("mysql2");
|
const mysql = require("mysql2");
|
||||||
const { NtlmClient } = require("axios-ntlm");
|
const { NtlmClient } = require("axios-ntlm");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
|
const grpc = require("@grpc/grpc-js");
|
||||||
|
const protojs = require("protobufjs");
|
||||||
const radiusClient = require("node-radius-client");
|
const radiusClient = require("node-radius-client");
|
||||||
const {
|
const {
|
||||||
dictionaries: {
|
dictionaries: {
|
||||||
rfc2865: { file, attributes },
|
rfc2865: { file, attributes },
|
||||||
},
|
},
|
||||||
} = require("node-radius-utils");
|
} = require("node-radius-utils");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
|
||||||
// From ping-lite
|
// From ping-lite
|
||||||
exports.WIN = /^win/.test(process.platform);
|
exports.WIN = /^win/.test(process.platform);
|
||||||
@ -681,3 +684,112 @@ module.exports.send403 = (res, msg = "") => {
|
|||||||
"msg": msg,
|
"msg": msg,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function timeObjectConvertTimezone(obj, timezone, timeObjectToUTC = true) {
|
||||||
|
let offsetString;
|
||||||
|
|
||||||
|
if (timezone) {
|
||||||
|
offsetString = dayjs().tz(timezone).format("Z");
|
||||||
|
} else {
|
||||||
|
offsetString = dayjs().format("Z");
|
||||||
|
}
|
||||||
|
|
||||||
|
let hours = parseInt(offsetString.substring(1, 3));
|
||||||
|
let minutes = parseInt(offsetString.substring(4, 6));
|
||||||
|
|
||||||
|
if (
|
||||||
|
(timeObjectToUTC && offsetString.startsWith("+")) ||
|
||||||
|
(!timeObjectToUTC && offsetString.startsWith("-"))
|
||||||
|
) {
|
||||||
|
hours *= -1;
|
||||||
|
minutes *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.hours += hours;
|
||||||
|
obj.minutes += minutes;
|
||||||
|
|
||||||
|
// Handle out of bound
|
||||||
|
if (obj.minutes < 0) {
|
||||||
|
obj.minutes += 60;
|
||||||
|
obj.hours--;
|
||||||
|
} else if (obj.minutes > 60) {
|
||||||
|
obj.minutes -= 60;
|
||||||
|
obj.hours++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.hours < 0) {
|
||||||
|
obj.hours += 24;
|
||||||
|
} else if (obj.hours > 24) {
|
||||||
|
obj.hours -= 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {string} timezone
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
module.exports.timeObjectToUTC = (obj, timezone = undefined) => {
|
||||||
|
return timeObjectConvertTimezone(obj, timezone, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {string} timezone
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
module.exports.timeObjectToLocal = (obj, timezone = undefined) => {
|
||||||
|
return timeObjectConvertTimezone(obj, timezone, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create gRPC client stib
|
||||||
|
* @param {Object} options from gRPC client
|
||||||
|
*/
|
||||||
|
module.exports.grpcQuery = async (options) => {
|
||||||
|
const { grpcUrl, grpcProtobufData, grpcServiceName, grpcEnableTls, grpcMethod, grpcBody } = options;
|
||||||
|
const protocObject = protojs.parse(grpcProtobufData);
|
||||||
|
const protoServiceObject = protocObject.root.lookupService(grpcServiceName);
|
||||||
|
const Client = grpc.makeGenericClientConstructor({});
|
||||||
|
const credentials = grpcEnableTls ? grpc.credentials.createSsl() : grpc.credentials.createInsecure();
|
||||||
|
const client = new Client(
|
||||||
|
grpcUrl,
|
||||||
|
credentials
|
||||||
|
);
|
||||||
|
const grpcService = protoServiceObject.create(function (method, requestData, cb) {
|
||||||
|
const fullServiceName = method.fullName;
|
||||||
|
const serviceFQDN = fullServiceName.split(".");
|
||||||
|
const serviceMethod = serviceFQDN.pop();
|
||||||
|
const serviceMethodClientImpl = `/${serviceFQDN.slice(1).join(".")}/${serviceMethod}`;
|
||||||
|
log.debug("monitor", `gRPC method ${serviceMethodClientImpl}`);
|
||||||
|
client.makeUnaryRequest(
|
||||||
|
serviceMethodClientImpl,
|
||||||
|
arg => arg,
|
||||||
|
arg => arg,
|
||||||
|
requestData,
|
||||||
|
cb);
|
||||||
|
}, false, false);
|
||||||
|
return new Promise((resolve, _) => {
|
||||||
|
return grpcService[`${grpcMethod}`](JSON.parse(grpcBody), function (err, response) {
|
||||||
|
const responseData = JSON.stringify(response);
|
||||||
|
if (err) {
|
||||||
|
return resolve({
|
||||||
|
code: err.code,
|
||||||
|
errorMessage: err.details,
|
||||||
|
data: ""
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.debug("monitor:", `gRPC response: ${response}`);
|
||||||
|
return resolve({
|
||||||
|
code: 1,
|
||||||
|
errorMessage: "",
|
||||||
|
data: responseData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -22,6 +22,19 @@ textarea.form-control {
|
|||||||
width: 10px;
|
width: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-maintenance {
|
||||||
|
color: white !important;
|
||||||
|
background-color: $maintenance !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-dark {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-maintenance {
|
||||||
|
color: $maintenance !important;
|
||||||
|
}
|
||||||
|
|
||||||
.list-group {
|
.list-group {
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
|
|
||||||
@ -107,6 +120,19 @@ optgroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-normal {
|
||||||
|
$bg-color: #F5F5F5;
|
||||||
|
|
||||||
|
background-color: $bg-color;
|
||||||
|
border-color: $bg-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
$hover-color: darken($bg-color, 3%);
|
||||||
|
background-color: $hover-color;
|
||||||
|
border-color: $hover-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn-warning {
|
.btn-warning {
|
||||||
color: white;
|
color: white;
|
||||||
|
|
||||||
@ -256,6 +282,20 @@ optgroup {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-normal {
|
||||||
|
$bg-color: $dark-header-bg;
|
||||||
|
|
||||||
|
color: $dark-font-color;
|
||||||
|
background-color: $bg-color;
|
||||||
|
border-color: $bg-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
$hover-color: darken($bg-color, 3%);
|
||||||
|
background-color: $hover-color;
|
||||||
|
border-color: $hover-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn-warning {
|
.btn-warning {
|
||||||
color: $dark-font-color2;
|
color: $dark-font-color2;
|
||||||
|
|
||||||
@ -323,6 +363,7 @@ optgroup {
|
|||||||
&.bg-info,
|
&.bg-info,
|
||||||
&.bg-warning,
|
&.bg-warning,
|
||||||
&.bg-danger,
|
&.bg-danger,
|
||||||
|
&.bg-maintenance,
|
||||||
&.bg-light {
|
&.bg-light {
|
||||||
color: $dark-font-color2;
|
color: $dark-font-color2;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
$primary: #5cdd8b;
|
$primary: #5cdd8b;
|
||||||
$danger: #dc3545;
|
$danger: #dc3545;
|
||||||
$warning: #f8a306;
|
$warning: #f8a306;
|
||||||
|
$maintenance: #1747f5;
|
||||||
$link-color: #111;
|
$link-color: #111;
|
||||||
$border-radius: 50rem;
|
$border-radius: 50rem;
|
||||||
|
|
||||||
|
39
src/assets/vue-datepicker.scss
Normal file
39
src/assets/vue-datepicker.scss
Normal file
@ -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;
|
||||||
|
}
|
@ -3,14 +3,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import dayjs from "dayjs";
|
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
|
||||||
import timezone from "dayjs/plugin/timezone"; // dependent on utc plugin
|
|
||||||
import utc from "dayjs/plugin/utc";
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
/** Value of date time */
|
/** Value of date time */
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
v-for="(beat, index) in shortBeatList"
|
v-for="(beat, index) in shortBeatList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="beat"
|
class="beat"
|
||||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
|
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2), 'maintenance' : (beat.status === 3) }"
|
||||||
:style="beatStyle"
|
:style="beatStyle"
|
||||||
:title="getBeatTitle(beat)"
|
:title="getBeatTitle(beat)"
|
||||||
/>
|
/>
|
||||||
@ -211,6 +211,10 @@ export default {
|
|||||||
background-color: $warning;
|
background-color: $warning;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.maintenance {
|
||||||
|
background-color: $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.empty):hover {
|
&:not(.empty):hover {
|
||||||
transition: all ease-in-out 0.15s;
|
transition: all ease-in-out 0.15s;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
@ -42,7 +42,7 @@ export default {
|
|||||||
/** Should the field auto complete */
|
/** Should the field auto complete */
|
||||||
autocomplete: {
|
autocomplete: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: "new-password",
|
||||||
},
|
},
|
||||||
/** Is the input required? */
|
/** Is the input required? */
|
||||||
required: {
|
required: {
|
||||||
|
44
src/components/MaintenanceTime.vue
Normal file
44
src/components/MaintenanceTime.vue
Normal file
@ -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>
|
@ -16,18 +16,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="js">
|
||||||
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
|
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
|
||||||
import "chartjs-adapter-dayjs";
|
import "chartjs-adapter-dayjs";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
|
||||||
import utc from "dayjs/plugin/utc";
|
|
||||||
import { LineChart } from "vue-chart-3";
|
import { LineChart } from "vue-chart-3";
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
import { DOWN, log } from "../util.ts";
|
import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
|
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
|
||||||
@ -163,7 +159,8 @@ export default {
|
|||||||
},
|
},
|
||||||
chartData() {
|
chartData() {
|
||||||
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
||||||
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down, 0 if target is up
|
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down (red color), under maintenance (blue color) or pending (orange color), 0 if target is up
|
||||||
|
let colorData = []; // Color Data for Bar Chart
|
||||||
|
|
||||||
let heartbeatList = this.heartbeatList ||
|
let heartbeatList = this.heartbeatList ||
|
||||||
(this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
|
(this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
|
||||||
@ -185,8 +182,9 @@ export default {
|
|||||||
});
|
});
|
||||||
downData.push({
|
downData.push({
|
||||||
x,
|
x,
|
||||||
y: beat.status === DOWN ? 1 : 0,
|
y: (beat.status === DOWN || beat.status === MAINTENANCE || beat.status === PENDING) ? 1 : 0,
|
||||||
});
|
});
|
||||||
|
colorData.push((beat.status === MAINTENANCE) ? "rgba(23,71,245,0.41)" : ((beat.status === PENDING) ? "rgba(245,182,23,0.41)" : "#DC354568"));
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -205,7 +203,7 @@ export default {
|
|||||||
type: "bar",
|
type: "bar",
|
||||||
data: downData,
|
data: downData,
|
||||||
borderColor: "#00000000",
|
borderColor: "#00000000",
|
||||||
backgroundColor: "#DC354568",
|
backgroundColor: colorData,
|
||||||
yAxisID: "y1",
|
yAxisID: "y1",
|
||||||
barThickness: "flex",
|
barThickness: "flex",
|
||||||
barPercentage: 1,
|
barPercentage: 1,
|
||||||
|
@ -225,4 +225,8 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-maintenance {
|
||||||
|
background-color: $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -26,6 +26,10 @@ export default {
|
|||||||
return "warning";
|
return "warning";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.status === 3) {
|
||||||
|
return "maintenance";
|
||||||
|
}
|
||||||
|
|
||||||
return "secondary";
|
return "secondary";
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -42,6 +46,10 @@ export default {
|
|||||||
return this.$t("Pending");
|
return this.$t("Pending");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.status === 3) {
|
||||||
|
return this.$t("statusMaintenance");
|
||||||
|
}
|
||||||
|
|
||||||
return this.$t("Unknown");
|
return this.$t("Unknown");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -25,6 +25,10 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
uptime() {
|
uptime() {
|
||||||
|
|
||||||
|
if (this.type === "maintenance") {
|
||||||
|
return this.$t("statusMaintenance");
|
||||||
|
}
|
||||||
|
|
||||||
let key = this.monitor.id + "_" + this.type;
|
let key = this.monitor.id + "_" + this.type;
|
||||||
|
|
||||||
if (this.$root.uptimeList[key] !== undefined) {
|
if (this.$root.uptimeList[key] !== undefined) {
|
||||||
@ -35,6 +39,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
color() {
|
color() {
|
||||||
|
if (this.type === "maintenance" || this.monitor.maintenance) {
|
||||||
|
return "maintenance";
|
||||||
|
}
|
||||||
|
|
||||||
if (this.lastHeartBeat.status === 0) {
|
if (this.lastHeartBeat.status === 0) {
|
||||||
return "danger";
|
return "danger";
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
</i18n-t>
|
</i18n-t>
|
||||||
<input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
|
<input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
|
||||||
<label for="clicksendsms-key" class="form-label">{{ $t("API Key") }}</label>
|
<label for="clicksendsms-key" class="form-label">{{ $t("API Key") }}</label>
|
||||||
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="goalert-token" class="form-label">{{ $t("Token") }}</label>
|
<label for="goalert-token" class="form-label">{{ $t("Token") }}</label>
|
||||||
<HiddenInput id="goalert-token" v-model="$parent.notification.goAlertToken" autocomplete="one-time-code" :required="true"></HiddenInput>
|
<HiddenInput id="goalert-token" v-model="$parent.notification.goAlertToken" autocomplete="new-password" :required="true"></HiddenInput>
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
{{ $t("goAlertIntegrationKeyInfo") }}
|
{{ $t("goAlertIntegrationKeyInfo") }}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="gotify-application-token" class="form-label">{{ $t("Application Token") }}</label>
|
<label for="gotify-application-token" class="form-label">{{ $t("Application Token") }}</label>
|
||||||
<HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>
|
<label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="line-channel-access-token" class="form-label">{{ $t("Channel access token") }}</label>
|
<label for="line-channel-access-token" class="form-label">{{ $t("Channel access token") }}</label>
|
||||||
<HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
|
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
|
||||||
<b>{{ $t("Basic Settings") }}</b>
|
<b>{{ $t("Basic Settings") }}</b>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="access-token" class="form-label">{{ $t("Access Token") }}</label><span style="color: red;"><sup>*</sup></span>
|
<label for="access-token" class="form-label">{{ $t("Access Token") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||||
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="one-time-code" :maxlength="500"></HiddenInput>
|
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="new-password" :maxlength="500"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
|
@ -27,6 +27,10 @@
|
|||||||
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
|
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ntfy-icon" class="form-label">{{ $t("IconUrl") }}</label>
|
||||||
|
<input id="ntfy-icon" v-model="$parent.notification.ntfyIcon" type="text" class="form-control">
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="octopush-key" class="form-label">{{ $t("octopushAPIKey") }}</label>
|
<label for="octopush-key" class="form-label">{{ $t("octopushAPIKey") }}</label>
|
||||||
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
<label for="octopush-login" class="form-label">{{ $t("octopushLogin") }}</label>
|
<label for="octopush-login" class="form-label">{{ $t("octopushLogin") }}</label>
|
||||||
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
|
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<label for="promosms-login" class="form-label">{{ $t("promosmsLogin") }}</label>
|
<label for="promosms-login" class="form-label">{{ $t("promosmsLogin") }}</label>
|
||||||
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
|
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
|
||||||
<label for="promosms-key" class="form-label">{{ $t("promosmsPassword") }}</label>
|
<label for="promosms-key" class="form-label">{{ $t("promosmsPassword") }}</label>
|
||||||
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="promosms-type-sms" class="form-label">{{ $t("SMS Type") }}</label>
|
<label for="promosms-type-sms" class="form-label">{{ $t("SMS Type") }}</label>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushdeer-key" class="form-label">{{ $t("PushDeer Key") }}</label>
|
<label for="pushdeer-key" class="form-label">{{ $t("PushDeer Key") }}</label>
|
||||||
<HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="one-time-code" placeholder="PDUxxxx"></HiddenInput>
|
<HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="new-password" placeholder="PDUxxxx"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushbullet-access-token" class="form-label">{{ $t("Access Token") }}</label>
|
<label for="pushbullet-access-token" class="form-label">{{ $t("Access Token") }}</label>
|
||||||
<HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushover-user" class="form-label">{{ $t("User Key") }}<span style="color: red;"><sup>*</sup></span></label>
|
<label for="pushover-user" class="form-label">{{ $t("User Key") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
<HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
<label for="pushover-app-token" class="form-label">{{ $t("Application Token") }}<span style="color: red;"><sup>*</sup></span></label>
|
<label for="pushover-app-token" class="form-label">{{ $t("Application Token") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
<HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
<label for="pushover-device" class="form-label">{{ $t("Device") }}</label>
|
<label for="pushover-device" class="form-label">{{ $t("Device") }}</label>
|
||||||
<input id="pushover-device" v-model="$parent.notification.pushoverdevice" type="text" class="form-control">
|
<input id="pushover-device" v-model="$parent.notification.pushoverdevice" type="text" class="form-control">
|
||||||
<label for="pushover-device" class="form-label">{{ $t("Message Title") }}</label>
|
<label for="pushover-device" class="form-label">{{ $t("Message Title") }}</label>
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushy-app-token" class="form-label">{{ $t("pushyAPIKey") }}</label>
|
<label for="pushy-app-token" class="form-label">{{ $t("pushyAPIKey") }}</label>
|
||||||
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
|
<label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||||
|
40
src/components/notifications/SMSEagle.vue
Normal file
40
src/components/notifications/SMSEagle.vue
Normal file
@ -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>
|
@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">{{ $t("Password") }}</label>
|
<label for="password" class="form-label">{{ $t("Password") }}</label>
|
||||||
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="false" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="false" autocomplete="new-password"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="serverchan-sendkey" class="form-label">{{ $t("SendKey") }}</label>
|
<label for="serverchan-sendkey" class="form-label">{{ $t("SendKey") }}</label>
|
||||||
<HiddenInput id="serverchan-sendkey" v-model="$parent.notification.serverChanSendKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="serverchan-sendkey" v-model="$parent.notification.serverChanSendKey" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="serwersms-key" class="form-label">{{ $t('serwersmsAPIPassword') }}</label>
|
<label for="serwersms-key" class="form-label">{{ $t('serwersmsAPIPassword') }}</label>
|
||||||
<HiddenInput id="serwersms-key" v-model="$parent.notification.serwersmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="serwersms-key" v-model="$parent.notification.serwersmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="serwersms-phone-number" class="form-label">{{ $t("serwersmsPhoneNumber") }}</label>
|
<label for="serwersms-phone-number" class="form-label">{{ $t("serwersmsPhoneNumber") }}</label>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="push-api-key" class="form-label">{{ $t("API Key") }}</label>
|
<label for="push-api-key" class="form-label">{{ $t("API Key") }}</label>
|
||||||
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
|
<label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
|
||||||
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
<i18n-t tag="div" keypath="wayToGetTelegramToken" class="form-text">
|
<i18n-t tag="div" keypath="wayToGetTelegramToken" class="form-text">
|
||||||
<a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>
|
<a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
|
@ -33,6 +33,7 @@ import Signal from "./Signal.vue";
|
|||||||
import SMSManager from "./SMSManager.vue";
|
import SMSManager from "./SMSManager.vue";
|
||||||
import Slack from "./Slack.vue";
|
import Slack from "./Slack.vue";
|
||||||
import Squadcast from "./Squadcast.vue";
|
import Squadcast from "./Squadcast.vue";
|
||||||
|
import SMSEagle from "./SMSEagle.vue";
|
||||||
import Stackfield from "./Stackfield.vue";
|
import Stackfield from "./Stackfield.vue";
|
||||||
import STMP from "./SMTP.vue";
|
import STMP from "./SMTP.vue";
|
||||||
import Teams from "./Teams.vue";
|
import Teams from "./Teams.vue";
|
||||||
@ -83,6 +84,7 @@ const NotificationFormList = {
|
|||||||
"SMSManager": SMSManager,
|
"SMSManager": SMSManager,
|
||||||
"slack": Slack,
|
"slack": Slack,
|
||||||
"squadcast": Squadcast,
|
"squadcast": Squadcast,
|
||||||
|
"SMSEagle": SMSEagle,
|
||||||
"smtp": STMP,
|
"smtp": STMP,
|
||||||
"stackfield": Stackfield,
|
"stackfield": Stackfield,
|
||||||
"teams": Teams,
|
"teams": Teams,
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<form class="my-4" @submit.prevent="saveGeneral">
|
<form class="my-4" autocomplete="off" @submit.prevent="saveGeneral">
|
||||||
<!-- Timezone -->
|
<!-- Client side Timezone -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="timezone" class="form-label">
|
<label for="timezone" class="form-label">
|
||||||
{{ $t("Timezone") }}
|
{{ $t("Display Timezone") }}
|
||||||
</label>
|
</label>
|
||||||
<select id="timezone" v-model="$root.userTimezone" class="form-select">
|
<select id="timezone" v-model="$root.userTimezone" class="form-select">
|
||||||
<option value="auto">
|
<option value="auto">
|
||||||
@ -20,6 +20,23 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Server Timezone -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="timezone" class="form-label">
|
||||||
|
{{ $t("Server Timezone") }}
|
||||||
|
</label>
|
||||||
|
<select id="timezone" v-model="settings.serverTimezone" class="form-select">
|
||||||
|
<option value="UTC">UTC</option>
|
||||||
|
<option
|
||||||
|
v-for="(timezone, index) in timezoneList"
|
||||||
|
:key="index"
|
||||||
|
:value="timezone.value"
|
||||||
|
>
|
||||||
|
{{ timezone.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Search Engine -->
|
<!-- Search Engine -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
@ -105,6 +122,7 @@
|
|||||||
name="primaryBaseURL"
|
name="primaryBaseURL"
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
pattern="https?://.+"
|
pattern="https?://.+"
|
||||||
|
autocomplete="new-password"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryBaseURL">
|
<button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryBaseURL">
|
||||||
{{ $t("Auto Get") }}
|
{{ $t("Auto Get") }}
|
||||||
@ -122,7 +140,7 @@
|
|||||||
<HiddenInput
|
<HiddenInput
|
||||||
id="steamAPIKey"
|
id="steamAPIKey"
|
||||||
v-model="settings.steamAPIKey"
|
v-model="settings.steamAPIKey"
|
||||||
autocomplete="one-time-code"
|
autocomplete="new-password"
|
||||||
/>
|
/>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
{{ $t("steamApiKeyDescription") }}
|
{{ $t("steamApiKeyDescription") }}
|
||||||
@ -145,11 +163,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import HiddenInput from "../../components/HiddenInput.vue";
|
import HiddenInput from "../../components/HiddenInput.vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
|
||||||
import timezone from "dayjs/plugin/timezone";
|
|
||||||
import { timezoneList } from "../../util-frontend";
|
import { timezoneList } from "../../util-frontend";
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
<HiddenInput
|
<HiddenInput
|
||||||
id="cloudflareTunnelToken"
|
id="cloudflareTunnelToken"
|
||||||
v-model="cloudflareTunnelToken"
|
v-model="cloudflareTunnelToken"
|
||||||
autocomplete="one-time-code"
|
autocomplete="new-password"
|
||||||
:readonly="running"
|
:readonly="running"
|
||||||
/>
|
/>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
|
@ -41,6 +41,9 @@ import {
|
|||||||
faUndo,
|
faUndo,
|
||||||
faPlusCircle,
|
faPlusCircle,
|
||||||
faAngleDown,
|
faAngleDown,
|
||||||
|
faWrench,
|
||||||
|
faHeartbeat,
|
||||||
|
faFilter,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
@ -82,6 +85,9 @@ library.add(
|
|||||||
faPlusCircle,
|
faPlusCircle,
|
||||||
faAngleDown,
|
faAngleDown,
|
||||||
faLink,
|
faLink,
|
||||||
|
faWrench,
|
||||||
|
faHeartbeat,
|
||||||
|
faFilter,
|
||||||
);
|
);
|
||||||
|
|
||||||
export { FontAwesomeIcon };
|
export { FontAwesomeIcon };
|
||||||
|
@ -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. 😏
|
||||||
|
@ -582,4 +582,53 @@ export default {
|
|||||||
goAlert: "GoAlert",
|
goAlert: "GoAlert",
|
||||||
backupOutdatedWarning: "Отпаднало: Тъй като са добавени много функции, тази опция за архивиране не е достатъчно поддържана и не може да генерира или възстанови пълен архив.",
|
backupOutdatedWarning: "Отпаднало: Тъй като са добавени много функции, тази опция за архивиране не е достатъчно поддържана и не може да генерира или възстанови пълен архив.",
|
||||||
backupRecommend: "Моля, архивирайте дяла или папката (./data/) директно вместо това.",
|
backupRecommend: "Моля, архивирайте дяла или папката (./data/) директно вместо това.",
|
||||||
|
Maintenance: "Поддръжка",
|
||||||
|
statusMaintenance: "Поддръжка",
|
||||||
|
"Schedule maintenance": "Планиране на поддръжка",
|
||||||
|
"Affected Monitors": "Засегнати монитори",
|
||||||
|
"Pick Affected Monitors...": "Изберете засегнати монитори...",
|
||||||
|
"Start of maintenance": "Стартирай поддръжка",
|
||||||
|
"All Status Pages": "Всички статус страници",
|
||||||
|
"Select status pages...": "Изберете статус страници...",
|
||||||
|
recurringIntervalMessage: "Изпълнявай ежедневно | Изпълнявай всеки {0} дни",
|
||||||
|
affectedMonitorsDescription: "Изберете монитори, засегнати от текущата поддръжка",
|
||||||
|
affectedStatusPages: "Покажи това съобщение за поддръжка на избрани статус страници",
|
||||||
|
atLeastOneMonitor: "Изберете поне един засегнат монитор",
|
||||||
|
deleteMaintenanceMsg: "Сигурни ли сте, че желаете да изтриете тази поддръжка?",
|
||||||
|
Optional: "По желание",
|
||||||
|
squadcast: "Squadcast",
|
||||||
|
SendKey: "SendKey",
|
||||||
|
"SMSManager API Docs": "SMSManager API Документация ",
|
||||||
|
"Gateway Type": "Тип на шлюза",
|
||||||
|
SMSManager: "SMSManager",
|
||||||
|
"You can divide numbers with": "Може да разделяте числата с",
|
||||||
|
or: "или",
|
||||||
|
recurringInterval: "Интервал",
|
||||||
|
Recurring: "Повтаряне",
|
||||||
|
strategyManual: "Активен/Неактивен ръчно",
|
||||||
|
warningTimezone: "Използва се часовата зона на сървъра",
|
||||||
|
weekdayShortMon: "Пон",
|
||||||
|
weekdayShortTue: "Вт",
|
||||||
|
weekdayShortWed: "Ср",
|
||||||
|
weekdayShortThu: "Чет",
|
||||||
|
weekdayShortFri: "Пет",
|
||||||
|
weekdayShortSat: "Съб",
|
||||||
|
weekdayShortSun: "Нед",
|
||||||
|
dayOfWeek: "Ден",
|
||||||
|
dayOfMonth: "Дата",
|
||||||
|
lastDay: "Последен ден",
|
||||||
|
lastDay1: "Последен ден от месеца",
|
||||||
|
lastDay2: "2-ри последен ден на месеца",
|
||||||
|
lastDay3: "3-ти последен ден на месеца",
|
||||||
|
lastDay4: "4-ти последен ден на месеца",
|
||||||
|
"No Maintenance": "Няма поддръжка",
|
||||||
|
pauseMaintenanceMsg: "Сигурни ли сте, че желаете да направите пауза?",
|
||||||
|
"maintenanceStatus-under-maintenance": "В режим подръжка",
|
||||||
|
"maintenanceStatus-inactive": "Неактивен",
|
||||||
|
"maintenanceStatus-scheduled": "Планиран",
|
||||||
|
"maintenanceStatus-ended": "Прилючена",
|
||||||
|
"maintenanceStatus-unknown": "Неизвестен",
|
||||||
|
"Display Timezone": "Покажи часова зона",
|
||||||
|
"Server Timezone": "Часова зона на сървъра",
|
||||||
|
statusPageMaintenanceEndDate: "Край",
|
||||||
};
|
};
|
||||||
|
@ -576,4 +576,59 @@ export default {
|
|||||||
"Then choose an action, for example switch the scene to where an RGB light is red.": "Dann eine Aktion wählen, zum Beispiel eine Scene wählen in der ein RGB Licht rot ist.",
|
"Then choose an action, for example switch the scene to where an RGB light is red.": "Dann eine Aktion wählen, zum Beispiel eine Scene wählen in der ein RGB Licht rot ist.",
|
||||||
"Frontend Version": "Frontend Version",
|
"Frontend Version": "Frontend Version",
|
||||||
"Frontend Version do not match backend version!": "Die Frontend Version stimmt nicht mit der backend version überein!",
|
"Frontend Version do not match backend version!": "Die Frontend Version stimmt nicht mit der backend version überein!",
|
||||||
|
Maintenance: "Wartung",
|
||||||
|
statusMaintenance: "Wartung",
|
||||||
|
"Schedule maintenance": "Geplante Wartung",
|
||||||
|
"Affected Monitors": "Betroffene Monitore",
|
||||||
|
"Pick Affected Monitors...": "Wähle betroffene Monitore...",
|
||||||
|
"Start of maintenance": "Beginn der Wartung",
|
||||||
|
"All Status Pages": "Alle Status Seiten",
|
||||||
|
"Select status pages...": "Wähle Status Seiten...",
|
||||||
|
recurringIntervalMessage: "einmal pro Tag ausgeführt | Wird alle {0} Tage ausgführt",
|
||||||
|
affectedMonitorsDescription: "Wähle alle Monitore die von der Wartung betroffen sind",
|
||||||
|
affectedStatusPages: "Zeige diese Nachricht auf ausgewählten Status Seiten",
|
||||||
|
atLeastOneMonitor: "Wähle mindestens einen Monitor",
|
||||||
|
deleteMaintenanceMsg: "Möchtest du diese Wartung löschen?",
|
||||||
|
"Base URL": "Basis URL",
|
||||||
|
goAlertInfo: "GoAlert ist eine Open-Source Applikation für Rufbereitschaft Planung, automaitsche Esklaltion und Benachrichtigung (z.B. SMS oder Telefonanrufe). Beauftragen Sie automatisch die richtige Person, auf die richtige Art und Weise und zum richtigen Zeitpunkt! {0}",
|
||||||
|
goAlertIntegrationKeyInfo: "Bekomm einenen gernerischen API Schlüssel in folgeden Format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\". Normalerweise der Wert des Token aus der URL.",
|
||||||
|
goAlert: "GoAlert",
|
||||||
|
backupOutdatedWarning: "Veraltet: Eine menge Neuerungen sind eingeflossen und diese Funktion wurde etwas vernachlässigt worden. Es kann kein vollständiges Backup erstellt oder eingspielt werden.",
|
||||||
|
backupRecommend: "Bitte Backup das Volume oder den Ordner (./ data /) selbst.",
|
||||||
|
Optional: "Optional",
|
||||||
|
squadcast: "Squadcast",
|
||||||
|
SendKey: "SendKey",
|
||||||
|
"SMSManager API Docs": "SMSManager API Dokumente",
|
||||||
|
"Gateway Type": "Gateway Type",
|
||||||
|
SMSManager: "SMSManager",
|
||||||
|
"You can divide numbers with": "Du kannst Zahlen teilen mit",
|
||||||
|
or: "oder",
|
||||||
|
recurringInterval: "Intervall",
|
||||||
|
Recurring: "Wiederkehrend",
|
||||||
|
strategyManual: "Active/Inactive Manually",
|
||||||
|
warningTimezone: "Es wird die Zeitzone des Servers genutzt",
|
||||||
|
weekdayShortMon: "Mo",
|
||||||
|
weekdayShortTue: "Di",
|
||||||
|
weekdayShortWed: "Mi",
|
||||||
|
weekdayShortThu: "Do",
|
||||||
|
weekdayShortFri: "Fr",
|
||||||
|
weekdayShortSat: "Sa",
|
||||||
|
weekdayShortSun: "So",
|
||||||
|
dayOfWeek: "Tag der Woche",
|
||||||
|
dayOfMonth: "Tag im Monat",
|
||||||
|
lastDay: "Letzter Tag",
|
||||||
|
lastDay1: "Letzter Tag im Monat",
|
||||||
|
lastDay2: "Vorletzer Tag im Monat",
|
||||||
|
lastDay3: "3. letzter Tag im Monat",
|
||||||
|
lastDay4: "4. letzter Tag im Monat",
|
||||||
|
"No Maintenance": "Keine Wartung",
|
||||||
|
pauseMaintenanceMsg: "Möchtest du wirklich pausieren?",
|
||||||
|
"maintenanceStatus-under-maintenance": "Unter Wartung",
|
||||||
|
"maintenanceStatus-inactive": "Inaktiv",
|
||||||
|
"maintenanceStatus-scheduled": "Geplant",
|
||||||
|
"maintenanceStatus-ended": "Ende",
|
||||||
|
"maintenanceStatus-unknown": "Unbekannt",
|
||||||
|
"Display Timezone": "Zeitzone anzeigen",
|
||||||
|
"Server Timezone": "Server Zeitzone",
|
||||||
|
statusPageMaintenanceEndDate: "Ende",
|
||||||
};
|
};
|
||||||
|
@ -8,12 +8,27 @@ export default {
|
|||||||
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
|
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
|
||||||
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
|
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
|
||||||
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
||||||
|
enableGRPCTls: "Allow to send gRPC request with TLS connection",
|
||||||
|
grpcMethodDescription: "Method name is convert to cammelCase format such as sayHello, check, etc.",
|
||||||
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
|
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
|
||||||
|
Maintenance: "Maintenance",
|
||||||
|
statusMaintenance: "Maintenance",
|
||||||
|
"Schedule maintenance": "Schedule maintenance",
|
||||||
|
"Affected Monitors": "Affected Monitors",
|
||||||
|
"Pick Affected Monitors...": "Pick Affected Monitors...",
|
||||||
|
"Start of maintenance": "Start of maintenance",
|
||||||
|
"All Status Pages": "All Status Pages",
|
||||||
|
"Select status pages...": "Select status pages...",
|
||||||
|
recurringIntervalMessage: "Run once every day | Run once every {0} days",
|
||||||
|
affectedMonitorsDescription: "Select monitors that are affected by current maintenance",
|
||||||
|
affectedStatusPages: "Show this maintenance message on selected status pages",
|
||||||
|
atLeastOneMonitor: "Select at least one affected monitor",
|
||||||
passwordNotMatchMsg: "The repeat password does not match.",
|
passwordNotMatchMsg: "The repeat password does not match.",
|
||||||
notificationDescription: "Notifications must be assigned to a monitor to function.",
|
notificationDescription: "Notifications must be assigned to a monitor to function.",
|
||||||
keywordDescription: "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
|
keywordDescription: "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
|
||||||
pauseDashboardHome: "Pause",
|
pauseDashboardHome: "Pause",
|
||||||
deleteMonitorMsg: "Are you sure want to delete this monitor?",
|
deleteMonitorMsg: "Are you sure want to delete this monitor?",
|
||||||
|
deleteMaintenanceMsg: "Are you sure want to delete this maintenance?",
|
||||||
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?",
|
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?",
|
||||||
dnsPortDescription: "DNS server port. Defaults to 53. You can change the port at any time.",
|
dnsPortDescription: "DNS server port. Defaults to 53. You can change the port at any time.",
|
||||||
resolverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.",
|
resolverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.",
|
||||||
@ -365,6 +380,16 @@ export default {
|
|||||||
serwersmsAPIPassword: "API Password",
|
serwersmsAPIPassword: "API Password",
|
||||||
serwersmsPhoneNumber: "Phone number",
|
serwersmsPhoneNumber: "Phone number",
|
||||||
serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
|
serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
|
||||||
|
smseagle: "SMSEagle",
|
||||||
|
smseagleTo: "Phone number(s)",
|
||||||
|
smseagleGroup: "Phonebook group name(s)",
|
||||||
|
smseagleContact: "Phonebook contact name(s)",
|
||||||
|
smseagleRecipientType: "Recipient type",
|
||||||
|
smseagleRecipient: "Recipient(s) (multiple must be separated with comma)",
|
||||||
|
smseagleToken: "API Access token",
|
||||||
|
smseagleUrl: "Your SMSEagle device URL",
|
||||||
|
smseagleEncoding: "Send as Unicode",
|
||||||
|
smseaglePriority: "Message priority (0-9, default = 0)",
|
||||||
stackfield: "Stackfield",
|
stackfield: "Stackfield",
|
||||||
Customize: "Customize",
|
Customize: "Customize",
|
||||||
"Custom Footer": "Custom Footer",
|
"Custom Footer": "Custom Footer",
|
||||||
@ -590,4 +615,33 @@ export default {
|
|||||||
SMSManager: "SMSManager",
|
SMSManager: "SMSManager",
|
||||||
"You can divide numbers with": "You can divide numbers with",
|
"You can divide numbers with": "You can divide numbers with",
|
||||||
"or": "or",
|
"or": "or",
|
||||||
|
recurringInterval: "Interval",
|
||||||
|
"Recurring": "Recurring",
|
||||||
|
strategyManual: "Active/Inactive Manually",
|
||||||
|
warningTimezone: "It is using the server's timezone",
|
||||||
|
weekdayShortMon: "Mon",
|
||||||
|
weekdayShortTue: "Tue",
|
||||||
|
weekdayShortWed: "Wed",
|
||||||
|
weekdayShortThu: "Thu",
|
||||||
|
weekdayShortFri: "Fri",
|
||||||
|
weekdayShortSat: "Sat",
|
||||||
|
weekdayShortSun: "Sun",
|
||||||
|
dayOfWeek: "Day of Week",
|
||||||
|
dayOfMonth: "Day of Month",
|
||||||
|
lastDay: "Last Day",
|
||||||
|
lastDay1: "Last Day of Month",
|
||||||
|
lastDay2: "2nd Last Day of Month",
|
||||||
|
lastDay3: "3rd Last Day of Month",
|
||||||
|
lastDay4: "4th Last Day of Month",
|
||||||
|
"No Maintenance": "No Maintenance",
|
||||||
|
pauseMaintenanceMsg: "Are you sure want to pause?",
|
||||||
|
"maintenanceStatus-under-maintenance": "Under Maintenance",
|
||||||
|
"maintenanceStatus-inactive": "Inactive",
|
||||||
|
"maintenanceStatus-scheduled": "Scheduled",
|
||||||
|
"maintenanceStatus-ended": "Ended",
|
||||||
|
"maintenanceStatus-unknown": "Unknown",
|
||||||
|
"Display Timezone": "Display Timezone",
|
||||||
|
"Server Timezone": "Server Timezone",
|
||||||
|
statusPageMaintenanceEndDate: "End",
|
||||||
|
IconUrl: "Icon URL",
|
||||||
};
|
};
|
||||||
|
@ -531,4 +531,57 @@ export default {
|
|||||||
backupRecommend: "Veuillez sauvegarder le volume ou le dossier de données (./data/) directement à la place.",
|
backupRecommend: "Veuillez sauvegarder le volume ou le dossier de données (./data/) directement à la place.",
|
||||||
Optional: "Optionnel",
|
Optional: "Optionnel",
|
||||||
squadcast: "Squadcast",
|
squadcast: "Squadcast",
|
||||||
|
Maintenance: "Maintenance",
|
||||||
|
statusMaintenance: "Maintenance",
|
||||||
|
"Schedule maintenance": "Planifier la maintenance",
|
||||||
|
"Affected Monitors": "Moniteurs concernés",
|
||||||
|
"Pick Affected Monitors...": "Sélectionnez les moniteurs concernés...",
|
||||||
|
"Start of maintenance": "Début de la maintenance",
|
||||||
|
"All Status Pages": "Toutes les pages d'état",
|
||||||
|
"Select status pages...": "Sélectionnez les pages d'état...",
|
||||||
|
recurringIntervalMessage: "Exécuter une fois par jour | Exécuter une fois tous les {0} jours",
|
||||||
|
affectedMonitorsDescription: "Sélectionnez les moniteurs concernés par la maintenance en cours",
|
||||||
|
affectedStatusPages: "Afficher ce message de maintenance sur les pages d'état sélectionnées",
|
||||||
|
atLeastOneMonitor: "Sélectionnez au moins un moniteur concerné",
|
||||||
|
deleteMaintenanceMsg: "Voulez-vous vraiment supprimer cette maintenance ?",
|
||||||
|
pushyAPIKey: "Clé API secrète",
|
||||||
|
pushyToken: "Jeton d'appareil",
|
||||||
|
"You can divide numbers with": "Vous pouvez diviser des nombres avec",
|
||||||
|
or: "ou",
|
||||||
|
recurringInterval: "Intervalle",
|
||||||
|
Recurring: "Récurrent",
|
||||||
|
"Single Maintenance Window": "Fenêtre de maintenance unique",
|
||||||
|
"Maintenance Time Window of a Day": "Fenêtre de temps de maintenance",
|
||||||
|
"Effective Date Range": "Plage de dates d'effet",
|
||||||
|
strategyManual: "activer/desactiver manuellement",
|
||||||
|
warningTimezone: "Il utilise le fuseau horaire du serveur",
|
||||||
|
weekdayShortMon: "Lun",
|
||||||
|
weekdayShortTue: "Mar",
|
||||||
|
weekdayShortWed: "Mer",
|
||||||
|
weekdayShortThu: "Jeu",
|
||||||
|
weekdayShortFri: "Ven",
|
||||||
|
weekdayShortSat: "Sam",
|
||||||
|
weekdayShortSun: "Dim",
|
||||||
|
dayOfWeek: "Jour de la semaine",
|
||||||
|
dayOfMonth: "Jour du mois",
|
||||||
|
lastDay: "Dernier jour",
|
||||||
|
lastDay1: "Dernier jour du mois",
|
||||||
|
lastDay2: "2ème dernier jour du mois",
|
||||||
|
lastDay3: "3ème dernier jour du mois",
|
||||||
|
lastDay4: "4ème dernier jour du mois",
|
||||||
|
"No Maintenance": "Aucune Maintenance",
|
||||||
|
pauseMaintenanceMsg: "Voulez-vous vraiment mettre en pause ?",
|
||||||
|
"maintenanceStatus-under-maintenance": "En maintenance",
|
||||||
|
"maintenanceStatus-inactive": "Inactif",
|
||||||
|
"maintenanceStatus-scheduled": "Programmé",
|
||||||
|
"maintenanceStatus-ended": "Terminé",
|
||||||
|
"maintenanceStatus-unknown": "Inconnue",
|
||||||
|
"Display Timezone": "Afficher le fuseau horaire",
|
||||||
|
"Server Timezone": "Fuseau horaire du serveur",
|
||||||
|
"Date and Time": "Date et heure",
|
||||||
|
"DateTime Range": "Plage de dates et d'heures",
|
||||||
|
Strategy: "Stratégie",
|
||||||
|
statusPageMaintenanceEndDate: "Fin",
|
||||||
|
"Free Mobile User Identifier": "Identifiant d'utilisateur Free Mobile",
|
||||||
|
"Free Mobile API Key": "Clé API Free Mobile",
|
||||||
};
|
};
|
||||||
|
@ -27,8 +27,8 @@ export default {
|
|||||||
confirmImportMsg: "Apakah Anda yakin untuk mengimpor cadangan? Pastikan Anda telah memilih opsi impor yang tepat.",
|
confirmImportMsg: "Apakah Anda yakin untuk mengimpor cadangan? Pastikan Anda telah memilih opsi impor yang tepat.",
|
||||||
twoFAVerifyLabel: "Silakan ketik token Anda untuk memverifikasi bahwa 2FA berfungsi",
|
twoFAVerifyLabel: "Silakan ketik token Anda untuk memverifikasi bahwa 2FA berfungsi",
|
||||||
tokenValidSettingsMsg: "Token benar! Anda sekarang dapat menyimpan pengaturan 2FA.",
|
tokenValidSettingsMsg: "Token benar! Anda sekarang dapat menyimpan pengaturan 2FA.",
|
||||||
confirmEnableTwoFAMsg: "Apakah anda yakin ingin mengaktifkan 2FA?",
|
confirmEnableTwoFAMsg: "Apakah Anda yakin ingin mengaktifkan 2FA?",
|
||||||
confirmDisableTwoFAMsg: "Apakah anda yakin ingin menonaktifkan 2FA?",
|
confirmDisableTwoFAMsg: "Apakah Anda yakin ingin menonaktifkan 2FA?",
|
||||||
Settings: "Pengaturan",
|
Settings: "Pengaturan",
|
||||||
Dashboard: "Dasbor",
|
Dashboard: "Dasbor",
|
||||||
"New Update": "Pembaruan Baru",
|
"New Update": "Pembaruan Baru",
|
||||||
@ -126,7 +126,7 @@ export default {
|
|||||||
"Resolver Server": "Resolver Server",
|
"Resolver Server": "Resolver Server",
|
||||||
"Resource Record Type": "Resource Record Type",
|
"Resource Record Type": "Resource Record Type",
|
||||||
"Last Result": "Hasil Terakhir",
|
"Last Result": "Hasil Terakhir",
|
||||||
"Create your admin account": "Buat akun admin anda",
|
"Create your admin account": "Buat akun admin Anda",
|
||||||
"Repeat Password": "Ulangi Sandi",
|
"Repeat Password": "Ulangi Sandi",
|
||||||
"Import Backup": "Impor Cadangan",
|
"Import Backup": "Impor Cadangan",
|
||||||
"Export Backup": "Ekspor Cadangan",
|
"Export Backup": "Ekspor Cadangan",
|
||||||
@ -568,7 +568,7 @@ export default {
|
|||||||
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Token Akses Berumur Panjang dapat dibuat dengan mengklik nama profil Anda (kiri bawah) dan menggulir ke bawah lalu klik Buat Token. ",
|
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Token Akses Berumur Panjang dapat dibuat dengan mengklik nama profil Anda (kiri bawah) dan menggulir ke bawah lalu klik Buat Token. ",
|
||||||
"Notification Service": "Layanan Pemberitahuan",
|
"Notification Service": "Layanan Pemberitahuan",
|
||||||
"default: notify all devices": "bawaan: notifikasi seluruh perangkat",
|
"default: notify all devices": "bawaan: notifikasi seluruh perangkat",
|
||||||
"A listof Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Daftar Layanan Pemberitahuan dapat ditemukan di Home Assistant pada \"Developer Tools > Services\" cari \"notification\" lalu cari nama perangkat Anda.",
|
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Daftar Layanan Pemberitahuan dapat ditemukan di Home Assistant pada \"Developer Tools > Services\" cari \"notification\" lalu cari nama perangkat Anda.",
|
||||||
"Automations can optionally be triggered in Home Assistant:": "Otomatisasi dapat dipicu secara opsional di Home Assistant:",
|
"Automations can optionally be triggered in Home Assistant:": "Otomatisasi dapat dipicu secara opsional di Home Assistant:",
|
||||||
"Trigger type:": "Tipe Trigger/Pemicu:",
|
"Trigger type:": "Tipe Trigger/Pemicu:",
|
||||||
"Event type:": "Tipe event:",
|
"Event type:": "Tipe event:",
|
||||||
|
@ -125,7 +125,7 @@ export default {
|
|||||||
Export: "Eksportuj",
|
Export: "Eksportuj",
|
||||||
Import: "Importuj",
|
Import: "Importuj",
|
||||||
respTime: "Czas odp. (ms)",
|
respTime: "Czas odp. (ms)",
|
||||||
notAvailableShort: "N/A",
|
notAvailableShort: "N/D",
|
||||||
"Default enabled": "Włącz domyślnie",
|
"Default enabled": "Włącz domyślnie",
|
||||||
"Apply on all existing monitors": "Zastosuj do istniejących monitorów",
|
"Apply on all existing monitors": "Zastosuj do istniejących monitorów",
|
||||||
Create: "Stwórz",
|
Create: "Stwórz",
|
||||||
@ -181,7 +181,7 @@ export default {
|
|||||||
"Edit Status Page": "Edytuj stronę statusu",
|
"Edit Status Page": "Edytuj stronę statusu",
|
||||||
"Go to Dashboard": "Idź do panelu",
|
"Go to Dashboard": "Idź do panelu",
|
||||||
"Status Page": "Strona statusu",
|
"Status Page": "Strona statusu",
|
||||||
"Status Pages": "Strona statusu",
|
"Status Pages": "Strony statusów",
|
||||||
defaultNotificationName: "Moje powiadomienie {notification} ({number})",
|
defaultNotificationName: "Moje powiadomienie {notification} ({number})",
|
||||||
here: "tutaj",
|
here: "tutaj",
|
||||||
Required: "Wymagane",
|
Required: "Wymagane",
|
||||||
@ -359,6 +359,16 @@ export default {
|
|||||||
serwersmsAPIPassword: "Hasło API",
|
serwersmsAPIPassword: "Hasło API",
|
||||||
serwersmsPhoneNumber: "Numer telefonu",
|
serwersmsPhoneNumber: "Numer telefonu",
|
||||||
serwersmsSenderName: "Nazwa nadawcy (zatwierdzona w panelu klienta)",
|
serwersmsSenderName: "Nazwa nadawcy (zatwierdzona w panelu klienta)",
|
||||||
|
smseagle: "SMSEagle",
|
||||||
|
smseagleTo: "Numer/y telefonu",
|
||||||
|
smseagleGroup: "Grupa/y z Książki adresowej",
|
||||||
|
smseagleContact: "Kontakt/y z Książki adresowej",
|
||||||
|
smseagleRecipientType: "Typ odbiorcy",
|
||||||
|
smseagleRecipient: "Odbiorca/y (wiele musi być oddzielone przecinkami)",
|
||||||
|
smseagleToken: "Klucz dostępu API",
|
||||||
|
smseagleUrl: "URL Twojego urządzenia SMSEagle",
|
||||||
|
smseagleEncoding: "Wyślij jako Unicode",
|
||||||
|
smseaglePriority: "Priorytet wiadomości (0-9, domyślnie = 0)",
|
||||||
stackfield: "Stackfield",
|
stackfield: "Stackfield",
|
||||||
Customize: "Dostosuj",
|
Customize: "Dostosuj",
|
||||||
"Custom Footer": "Niestandardowa stopka",
|
"Custom Footer": "Niestandardowa stopka",
|
||||||
@ -435,7 +445,7 @@ export default {
|
|||||||
"HTTP Basic Auth": "Podstawowa autoryzacja HTTP",
|
"HTTP Basic Auth": "Podstawowa autoryzacja HTTP",
|
||||||
"New Status Page": "Nowa strona statusu",
|
"New Status Page": "Nowa strona statusu",
|
||||||
"Page Not Found": "Strona nie została znaleziona",
|
"Page Not Found": "Strona nie została znaleziona",
|
||||||
"Reverse Proxy": "Odwrotne Proxy",
|
"Reverse Proxy": "Zwrotny serwer proxy",
|
||||||
Backup: "Backup",
|
Backup: "Backup",
|
||||||
About: "O skrypcie",
|
About: "O skrypcie",
|
||||||
wayToGetCloudflaredURL: "(Pobierz cloudflared z {0})",
|
wayToGetCloudflaredURL: "(Pobierz cloudflared z {0})",
|
||||||
@ -447,7 +457,7 @@ export default {
|
|||||||
"For example: nginx, Apache and Traefik.": "Na przykład: nginx, Apache i Traefik.",
|
"For example: nginx, Apache and Traefik.": "Na przykład: nginx, Apache i Traefik.",
|
||||||
"Please read": "Przeczytaj proszę",
|
"Please read": "Przeczytaj proszę",
|
||||||
"Subject:": "Temat:",
|
"Subject:": "Temat:",
|
||||||
"Valid To:": "Ważdny do:",
|
"Valid To:": "Ważny do:",
|
||||||
"Days Remaining:": "Pozostało dni:",
|
"Days Remaining:": "Pozostało dni:",
|
||||||
"Issuer:": "Wydawca:",
|
"Issuer:": "Wydawca:",
|
||||||
"Fingerprint:": "Odcisk palca:",
|
"Fingerprint:": "Odcisk palca:",
|
||||||
@ -467,4 +477,168 @@ export default {
|
|||||||
"Domain Names": "Domeny",
|
"Domain Names": "Domeny",
|
||||||
signedInDisp: "Zalogowany jako {0}",
|
signedInDisp: "Zalogowany jako {0}",
|
||||||
signedInDispDisabled: "Autoryzacja wyłączona.",
|
signedInDispDisabled: "Autoryzacja wyłączona.",
|
||||||
|
resendEveryXTimes: "Wysyłaj ponownie co {0} razy",
|
||||||
|
resendDisabled: "Ponowne wysyłanie jest wyłączone",
|
||||||
|
Maintenance: "Konserwacja",
|
||||||
|
statusMaintenance: "Konserwacja",
|
||||||
|
"Schedule maintenance": "Planowanie konserwacji",
|
||||||
|
"Affected Monitors": "Monitory dotknięte problemem",
|
||||||
|
"Pick Affected Monitors...": "Wybierz monitory, których to dotyczy...",
|
||||||
|
"Start of maintenance": "Rozpoczęcie konserwacji",
|
||||||
|
"All Status Pages": "Wszystkie strony statusu",
|
||||||
|
"Select status pages...": "Wybierz strony statusu...",
|
||||||
|
recurringIntervalMessage: "Uruchom raz dziennie | Uruchom raz na {0} dni",
|
||||||
|
affectedMonitorsDescription: "Wybierz monitory, których dotyczy bieżąca konserwacja",
|
||||||
|
affectedStatusPages: "Pokaż ten komunikat o konserwacji na wybranych stronach statusu",
|
||||||
|
atLeastOneMonitor: "Wybierz co najmniej jeden monitor, którego dotyczy problem",
|
||||||
|
deleteMaintenanceMsg: "Czy na pewno chcesz usunąć tę konserwację?",
|
||||||
|
dnsPortDescription: "Port serwera DNS. Domyślnie 53. Możesz zmienić port w dowolnym momencie.",
|
||||||
|
"Resend Notification if Down X times consequently": "Wyślij ponownie powiadomienie, jeśli nie działa X razy pod rząd",
|
||||||
|
error: "błąd",
|
||||||
|
critical: "krytyczny",
|
||||||
|
wayToGetPagerDutyKey: "Możesz to uzyskać, przechodząc do Service -> Service Directory -> (wybierz usługę) -> Integrations -> Add integration. Tutaj możesz wyszukać \"Events API V2\". Więcej informacji {0}",
|
||||||
|
"Integration Key": "Klucz integracji",
|
||||||
|
"Integration URL": "Adres URL integracji",
|
||||||
|
"Auto resolve or acknowledged": "Automatycznie rozwiązany lub potwierdzony",
|
||||||
|
"do nothing": "nie rób nic",
|
||||||
|
"auto acknowledged": "auto potwierdzony",
|
||||||
|
"auto resolve": "automatycznie rozwiązany",
|
||||||
|
"Bark Group": "Grupa Bark",
|
||||||
|
"Bark Sound": "Dźwięk Bark",
|
||||||
|
"HTTP Headers": "Nagłówki HTTP",
|
||||||
|
"Trust Proxy": "Ufaj proxy",
|
||||||
|
HomeAssistant: "Home Assistant",
|
||||||
|
RadiusSecret: "Sekretny klucz Radius",
|
||||||
|
RadiusSecretDescription: "Współdzielony sekretny klucz pomiędzy klientem a serwerem",
|
||||||
|
RadiusCalledStationId: "Id stacji wywoływanej",
|
||||||
|
RadiusCalledStationIdDescription: "Identyfikator wywoływanego urządzenia",
|
||||||
|
RadiusCallingStationId: "Id stacji wywoławczej",
|
||||||
|
RadiusCallingStationIdDescription: "Identyfikator urządzenia wywołującego",
|
||||||
|
"Certificate Expiry Notification": "Powiadomienie o wygaśnięciu certyfikatu",
|
||||||
|
"API Username": "Nazwa użytkownika API",
|
||||||
|
"API Key": "Klucz API",
|
||||||
|
"Recipient Number": "Numer odbiorcy",
|
||||||
|
"From Name/Number": "Od nazwa/numer",
|
||||||
|
"Leave blank to use a shared sender number.": "Pozostaw puste, aby użyć wspólnego numeru nadawcy.",
|
||||||
|
"Octopush API Version": "Wersja API Octopush",
|
||||||
|
"Legacy Octopush-DM": "Starsze Octopush-DM",
|
||||||
|
endpoint: "punkt końcowy",
|
||||||
|
octopushAPIKey: "\"API key\" z poświadczeń HTTP API w panelu sterowania",
|
||||||
|
octopushLogin: "\"Login\" z poświadczeń HTTP API w panelu sterowania",
|
||||||
|
promosmsLogin: "Nazwa logowania API",
|
||||||
|
promosmsPassword: "Hasło API",
|
||||||
|
"pushoversounds pushover": "Pushover (domyślny)",
|
||||||
|
"pushoversounds bike": "Bike",
|
||||||
|
"pushoversounds bugle": "Bugle",
|
||||||
|
"pushoversounds cashregister": "Cash Register",
|
||||||
|
"pushoversounds classical": "Classical",
|
||||||
|
"pushoversounds cosmic": "Cosmic",
|
||||||
|
"pushoversounds falling": "Falling",
|
||||||
|
"pushoversounds gamelan": "Gamelan",
|
||||||
|
"pushoversounds incoming": "Incoming",
|
||||||
|
"pushoversounds intermission": "Intermission",
|
||||||
|
"pushoversounds magic": "Magic",
|
||||||
|
"pushoversounds mechanical": "Mechanical",
|
||||||
|
"pushoversounds pianobar": "Piano Bar",
|
||||||
|
"pushoversounds siren": "Siren",
|
||||||
|
"pushoversounds spacealarm": "Space Alarm",
|
||||||
|
"pushoversounds tugboat": "Tug Boat",
|
||||||
|
"pushoversounds alien": "Alien Alarm (długie)",
|
||||||
|
"pushoversounds climb": "Climb (długie)",
|
||||||
|
"pushoversounds persistent": "Persistent (długie)",
|
||||||
|
"pushoversounds echo": "Pushover Echo (długie)",
|
||||||
|
"pushoversounds updown": "Up Down (długie)",
|
||||||
|
"pushoversounds vibrate": "Tylko wibracje",
|
||||||
|
"pushoversounds none": "Brak (cisza)",
|
||||||
|
pushyAPIKey: "Tajny klucz API",
|
||||||
|
pushyToken: "Token urządzenia",
|
||||||
|
"Show update if available": "Pokaż aktualizację, jeśli jest dostępna",
|
||||||
|
"Also check beta release": "Sprawdź również wydanie beta",
|
||||||
|
"Using a Reverse Proxy?": "Używasz odwróconego proxy?",
|
||||||
|
"Check how to config it for WebSocket": "Sprawdź jak go skonfigurować dla WebSocket",
|
||||||
|
"Steam Game Server": "Serwer gry Steam",
|
||||||
|
"Most likely causes:": "Najbardziej prawdopodobne przyczyny:",
|
||||||
|
"The resource is no longer available.": "Zasób nie jest już dostępny.",
|
||||||
|
"There might be a typing error in the address.": "W adresie może być błąd w pisowni.",
|
||||||
|
"What you can try:": "Co możesz spróbować:",
|
||||||
|
"Retype the address.": "Ponownie wpisz adres.",
|
||||||
|
"Go back to the previous page.": "Wróć do poprzedniej strony.",
|
||||||
|
"Coming Soon": "Wkrótce",
|
||||||
|
wayToGetClickSendSMSToken: "Możesz uzyskać nazwę użytkownika API i klucz API z {0}.",
|
||||||
|
"Connection String": "Ciąg połączenia",
|
||||||
|
Query: "Zapytanie",
|
||||||
|
settingsCertificateExpiry: "Wygaśnięcie certyfikatu TLS",
|
||||||
|
certificationExpiryDescription: "Monitory HTTPS uruchamiają powiadomienia o wygaśnięciu certyfikatu TLS w:",
|
||||||
|
"Setup Docker Host": "Konfiguracja hosta Docker",
|
||||||
|
"Connection Type": "Typ połączenia",
|
||||||
|
"Docker Daemon": "Demon Dockera",
|
||||||
|
deleteDockerHostMsg: "Czy na pewno chcesz usunąć ten host Dockera dla wszystkich monitorów?",
|
||||||
|
socket: "Gniazdo",
|
||||||
|
tcp: "TCP / HTTP",
|
||||||
|
"Docker Container": "Kontener Dockera",
|
||||||
|
"Container Name / ID": "Nazwa kontenera / ID",
|
||||||
|
"Docker Host": "Host Dockera",
|
||||||
|
"Docker Hosts": "Hosty Dockera",
|
||||||
|
"ntfy Topic": "Temat ntfy",
|
||||||
|
Domain: "Domena",
|
||||||
|
Workstation: "Stacja robocza",
|
||||||
|
disableCloudflaredNoAuthMsg: "Jesteś w trybie No Auth, hasło nie jest wymagane.",
|
||||||
|
trustProxyDescription: "Zaufaj nagłówkom 'X-Forwarded-*'. Jeśli chcesz uzyskać poprawne IP klienta, a twój Uptime Kuma jest za Nginx lub Apache, powinieneś to włączyć.",
|
||||||
|
wayToGetLineNotifyToken: "Możesz uzyskać token dostępu z {0}",
|
||||||
|
Examples: "Przykłady",
|
||||||
|
"Home Assistant URL": "URL Home Assistant",
|
||||||
|
"Long-Lived Access Token": "Długotrwały token dostępu",
|
||||||
|
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Długotrwały token dostępu można utworzyć klikając na nazwę swojego profilu (na dole po lewej stronie) i przewijając do dołu, a następnie klikając Create Token. ",
|
||||||
|
"Notification Service": "Usługa powiadamiania",
|
||||||
|
"default: notify all devices": "domyślnie: powiadamiaj wszystkie urządzenia",
|
||||||
|
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Listę usług powiadamiania można znaleźć w Home Assistant pod \"Developer Tools > Services\" wyszukaj \"notification\", aby znaleźć nazwę swojego urządzenia/telefonu.",
|
||||||
|
"Automations can optionally be triggered in Home Assistant:": "Automaty mogą być opcjonalnie uruchamiane w Home Assistant:",
|
||||||
|
"Trigger type:": "Typ wyzwalacza:",
|
||||||
|
"Event type:": "Typ zdarzenia:",
|
||||||
|
"Event data:": "Dane o zdarzeniu:",
|
||||||
|
"Then choose an action, for example switch the scene to where an RGB light is red.": "Następnie wybierz akcję, na przykład przełącz scenę na taką, w której światło RGB jest czerwone.",
|
||||||
|
"Frontend Version": "Wersja frontu",
|
||||||
|
"Frontend Version do not match backend version!": "Wersja frontu nie pasuje do wersji backendu!",
|
||||||
|
"Base URL": "Bazowy adres URL",
|
||||||
|
goAlertInfo: "GoAlert to aplikacja open source do planowania, automatycznych eskalacji i powiadomień (jak SMS lub połączenia głosowe). Automatycznie angażuj właściwą osobę, we właściwy sposób i we właściwym czasie! {0}",
|
||||||
|
goAlertIntegrationKeyInfo: "Pobierz generyczny klucz integracyjny API dla usługi, którego wartość skopiowanego tokena URL jest zwykle w formacie \"aaaaaaaa-bbb-cccc-dddd-eeeeee\".",
|
||||||
|
goAlert: "GoAlert",
|
||||||
|
backupOutdatedWarning: "Przestarzałe: ponieważ dodano wiele funkcji i funkcja tworzenia kopii zapasowych nie jest wystarczająco utrzymywana, nie może generować ani przywracać pełnej kopii zapasowej.",
|
||||||
|
backupRecommend: "Zamiast tego należy wykonać bezpośrednią kopię zapasową woluminu lub folderu danych (./data/).",
|
||||||
|
Optional: "Opcjonalne",
|
||||||
|
squadcast: "Squadcast",
|
||||||
|
SendKey: "SendKey",
|
||||||
|
"SMSManager API Docs": "Dokumentacja API SMSManager ",
|
||||||
|
"Gateway Type": "Typ bramy",
|
||||||
|
SMSManager: "SMSManager",
|
||||||
|
"You can divide numbers with": "Możesz dzielić liczby przez",
|
||||||
|
or: "lub",
|
||||||
|
recurringInterval: "odstęp czasu",
|
||||||
|
Recurring: "powtarzający się",
|
||||||
|
strategyManual: "Aktywowany/dezaktywowany ręcznie",
|
||||||
|
warningTimezone: "Używa strefy czasowej serwera",
|
||||||
|
weekdayShortMon: "pon",
|
||||||
|
weekdayShortTue: "wt",
|
||||||
|
weekdayShortWed: "śr",
|
||||||
|
weekdayShortThu: "czw",
|
||||||
|
weekdayShortFri: "pt",
|
||||||
|
weekdayShortSat: "sob",
|
||||||
|
weekdayShortSun: "niedz",
|
||||||
|
dayOfWeek: "Dzień tygodnia",
|
||||||
|
dayOfMonth: "Dzień miesiąca",
|
||||||
|
lastDay: "Ostatni dzień",
|
||||||
|
lastDay1: "Ostatni dzień miesiąca",
|
||||||
|
lastDay2: "2. ostatni dzień miesiąca",
|
||||||
|
lastDay3: "3. ostatni dzień miesiąca",
|
||||||
|
lastDay4: "4. ostatni dzień miesiąca",
|
||||||
|
"No Maintenance": "Brak konserwacji",
|
||||||
|
pauseMaintenanceMsg: "Jesteś pewien, że chcesz zatrzymać?",
|
||||||
|
"maintenanceStatus-under-maintenance": "Podczas konserwacji",
|
||||||
|
"maintenanceStatus-inactive": "Nieaktywny",
|
||||||
|
"maintenanceStatus-scheduled": "Zaplanowany",
|
||||||
|
"maintenanceStatus-ended": "Zakończony",
|
||||||
|
"maintenanceStatus-unknown": "Nieznany",
|
||||||
|
"Display Timezone": "Wyświetlana strefa czasowa",
|
||||||
|
"Server Timezone": "Strefa czasowa serwera",
|
||||||
|
statusPageMaintenanceEndDate: "Koniec",
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ export default {
|
|||||||
languageName: "简体中文",
|
languageName: "简体中文",
|
||||||
checkEverySecond: "检测频率 {0} 秒",
|
checkEverySecond: "检测频率 {0} 秒",
|
||||||
retryCheckEverySecond: "重试间隔 {0} 秒",
|
retryCheckEverySecond: "重试间隔 {0} 秒",
|
||||||
retriesDescription: "服务被标记为故障并发送通知之前得最大重试次数",
|
retriesDescription: "服务被标记为故障并发送通知之前的最大重试次数",
|
||||||
ignoreTLSError: "忽略 HTTPS 站点的 TLS/SSL 错误",
|
ignoreTLSError: "忽略 HTTPS 站点的 TLS/SSL 错误",
|
||||||
upsideDownModeDescription: "反转状态监控,如果服务可访问,则认为是故障。",
|
upsideDownModeDescription: "反转状态监控,如果服务可访问,则认为是故障。",
|
||||||
maxRedirectDescription: "允许的最大重定向次数。设置为 0 禁用重定向。",
|
maxRedirectDescription: "允许的最大重定向次数。设置为 0 禁用重定向。",
|
||||||
|
@ -380,4 +380,6 @@ export default {
|
|||||||
proxyDescription: "必須將代理伺服器指派給監測器才能運作。",
|
proxyDescription: "必須將代理伺服器指派給監測器才能運作。",
|
||||||
enableProxyDescription: "此代理伺服器在啟用前不會在監測器上生效,您可以藉由控制啟用狀態來暫時對所有的監測器停用代理伺服器。",
|
enableProxyDescription: "此代理伺服器在啟用前不會在監測器上生效,您可以藉由控制啟用狀態來暫時對所有的監測器停用代理伺服器。",
|
||||||
setAsDefaultProxyDescription: "預設情況下,新監測器將啟用此代理伺服器。您仍可分別停用各監測器的代理伺服器。",
|
setAsDefaultProxyDescription: "預設情況下,新監測器將啟用此代理伺服器。您仍可分別停用各監測器的代理伺服器。",
|
||||||
|
Maintenance: "維護",
|
||||||
|
statusMaintenance: "維護中",
|
||||||
};
|
};
|
||||||
|
@ -9,11 +9,24 @@ export default {
|
|||||||
upsideDownModeDescription: "反轉顯示狀態。若服務可以連線,將顯示離線。",
|
upsideDownModeDescription: "反轉顯示狀態。若服務可以連線,將顯示離線。",
|
||||||
maxRedirectDescription: "最大重新導向跟隨次數。設為 0 將停用重新導向。",
|
maxRedirectDescription: "最大重新導向跟隨次數。設為 0 將停用重新導向。",
|
||||||
acceptedStatusCodesDescription: "選擇視為成功回應的狀態碼。",
|
acceptedStatusCodesDescription: "選擇視為成功回應的狀態碼。",
|
||||||
|
Maintenance: "維護",
|
||||||
|
statusMaintenance: "維護",
|
||||||
|
"Schedule maintenance": "排程維護",
|
||||||
|
"Affected Monitors": "受影響的監測器",
|
||||||
|
"Pick Affected Monitors...": "挑選受影響的監測器...",
|
||||||
|
"Start of maintenance": "維護起始",
|
||||||
|
"All Status Pages": "所有狀態頁",
|
||||||
|
"Select status pages...": "選擇狀態頁...",
|
||||||
|
recurringIntervalMessage: "每日執行 | 每 {0} 天執行",
|
||||||
|
affectedMonitorsDescription: "選擇受目前維護影響的監測器",
|
||||||
|
affectedStatusPages: "在已選取的狀態頁中顯示此維護訊息",
|
||||||
|
atLeastOneMonitor: "至少選擇一個受影響的監測器",
|
||||||
passwordNotMatchMsg: "密碼不相符。",
|
passwordNotMatchMsg: "密碼不相符。",
|
||||||
notificationDescription: "必須將通知指派給監測器才能運作。",
|
notificationDescription: "必須將通知指派給監測器才能運作。",
|
||||||
keywordDescription: "HTML 或 JSON 回應的搜尋關鍵字。區分大小寫。",
|
keywordDescription: "HTML 或 JSON 回應的搜尋關鍵字。區分大小寫。",
|
||||||
pauseDashboardHome: "暫停",
|
pauseDashboardHome: "暫停",
|
||||||
deleteMonitorMsg: "您確定要刪除此監測器嗎?",
|
deleteMonitorMsg: "您確定要刪除此監測器嗎?",
|
||||||
|
deleteMaintenanceMsg: "您確定要刪除此維護嗎?",
|
||||||
deleteNotificationMsg: "您確定要為所有監測器刪除此通知嗎?",
|
deleteNotificationMsg: "您確定要為所有監測器刪除此通知嗎?",
|
||||||
dnsPortDescription: "DNS 伺服器連接埠。預設為 53。您可以隨時變更連接埠。",
|
dnsPortDescription: "DNS 伺服器連接埠。預設為 53。您可以隨時變更連接埠。",
|
||||||
resolverserverDescription: "Cloudflare 為預設伺服器。您可以隨時更換解析伺服器。",
|
resolverserverDescription: "Cloudflare 為預設伺服器。您可以隨時更換解析伺服器。",
|
||||||
@ -305,7 +318,7 @@ export default {
|
|||||||
Method: "方法",
|
Method: "方法",
|
||||||
Body: "主體",
|
Body: "主體",
|
||||||
Headers: "標頭",
|
Headers: "標頭",
|
||||||
PushUrl: "Push URL",
|
PushUrl: "Push 網址",
|
||||||
HeadersInvalidFormat: "要求標頭不是有效的 JSON:",
|
HeadersInvalidFormat: "要求標頭不是有效的 JSON:",
|
||||||
BodyInvalidFormat: "請求主體不是有效的 JSON:",
|
BodyInvalidFormat: "請求主體不是有效的 JSON:",
|
||||||
"Monitor History": "監測器歷史紀錄",
|
"Monitor History": "監測器歷史紀錄",
|
||||||
@ -582,4 +595,40 @@ export default {
|
|||||||
goAlert: "GoAlert",
|
goAlert: "GoAlert",
|
||||||
backupOutdatedWarning: "過時:由於新功能的增加,且未妥善維護,故此備份功能無法產生或復原完整備份。",
|
backupOutdatedWarning: "過時:由於新功能的增加,且未妥善維護,故此備份功能無法產生或復原完整備份。",
|
||||||
backupRecommend: "請直接備份磁碟區或 ./data/ 資料夾。",
|
backupRecommend: "請直接備份磁碟區或 ./data/ 資料夾。",
|
||||||
|
"Optional": "選填",
|
||||||
|
squadcast: "Squadcast",
|
||||||
|
SendKey: "SendKey",
|
||||||
|
"SMSManager API Docs": "SMSManager API 文件 ",
|
||||||
|
"Gateway Type": "閘道類型",
|
||||||
|
SMSManager: "SMSManager",
|
||||||
|
"You can divide numbers with": "若要除數,您可以使用",
|
||||||
|
"or": "或是",
|
||||||
|
recurringInterval: "間隔",
|
||||||
|
"Recurring": "週期性",
|
||||||
|
strategyManual: "手動切換使用中/非使用中",
|
||||||
|
warningTimezone: "正在使用伺服器的時區",
|
||||||
|
weekdayShortMon: "一",
|
||||||
|
weekdayShortTue: "二",
|
||||||
|
weekdayShortWed: "三",
|
||||||
|
weekdayShortThu: "四",
|
||||||
|
weekdayShortFri: "五",
|
||||||
|
weekdayShortSat: "六",
|
||||||
|
weekdayShortSun: "日",
|
||||||
|
dayOfWeek: "每周特定一天",
|
||||||
|
dayOfMonth: "每月特定一天",
|
||||||
|
lastDay: "最後一天",
|
||||||
|
lastDay1: "每月的最後一天",
|
||||||
|
lastDay2: "每月的倒數第二天",
|
||||||
|
lastDay3: "每月的倒數第三天",
|
||||||
|
lastDay4: "每月的倒數第四天",
|
||||||
|
"No Maintenance": "無維護",
|
||||||
|
pauseMaintenanceMsg: "您確定要暫停嗎?",
|
||||||
|
"maintenanceStatus-under-maintenance": "維護中",
|
||||||
|
"maintenanceStatus-inactive": "非使用中",
|
||||||
|
"maintenanceStatus-scheduled": "已排程",
|
||||||
|
"maintenanceStatus-ended": "已結束",
|
||||||
|
"maintenanceStatus-unknown": "未知",
|
||||||
|
"Display Timezone": "顯示時區",
|
||||||
|
"Server Timezone": "伺服器時區",
|
||||||
|
statusPageMaintenanceEndDate: "結束",
|
||||||
};
|
};
|
||||||
|
@ -37,19 +37,32 @@
|
|||||||
<div class="profile-pic">{{ $root.usernameFirstChar }}</div>
|
<div class="profile-pic">{{ $root.usernameFirstChar }}</div>
|
||||||
<font-awesome-icon icon="angle-down" />
|
<font-awesome-icon icon="angle-down" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Header's Dropdown Menu -->
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
|
<!-- Username -->
|
||||||
<li>
|
<li>
|
||||||
<i18n-t v-if="$root.username != null" tag="span" keypath="signedInDisp" class="dropdown-item-text">
|
<i18n-t v-if="$root.username != null" tag="span" keypath="signedInDisp" class="dropdown-item-text">
|
||||||
<strong>{{ $root.username }}</strong>
|
<strong>{{ $root.username }}</strong>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
<span v-if="$root.username == null" class="dropdown-item-text">{{ $t("signedInDispDisabled") }}</span>
|
<span v-if="$root.username == null" class="dropdown-item-text">{{ $t("signedInDispDisabled") }}</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
|
||||||
|
<!-- Functions -->
|
||||||
<li>
|
<li>
|
||||||
<router-link to="/settings" class="dropdown-item" :class="{ active: $route.path.includes('settings') }">
|
<router-link to="/maintenance" class="dropdown-item" :class="{ active: $route.path.includes('manage-maintenance') }">
|
||||||
|
<font-awesome-icon icon="wrench" /> {{ $t("Maintenance") }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<router-link to="/settings/general" class="dropdown-item" :class="{ active: $route.path.includes('settings') }">
|
||||||
<font-awesome-icon icon="cog" /> {{ $t("Settings") }}
|
<font-awesome-icon icon="cog" /> {{ $t("Settings") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li v-if="$root.loggedIn && $root.socket.token !== 'autoLogin'">
|
<li v-if="$root.loggedIn && $root.socket.token !== 'autoLogin'">
|
||||||
<button class="dropdown-item" @click="$root.logout">
|
<button class="dropdown-item" @click="$root.logout">
|
||||||
<font-awesome-icon icon="sign-out-alt" />
|
<font-awesome-icon icon="sign-out-alt" />
|
||||||
@ -304,5 +317,4 @@ main {
|
|||||||
background-color: $dark-bg;
|
background-color: $dark-bg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -5,6 +5,7 @@ import Toast from "vue-toastification";
|
|||||||
import "vue-toastification/dist/index.css";
|
import "vue-toastification/dist/index.css";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import "./assets/app.scss";
|
import "./assets/app.scss";
|
||||||
|
import "./assets/vue-datepicker.scss";
|
||||||
import { i18n } from "./i18n";
|
import { i18n } from "./i18n";
|
||||||
import { FontAwesomeIcon } from "./icon.js";
|
import { FontAwesomeIcon } from "./icon.js";
|
||||||
import datetime from "./mixins/datetime";
|
import datetime from "./mixins/datetime";
|
||||||
@ -15,6 +16,13 @@ import theme from "./mixins/theme";
|
|||||||
import lang from "./mixins/lang";
|
import lang from "./mixins/lang";
|
||||||
import { router } from "./router";
|
import { router } from "./router";
|
||||||
import { appName } from "./util.ts";
|
import { appName } from "./util.ts";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import timezone from "dayjs/plugin/timezone";
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
mixins: [
|
mixins: [
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
|
||||||
import timezone from "dayjs/plugin/timezone";
|
|
||||||
import utc from "dayjs/plugin/utc";
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DateTime Mixin
|
* DateTime Mixin
|
||||||
@ -18,6 +12,19 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
toUTC(value) {
|
||||||
|
return dayjs.tz(value, this.timezone).utc().format();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for <input type="datetime" />
|
||||||
|
* @param value
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
toDateTimeInputFormat(value) {
|
||||||
|
return this.datetimeFormat(value, "YYYY-MM-DDTHH:mm");
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a given value in the format YYYY-MM-DD HH:mm:ss
|
* Return a given value in the format YYYY-MM-DD HH:mm:ss
|
||||||
* @param {any} value Value to format as date time
|
* @param {any} value Value to format as date time
|
||||||
@ -27,6 +34,17 @@ export default {
|
|||||||
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
|
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
datetimeMaintenance(value) {
|
||||||
|
const inputDate = new Date(value);
|
||||||
|
const now = new Date(Date.now());
|
||||||
|
|
||||||
|
if (inputDate.getFullYear() === now.getUTCFullYear() && inputDate.getMonth() === now.getUTCMonth() && inputDate.getDay() === now.getUTCDay()) {
|
||||||
|
return this.datetimeFormat(value, "HH:mm");
|
||||||
|
} else {
|
||||||
|
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a given value in the format YYYY-MM-DD
|
* Return a given value in the format YYYY-MM-DD
|
||||||
* @param {any} value Value to format as date
|
* @param {any} value Value to format as date
|
||||||
@ -64,7 +82,7 @@ export default {
|
|||||||
return dayjs.utc(value).tz(this.timezone).format(format);
|
return dayjs.utc(value).tz(this.timezone).format(format);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -33,6 +33,7 @@ export default {
|
|||||||
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
monitorList: { },
|
monitorList: { },
|
||||||
|
maintenanceList: { },
|
||||||
heartbeatList: { },
|
heartbeatList: { },
|
||||||
importantHeartbeatList: { },
|
importantHeartbeatList: { },
|
||||||
avgPingList: { },
|
avgPingList: { },
|
||||||
@ -57,7 +58,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
window.addEventListener("resize", this.onResize);
|
|
||||||
this.initSocketIO();
|
this.initSocketIO();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -129,6 +129,10 @@ export default {
|
|||||||
this.monitorList = data;
|
this.monitorList = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("maintenanceList", (data) => {
|
||||||
|
this.maintenanceList = data;
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("notificationList", (data) => {
|
socket.on("notificationList", (data) => {
|
||||||
this.notificationList = data;
|
this.notificationList = data;
|
||||||
});
|
});
|
||||||
@ -445,6 +449,13 @@ export default {
|
|||||||
socket.emit("getMonitorList", callback);
|
socket.emit("getMonitorList", callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getMaintenanceList(callback) {
|
||||||
|
if (! callback) {
|
||||||
|
callback = () => { };
|
||||||
|
}
|
||||||
|
socket.emit("getMaintenanceList", callback);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a monitor
|
* Add a monitor
|
||||||
* @param {Object} monitor Object representing monitor to add
|
* @param {Object} monitor Object representing monitor to add
|
||||||
@ -454,6 +465,26 @@ export default {
|
|||||||
socket.emit("add", monitor, callback);
|
socket.emit("add", monitor, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addMaintenance(maintenance, callback) {
|
||||||
|
socket.emit("addMaintenance", maintenance, callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
addMonitorMaintenance(maintenanceID, monitors, callback) {
|
||||||
|
socket.emit("addMonitorMaintenance", maintenanceID, monitors, callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
addMaintenanceStatusPage(maintenanceID, statusPages, callback) {
|
||||||
|
socket.emit("addMaintenanceStatusPage", maintenanceID, statusPages, callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
getMonitorMaintenance(maintenanceID, callback) {
|
||||||
|
socket.emit("getMonitorMaintenance", maintenanceID, callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
getMaintenanceStatusPage(maintenanceID, callback) {
|
||||||
|
socket.emit("getMaintenanceStatusPage", maintenanceID, callback);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete monitor by ID
|
* Delete monitor by ID
|
||||||
* @param {number} monitorID ID of monitor to delete
|
* @param {number} monitorID ID of monitor to delete
|
||||||
@ -463,6 +494,10 @@ export default {
|
|||||||
socket.emit("deleteMonitor", monitorID, callback);
|
socket.emit("deleteMonitor", monitorID, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteMaintenance(maintenanceID, callback) {
|
||||||
|
socket.emit("deleteMaintenance", maintenanceID, callback);
|
||||||
|
},
|
||||||
|
|
||||||
/** Clear the hearbeat list */
|
/** Clear the hearbeat list */
|
||||||
clearData() {
|
clearData() {
|
||||||
console.log("reset heartbeat list");
|
console.log("reset heartbeat list");
|
||||||
@ -550,7 +585,12 @@ export default {
|
|||||||
for (let monitorID in this.lastHeartbeatList) {
|
for (let monitorID in this.lastHeartbeatList) {
|
||||||
let lastHeartBeat = this.lastHeartbeatList[monitorID];
|
let lastHeartBeat = this.lastHeartbeatList[monitorID];
|
||||||
|
|
||||||
if (! lastHeartBeat) {
|
if (this.monitorList[monitorID].maintenance) {
|
||||||
|
result[monitorID] = {
|
||||||
|
text: this.$t("statusMaintenance"),
|
||||||
|
color: "maintenance",
|
||||||
|
};
|
||||||
|
} else if (! lastHeartBeat) {
|
||||||
result[monitorID] = unknown;
|
result[monitorID] = unknown;
|
||||||
} else if (lastHeartBeat.status === 1) {
|
} else if (lastHeartBeat.status === 1) {
|
||||||
result[monitorID] = {
|
result[monitorID] = {
|
||||||
@ -579,6 +619,7 @@ export default {
|
|||||||
let result = {
|
let result = {
|
||||||
up: 0,
|
up: 0,
|
||||||
down: 0,
|
down: 0,
|
||||||
|
maintenance: 0,
|
||||||
unknown: 0,
|
unknown: 0,
|
||||||
pause: 0,
|
pause: 0,
|
||||||
};
|
};
|
||||||
@ -587,7 +628,9 @@ export default {
|
|||||||
let beat = this.$root.lastHeartbeatList[monitorID];
|
let beat = this.$root.lastHeartbeatList[monitorID];
|
||||||
let monitor = this.$root.monitorList[monitorID];
|
let monitor = this.$root.monitorList[monitorID];
|
||||||
|
|
||||||
if (monitor && ! monitor.active) {
|
if (monitor && monitor.maintenance) {
|
||||||
|
result.maintenance++;
|
||||||
|
} else if (monitor && ! monitor.active) {
|
||||||
result.pause++;
|
result.pause++;
|
||||||
} else if (beat) {
|
} else if (beat) {
|
||||||
if (beat.status === 1) {
|
if (beat.status === 1) {
|
||||||
|
@ -46,6 +46,10 @@ export default {
|
|||||||
}
|
}
|
||||||
return this.userTheme;
|
return this.userTheme;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isDark() {
|
||||||
|
return this.theme === "dark";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
|
<div v-if="!$root.isMobile" class="col-12 col-md-5 col-xl-4">
|
||||||
<div>
|
<div>
|
||||||
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
|
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,6 +15,10 @@
|
|||||||
<h3>{{ $t("Down") }}</h3>
|
<h3>{{ $t("Down") }}</h3>
|
||||||
<span class="num text-danger">{{ $root.stats.down }}</span>
|
<span class="num text-danger">{{ $root.stats.down }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<h3>{{ $t("Maintenance") }}</h3>
|
||||||
|
<span class="num text-maintenance">{{ $root.stats.maintenance }}</span>
|
||||||
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3>{{ $t("Unknown") }}</h3>
|
<h3>{{ $t("Unknown") }}</h3>
|
||||||
<span class="num text-secondary">{{ $root.stats.unknown }}</span>
|
<span class="num text-secondary">{{ $root.stats.unknown }}</span>
|
||||||
|
@ -20,18 +20,20 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="functions">
|
<div class="functions">
|
||||||
<button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
|
<div class="btn-group" role="group">
|
||||||
<font-awesome-icon icon="pause" /> {{ $t("Pause") }}
|
<button v-if="monitor.active" class="btn btn-normal" @click="pauseDialog">
|
||||||
</button>
|
<font-awesome-icon icon="pause" /> {{ $t("Pause") }}
|
||||||
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
|
</button>
|
||||||
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
|
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
|
||||||
</button>
|
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
|
||||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
|
</button>
|
||||||
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
<router-link :to=" '/edit/' + monitor.id " class="btn btn-normal">
|
||||||
</router-link>
|
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
||||||
<button class="btn btn-danger" @click="deleteDialog">
|
</router-link>
|
||||||
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
<button class="btn btn-danger" @click="deleteDialog">
|
||||||
</button>
|
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shadow-box">
|
<div class="shadow-box">
|
||||||
@ -392,11 +394,6 @@ export default {
|
|||||||
@media (max-width: 550px) {
|
@media (max-width: 550px) {
|
||||||
.functions {
|
.functions {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
button, a {
|
|
||||||
margin-left: 10px !important;
|
|
||||||
margin-right: 10px !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ping-chart-wrapper {
|
.ping-chart-wrapper {
|
||||||
@ -439,12 +436,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.functions {
|
|
||||||
button, a {
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow-box {
|
.shadow-box {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
|
534
src/pages/EditMaintenance.vue
Normal file
534
src/pages/EditMaintenance.vue
Normal file
@ -0,0 +1,534 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="slide-fade" appear>
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-3">{{ pageName }}</h1>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<div class="shadow-box">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-10">
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">{{ $t("Title") }}</label>
|
||||||
|
<input
|
||||||
|
id="name" v-model="maintenance.title" type="text" class="form-control"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="description" class="form-label">{{ $t("Description") }}</label>
|
||||||
|
<textarea
|
||||||
|
id="description" v-model="maintenance.description" class="form-control"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Affected Monitors -->
|
||||||
|
<h2 class="mt-5">{{ $t("Affected Monitors") }}</h2>
|
||||||
|
{{ $t("affectedMonitorsDescription") }}
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<VueMultiselect
|
||||||
|
id="affected_monitors"
|
||||||
|
v-model="affectedMonitors"
|
||||||
|
:options="affectedMonitorsOptions"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:multiple="true"
|
||||||
|
:close-on-select="false"
|
||||||
|
:clear-on-select="false"
|
||||||
|
:preserve-search="true"
|
||||||
|
:placeholder="$t('Pick Affected Monitors...')"
|
||||||
|
:preselect-first="false"
|
||||||
|
:max-height="600"
|
||||||
|
:taggable="false"
|
||||||
|
></VueMultiselect>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status pages to display maintenance info on -->
|
||||||
|
<h2 class="mt-5">{{ $t("Status Pages") }}</h2>
|
||||||
|
{{ $t("affectedStatusPages") }}
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<!-- Show on all pages -->
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input
|
||||||
|
id="show-on-all-pages" v-model="showOnAllPages" class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
>
|
||||||
|
<label class="form-check-label" for="show-powered-by">{{
|
||||||
|
$t("All Status Pages")
|
||||||
|
}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!showOnAllPages">
|
||||||
|
<VueMultiselect
|
||||||
|
id="selected_status_pages"
|
||||||
|
v-model="selectedStatusPages"
|
||||||
|
:options="selectedStatusPagesOptions"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:multiple="true"
|
||||||
|
:close-on-select="false"
|
||||||
|
:clear-on-select="false"
|
||||||
|
:preserve-search="true"
|
||||||
|
:placeholder="$t('Select status pages...')"
|
||||||
|
:preselect-first="false"
|
||||||
|
:max-height="600"
|
||||||
|
:taggable="false"
|
||||||
|
></VueMultiselect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="mt-5">{{ $t("Date and Time") }}</h2>
|
||||||
|
|
||||||
|
<div>⚠️ {{ $t("warningTimezone") }}: <mark>{{ $root.info.serverTimezone }} ({{ $root.info.serverTimezoneOffset }})</mark></div>
|
||||||
|
|
||||||
|
<!-- Strategy -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="strategy" class="form-label">{{ $t("Strategy") }}</label>
|
||||||
|
<select id="strategy" v-model="maintenance.strategy" class="form-select">
|
||||||
|
<option value="manual">{{ $t("strategyManual") }}</option>
|
||||||
|
<option value="single">{{ $t("Single Maintenance Window") }}</option>
|
||||||
|
<option value="recurring-interval">{{ $t("Recurring") }} - {{ $t("recurringInterval") }}</option>
|
||||||
|
<option value="recurring-weekday">{{ $t("Recurring") }} - {{ $t("dayOfWeek") }}</option>
|
||||||
|
<option value="recurring-day-of-month">{{ $t("Recurring") }} - {{ $t("dayOfMonth") }}</option>
|
||||||
|
<option v-if="false" value="recurring-day-of-year">{{ $t("Recurring") }} - Day of Year</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Single Maintenance Window -->
|
||||||
|
<template v-if="maintenance.strategy === 'single'">
|
||||||
|
<!-- DateTime Range -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label class="form-label">{{ $t("DateTime Range") }}</label>
|
||||||
|
<Datepicker
|
||||||
|
v-model="maintenance.dateRange"
|
||||||
|
:dark="$root.isDark"
|
||||||
|
range
|
||||||
|
:monthChangeOnScroll="false"
|
||||||
|
:minDate="minDate"
|
||||||
|
format="yyyy-MM-dd HH:mm"
|
||||||
|
modelType="yyyy-MM-dd HH:mm:ss"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Recurring - Interval -->
|
||||||
|
<template v-if="maintenance.strategy === 'recurring-interval'">
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="interval-day" class="form-label">
|
||||||
|
{{ $t("recurringInterval") }}
|
||||||
|
|
||||||
|
<template v-if="maintenance.intervalDay >= 1">
|
||||||
|
({{
|
||||||
|
$tc("recurringIntervalMessage", maintenance.intervalDay, [
|
||||||
|
maintenance.intervalDay
|
||||||
|
])
|
||||||
|
}})
|
||||||
|
</template>
|
||||||
|
</label>
|
||||||
|
<input id="interval-day" v-model="maintenance.intervalDay" type="number" class="form-control" required min="1" max="3650" step="1">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Recurring - Weekday -->
|
||||||
|
<template v-if="maintenance.strategy === 'recurring-weekday'">
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="interval-day" class="form-label">
|
||||||
|
{{ $t("dayOfWeek") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Weekday Picker -->
|
||||||
|
<div class="weekday-picker">
|
||||||
|
<div v-for="(weekday, index) in weekdays" :key="index">
|
||||||
|
<label class="form-check-label" :for="weekday.id">{{ $t(weekday.langKey) }}</label>
|
||||||
|
<div class="form-check-inline"><input :id="weekday.id" v-model="maintenance.weekdays" type="checkbox" :value="weekday.value" class="form-check-input"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Recurring - Day of month -->
|
||||||
|
<template v-if="maintenance.strategy === 'recurring-day-of-month'">
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="interval-day" class="form-label">
|
||||||
|
{{ $t("dayOfMonth") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Day Picker -->
|
||||||
|
<div class="day-picker">
|
||||||
|
<div v-for="index in 31" :key="index">
|
||||||
|
<label class="form-check-label" :for="'day' + index">{{ index }}</label>
|
||||||
|
<div class="form-check-inline">
|
||||||
|
<input :id="'day' + index" v-model="maintenance.daysOfMonth" type="checkbox" :value="index" class="form-check-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 mb-2">{{ $t("lastDay") }}</div>
|
||||||
|
|
||||||
|
<div v-for="(lastDay, index) in lastDays" :key="index" class="form-check">
|
||||||
|
<input :id="lastDay.langKey" v-model="maintenance.daysOfMonth" type="checkbox" :value="lastDay.value" class="form-check-input">
|
||||||
|
<label class="form-check-label" :for="lastDay.langKey">
|
||||||
|
{{ $t(lastDay.langKey) }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- For any recurring types -->
|
||||||
|
<template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month'">
|
||||||
|
<!-- Maintenance Time Window of a Day -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label class="form-label">{{ $t("Maintenance Time Window of a Day") }}</label>
|
||||||
|
<Datepicker
|
||||||
|
v-model="maintenance.timeRange"
|
||||||
|
:dark="$root.isDark"
|
||||||
|
timePicker
|
||||||
|
disableTimeRangeValidation range
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label class="form-label">{{ $t("Effective Date Range") }}</label>
|
||||||
|
<Datepicker
|
||||||
|
v-model="maintenance.dateRange"
|
||||||
|
:dark="$root.isDark"
|
||||||
|
range datePicker
|
||||||
|
:monthChangeOnScroll="false"
|
||||||
|
:minDate="minDate"
|
||||||
|
format="yyyy-MM-dd HH:mm:ss"
|
||||||
|
modelType="yyyy-MM-dd HH:mm:ss"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="mt-4 mb-1">
|
||||||
|
<button
|
||||||
|
id="monitor-submit-btn" class="btn btn-primary" type="submit"
|
||||||
|
:disabled="processing"
|
||||||
|
>
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
import VueMultiselect from "vue-multiselect";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import Datepicker from "@vuepic/vue-datepicker";
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
VueMultiselect,
|
||||||
|
Datepicker
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
maintenance: {},
|
||||||
|
affectedMonitors: [],
|
||||||
|
affectedMonitorsOptions: [],
|
||||||
|
showOnAllPages: false,
|
||||||
|
selectedStatusPages: [],
|
||||||
|
dark: (this.$root.theme === "dark"),
|
||||||
|
neverEnd: false,
|
||||||
|
minDate: this.$root.date(dayjs()) + " 00:00",
|
||||||
|
lastDays: [
|
||||||
|
{
|
||||||
|
langKey: "lastDay1",
|
||||||
|
value: "lastDay1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
langKey: "lastDay2",
|
||||||
|
value: "lastDay2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
langKey: "lastDay3",
|
||||||
|
value: "lastDay3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
langKey: "lastDay4",
|
||||||
|
value: "lastDay4",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
weekdays: [
|
||||||
|
{
|
||||||
|
id: "weekday1",
|
||||||
|
langKey: "weekdayShortMon",
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "weekday2",
|
||||||
|
langKey: "weekdayShortTue",
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "weekday3",
|
||||||
|
langKey: "weekdayShortWed",
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "weekday4",
|
||||||
|
langKey: "weekdayShortThu",
|
||||||
|
value: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "weekday5",
|
||||||
|
langKey: "weekdayShortFri",
|
||||||
|
value: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "weekday6",
|
||||||
|
langKey: "weekdayShortSat",
|
||||||
|
value: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "weekday0",
|
||||||
|
langKey: "weekdayShortSun",
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
selectedStatusPagesOptions() {
|
||||||
|
return Object.values(this.$root.statusPageList).map(statusPage => {
|
||||||
|
return {
|
||||||
|
id: statusPage.id,
|
||||||
|
name: statusPage.title
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
pageName() {
|
||||||
|
return this.$t((this.isAdd) ? "Schedule Maintenance" : "Edit Maintenance");
|
||||||
|
},
|
||||||
|
|
||||||
|
isAdd() {
|
||||||
|
return this.$route.path === "/add-maintenance";
|
||||||
|
},
|
||||||
|
|
||||||
|
isEdit() {
|
||||||
|
return this.$route.path.startsWith("/maintenance/edit");
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"$route.fullPath"() {
|
||||||
|
this.init();
|
||||||
|
},
|
||||||
|
|
||||||
|
neverEnd(value) {
|
||||||
|
if (value) {
|
||||||
|
this.maintenance.recurringEndDate = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init();
|
||||||
|
|
||||||
|
this.$root.getMonitorList((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
Object.values(this.$root.monitorList).map(monitor => {
|
||||||
|
this.affectedMonitorsOptions.push({
|
||||||
|
id: monitor.id,
|
||||||
|
name: monitor.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
this.affectedMonitors = [];
|
||||||
|
this.selectedStatusPages = [];
|
||||||
|
|
||||||
|
if (this.isAdd) {
|
||||||
|
this.maintenance = {
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
strategy: "single",
|
||||||
|
active: 1,
|
||||||
|
intervalDay: 1,
|
||||||
|
dateRange: [ this.minDate ],
|
||||||
|
timeRange: [{
|
||||||
|
hours: 2,
|
||||||
|
minutes: 0,
|
||||||
|
}, {
|
||||||
|
hours: 3,
|
||||||
|
minutes: 0,
|
||||||
|
}],
|
||||||
|
weekdays: [],
|
||||||
|
daysOfMonth: [],
|
||||||
|
};
|
||||||
|
} else if (this.isEdit) {
|
||||||
|
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.maintenance = res.maintenance;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
Object.values(res.monitors).map(monitor => {
|
||||||
|
this.affectedMonitors.push(monitor);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("getMaintenanceStatusPage", this.$route.params.id, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
Object.values(res.statusPages).map(statusPage => {
|
||||||
|
this.selectedStatusPages.push({
|
||||||
|
id: statusPage.id,
|
||||||
|
name: statusPage.title
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.showOnAllPages = Object.values(res.statusPages).length === this.selectedStatusPagesOptions.length;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
if (this.affectedMonitors.length === 0) {
|
||||||
|
toast.error(this.$t("atLeastOneMonitor"));
|
||||||
|
return this.processing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAdd) {
|
||||||
|
this.$root.addMaintenance(this.maintenance, async (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
await this.addMonitorMaintenance(res.maintenanceID, async () => {
|
||||||
|
await this.addMaintenanceStatusPage(res.maintenanceID, () => {
|
||||||
|
toast.success(res.msg);
|
||||||
|
this.processing = false;
|
||||||
|
this.$root.getMaintenanceList();
|
||||||
|
this.$router.push("/maintenance");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
this.processing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$root.getSocket().emit("editMaintenance", this.maintenance, async (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
await this.addMonitorMaintenance(res.maintenanceID, async () => {
|
||||||
|
await this.addMaintenanceStatusPage(res.maintenanceID, () => {
|
||||||
|
this.processing = false;
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.init();
|
||||||
|
this.$router.push("/maintenance");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.processing = false;
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async addMonitorMaintenance(maintenanceID, callback) {
|
||||||
|
await this.$root.addMonitorMaintenance(maintenanceID, this.affectedMonitors, async (res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
} else {
|
||||||
|
this.$root.getMonitorList();
|
||||||
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async addMaintenanceStatusPage(maintenanceID, callback) {
|
||||||
|
await this.$root.addMaintenanceStatusPage(maintenanceID, (this.showOnAllPages) ? this.selectedStatusPagesOptions : this.selectedStatusPages, async (res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
} else {
|
||||||
|
this.$root.getMaintenanceList();
|
||||||
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.shadow-box {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-calendar::-webkit-calendar-picker-indicator {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-picker {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 40px;
|
||||||
|
|
||||||
|
.form-check-inline {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-picker {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 40px;
|
||||||
|
|
||||||
|
.form-check-inline {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -24,6 +24,9 @@
|
|||||||
<option value="keyword">
|
<option value="keyword">
|
||||||
HTTP(s) - {{ $t("Keyword") }}
|
HTTP(s) - {{ $t("Keyword") }}
|
||||||
</option>
|
</option>
|
||||||
|
<option value="grpc-keyword">
|
||||||
|
gRPC(s) - {{ $t("Keyword") }}
|
||||||
|
</option>
|
||||||
<option value="dns">
|
<option value="dns">
|
||||||
DNS
|
DNS
|
||||||
</option>
|
</option>
|
||||||
@ -73,6 +76,12 @@
|
|||||||
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
|
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- gRPC URL -->
|
||||||
|
<div v-if="monitor.type === 'grpc-keyword' " class="my-3">
|
||||||
|
<label for="grpc-url" class="form-label">{{ $t("URL") }}</label>
|
||||||
|
<input id="grpc-url" v-model="monitor.grpcUrl" type="url" class="form-control" pattern="[^\:]+:[0-9]{5}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Push URL -->
|
<!-- Push URL -->
|
||||||
<div v-if="monitor.type === 'push' " class="my-3">
|
<div v-if="monitor.type === 'push' " class="my-3">
|
||||||
<label for="push-url" class="form-label">{{ $t("PushUrl") }}</label>
|
<label for="push-url" class="form-label">{{ $t("PushUrl") }}</label>
|
||||||
@ -84,7 +93,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Keyword -->
|
<!-- Keyword -->
|
||||||
<div v-if="monitor.type === 'keyword' " class="my-3">
|
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword' " class="my-3">
|
||||||
<label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
|
<label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
|
||||||
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
|
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
@ -319,7 +328,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- HTTP / Keyword only -->
|
<!-- HTTP / Keyword only -->
|
||||||
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' ">
|
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'grpc-keyword' ">
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
|
<label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
|
||||||
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
|
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
|
||||||
@ -497,6 +506,55 @@
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- gRPC Options -->
|
||||||
|
<template v-if="monitor.type === 'grpc-keyword' ">
|
||||||
|
<!-- Proto service enable TLS -->
|
||||||
|
<h2 class="mt-5 mb-2">{{ $t("GRPC Options") }}</h2>
|
||||||
|
<div class="my-3 form-check">
|
||||||
|
<input id="grpc-enable-tls" v-model="monitor.grpcEnableTls" class="form-check-input" type="checkbox" value="">
|
||||||
|
<label class="form-check-label" for="grpc-enable-tls">
|
||||||
|
{{ $t("Enable TLS") }}
|
||||||
|
</label>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("enableGRPCTls") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Proto service name data -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="protobuf" class="form-label">{{ $t("Proto Service Name") }}</label>
|
||||||
|
<input id="name" v-model="monitor.grpcServiceName" type="text" class="form-control" :placeholder="protoServicePlaceholder" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Proto method data -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="protobuf" class="form-label">{{ $t("Proto Method") }}</label>
|
||||||
|
<input id="name" v-model="monitor.grpcMethod" type="text" class="form-control" :placeholder="protoMethodPlaceholder" required>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("grpcMethodDescription") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Proto data -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="protobuf" class="form-label">{{ $t("Proto Content") }}</label>
|
||||||
|
<textarea id="protobuf" v-model="monitor.grpcProtobuf" class="form-control" :placeholder="protoBufDataPlaceholder"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="body" class="form-label">{{ $t("Body") }}</label>
|
||||||
|
<textarea id="body" v-model="monitor.grpcBody" class="form-control" :placeholder="bodyPlaceholder"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Metadata: temporary disable waiting for next PR allow to send gRPC with metadata -->
|
||||||
|
<template v-if="false">
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="metadata" class="form-label">{{ $t("Metadata") }}</label>
|
||||||
|
<textarea id="metadata" v-model="monitor.grpcMetadata" class="form-control" :placeholder="headersPlaceholder"></textarea>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -575,6 +633,40 @@ export default {
|
|||||||
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
|
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
protoServicePlaceholder() {
|
||||||
|
return this.$t("Example:", [ "Health" ]);
|
||||||
|
},
|
||||||
|
|
||||||
|
protoMethodPlaceholder() {
|
||||||
|
return this.$t("Example:", [ "check" ]);
|
||||||
|
},
|
||||||
|
|
||||||
|
protoBufDataPlaceholder() {
|
||||||
|
return this.$t("Example:", [ `
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package grpc.health.v1;
|
||||||
|
|
||||||
|
service Health {
|
||||||
|
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||||
|
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message HealthCheckRequest {
|
||||||
|
string service = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HealthCheckResponse {
|
||||||
|
enum ServingStatus {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
SERVING = 1;
|
||||||
|
NOT_SERVING = 2;
|
||||||
|
SERVICE_UNKNOWN = 3; // Used only by the Watch method.
|
||||||
|
}
|
||||||
|
ServingStatus status = 1;
|
||||||
|
}
|
||||||
|
` ]);
|
||||||
|
},
|
||||||
bodyPlaceholder() {
|
bodyPlaceholder() {
|
||||||
return this.$t("Example:", [ `
|
return this.$t("Example:", [ `
|
||||||
{
|
{
|
||||||
|
161
src/pages/MaintenanceDetails.vue
Normal file
161
src/pages/MaintenanceDetails.vue
Normal file
@ -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>
|
280
src/pages/ManageMaintenance.vue
Normal file
280
src/pages/ManageMaintenance.vue
Normal file
@ -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>
|
@ -218,12 +218,29 @@
|
|||||||
{{ $t("Degraded Service") }}
|
{{ $t("Degraded Service") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="isMaintenance">
|
||||||
|
<font-awesome-icon icon="wrench" class="status-maintenance" />
|
||||||
|
{{ $t("maintenanceStatus-under-maintenance") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<font-awesome-icon icon="question-circle" style="color: #efefef;" />
|
<font-awesome-icon icon="question-circle" style="color: #efefef;" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Maintenance -->
|
||||||
|
<template v-if="maintenanceList.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="maintenance in maintenanceList" :key="maintenance.id"
|
||||||
|
class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert"
|
||||||
|
>
|
||||||
|
<h4 class="alert-heading">{{ maintenance.title }}</h4>
|
||||||
|
<div class="content">{{ maintenance.description }}</div>
|
||||||
|
<MaintenanceTime :maintenance="maintenance" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<strong v-if="editMode">{{ $t("Description") }}:</strong>
|
<strong v-if="editMode">{{ $t("Description") }}:</strong>
|
||||||
<Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" />
|
<Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" />
|
||||||
@ -295,8 +312,9 @@ import "vue-prism-editor/dist/prismeditor.min.css"; // import the styles somewhe
|
|||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
import Confirm from "../components/Confirm.vue";
|
import Confirm from "../components/Confirm.vue";
|
||||||
import PublicGroupList from "../components/PublicGroupList.vue";
|
import PublicGroupList from "../components/PublicGroupList.vue";
|
||||||
|
import MaintenanceTime from "../components/MaintenanceTime.vue";
|
||||||
import { getResBaseURL } from "../util-frontend";
|
import { getResBaseURL } from "../util-frontend";
|
||||||
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts";
|
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE } from "../util.ts";
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@ -316,6 +334,7 @@ export default {
|
|||||||
ImageCropUpload,
|
ImageCropUpload,
|
||||||
Confirm,
|
Confirm,
|
||||||
PrismEditor,
|
PrismEditor,
|
||||||
|
MaintenanceTime,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Leave Page for vue route change
|
// Leave Page for vue route change
|
||||||
@ -356,6 +375,7 @@ export default {
|
|||||||
loadedData: false,
|
loadedData: false,
|
||||||
baseURL: "",
|
baseURL: "",
|
||||||
clickedEditButton: false,
|
clickedEditButton: false,
|
||||||
|
maintenanceList: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -409,6 +429,10 @@ export default {
|
|||||||
return "bg-" + this.incident.style;
|
return "bg-" + this.incident.style;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
maintenanceClass() {
|
||||||
|
return "bg-maintenance";
|
||||||
|
},
|
||||||
|
|
||||||
overallStatus() {
|
overallStatus() {
|
||||||
|
|
||||||
if (Object.keys(this.$root.publicLastHeartbeatList).length === 0) {
|
if (Object.keys(this.$root.publicLastHeartbeatList).length === 0) {
|
||||||
@ -421,7 +445,9 @@ export default {
|
|||||||
for (let id in this.$root.publicLastHeartbeatList) {
|
for (let id in this.$root.publicLastHeartbeatList) {
|
||||||
let beat = this.$root.publicLastHeartbeatList[id];
|
let beat = this.$root.publicLastHeartbeatList[id];
|
||||||
|
|
||||||
if (beat.status === UP) {
|
if (beat.status === MAINTENANCE) {
|
||||||
|
return STATUS_PAGE_MAINTENANCE;
|
||||||
|
} else if (beat.status === UP) {
|
||||||
hasUp = true;
|
hasUp = true;
|
||||||
} else {
|
} else {
|
||||||
status = STATUS_PAGE_PARTIAL_DOWN;
|
status = STATUS_PAGE_PARTIAL_DOWN;
|
||||||
@ -447,6 +473,10 @@ export default {
|
|||||||
return this.overallStatus === STATUS_PAGE_ALL_DOWN;
|
return this.overallStatus === STATUS_PAGE_ALL_DOWN;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isMaintenance() {
|
||||||
|
return this.overallStatus === STATUS_PAGE_MAINTENANCE;
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
||||||
@ -551,6 +581,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.incident = res.data.incident;
|
this.incident = res.data.incident;
|
||||||
|
this.maintenanceList = res.data.maintenanceList;
|
||||||
this.$root.publicGroupList = res.data.publicGroupList;
|
this.$root.publicGroupList = res.data.publicGroupList;
|
||||||
}).catch( function (error) {
|
}).catch( function (error) {
|
||||||
if (error.response.status === 404) {
|
if (error.response.status === 404) {
|
||||||
@ -946,6 +977,24 @@ footer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maintenance-bg-info {
|
||||||
|
color: $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maintenance-icon {
|
||||||
|
font-size: 35px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .shadow-box {
|
||||||
|
background-color: #0d1117;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-maintenance {
|
||||||
|
color: $maintenance;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile {
|
.mobile {
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
@ -1007,4 +1056,10 @@ footer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-maintenance {
|
||||||
|
.alert-heading {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -6,6 +6,7 @@ import Dashboard from "./pages/Dashboard.vue";
|
|||||||
import DashboardHome from "./pages/DashboardHome.vue";
|
import DashboardHome from "./pages/DashboardHome.vue";
|
||||||
import Details from "./pages/Details.vue";
|
import Details from "./pages/Details.vue";
|
||||||
import EditMonitor from "./pages/EditMonitor.vue";
|
import EditMonitor from "./pages/EditMonitor.vue";
|
||||||
|
import EditMaintenance from "./pages/EditMaintenance.vue";
|
||||||
import List from "./pages/List.vue";
|
import List from "./pages/List.vue";
|
||||||
const Settings = () => import("./pages/Settings.vue");
|
const Settings = () => import("./pages/Settings.vue");
|
||||||
import Setup from "./pages/Setup.vue";
|
import Setup from "./pages/Setup.vue";
|
||||||
@ -14,6 +15,9 @@ import Entry from "./pages/Entry.vue";
|
|||||||
import ManageStatusPage from "./pages/ManageStatusPage.vue";
|
import ManageStatusPage from "./pages/ManageStatusPage.vue";
|
||||||
import AddStatusPage from "./pages/AddStatusPage.vue";
|
import AddStatusPage from "./pages/AddStatusPage.vue";
|
||||||
import NotFound from "./pages/NotFound.vue";
|
import NotFound from "./pages/NotFound.vue";
|
||||||
|
import DockerHosts from "./components/settings/Docker.vue";
|
||||||
|
import MaintenanceDetails from "./pages/MaintenanceDetails.vue";
|
||||||
|
import ManageMaintenance from "./pages/ManageMaintenance.vue";
|
||||||
|
|
||||||
// Settings - Sub Pages
|
// Settings - Sub Pages
|
||||||
import Appearance from "./components/settings/Appearance.vue";
|
import Appearance from "./components/settings/Appearance.vue";
|
||||||
@ -25,7 +29,6 @@ const Security = () => import("./components/settings/Security.vue");
|
|||||||
import Proxies from "./components/settings/Proxies.vue";
|
import Proxies from "./components/settings/Proxies.vue";
|
||||||
import Backup from "./components/settings/Backup.vue";
|
import Backup from "./components/settings/Backup.vue";
|
||||||
import About from "./components/settings/About.vue";
|
import About from "./components/settings/About.vue";
|
||||||
import DockerHosts from "./components/settings/Docker.vue";
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@ -126,6 +129,22 @@ const routes = [
|
|||||||
path: "/add-status-page",
|
path: "/add-status-page",
|
||||||
component: AddStatusPage,
|
component: AddStatusPage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/maintenance",
|
||||||
|
component: ManageMaintenance,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/maintenance/:id",
|
||||||
|
component: MaintenanceDetails,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/add-maintenance",
|
||||||
|
component: EditMaintenance,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/maintenance/edit/:id",
|
||||||
|
component: EditMaintenance,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
|
||||||
import utc from "dayjs/plugin/utc";
|
|
||||||
import timezones from "timezones-list";
|
import timezones from "timezones-list";
|
||||||
import { localeDirection, currentLocale } from "./i18n";
|
import { localeDirection, currentLocale } from "./i18n";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the offset from UTC in hours for the current locale.
|
* Returns the offset from UTC in hours for the current locale.
|
||||||
* @returns {number} The offset from UTC in hours.
|
* @returns {number} The offset from UTC in hours.
|
||||||
|
80
src/util.js
80
src/util.js
@ -7,17 +7,21 @@
|
|||||||
// Backend uses the compiled file util.js
|
// Backend uses the compiled file util.js
|
||||||
// Frontend uses util.ts
|
// Frontend uses util.ts
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
|
exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
|
||||||
const _dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const dayjs = _dayjs;
|
|
||||||
exports.isDev = process.env.NODE_ENV === "development";
|
exports.isDev = process.env.NODE_ENV === "development";
|
||||||
exports.appName = "Uptime Kuma";
|
exports.appName = "Uptime Kuma";
|
||||||
exports.DOWN = 0;
|
exports.DOWN = 0;
|
||||||
exports.UP = 1;
|
exports.UP = 1;
|
||||||
exports.PENDING = 2;
|
exports.PENDING = 2;
|
||||||
|
exports.MAINTENANCE = 3;
|
||||||
exports.STATUS_PAGE_ALL_DOWN = 0;
|
exports.STATUS_PAGE_ALL_DOWN = 0;
|
||||||
exports.STATUS_PAGE_ALL_UP = 1;
|
exports.STATUS_PAGE_ALL_UP = 1;
|
||||||
exports.STATUS_PAGE_PARTIAL_DOWN = 2;
|
exports.STATUS_PAGE_PARTIAL_DOWN = 2;
|
||||||
|
exports.STATUS_PAGE_MAINTENANCE = 3;
|
||||||
|
exports.SQL_DATE_FORMAT = "YYYY-MM-DD";
|
||||||
|
exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
|
||||||
|
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
|
||||||
/** Flip the status of s */
|
/** Flip the status of s */
|
||||||
function flipStatus(s) {
|
function flipStatus(s) {
|
||||||
if (s === exports.UP) {
|
if (s === exports.UP) {
|
||||||
@ -100,7 +104,7 @@ class Logger {
|
|||||||
}
|
}
|
||||||
module = module.toUpperCase();
|
module = module.toUpperCase();
|
||||||
level = level.toUpperCase();
|
level = level.toUpperCase();
|
||||||
const now = new Date().toISOString();
|
const now = dayjs.tz(new Date()).format();
|
||||||
const formattedMessage = (typeof msg === "string") ? `${now} [${module}] ${level}: ${msg}` : msg;
|
const formattedMessage = (typeof msg === "string") ? `${now} [${module}] ${level}: ${msg}` : msg;
|
||||||
if (level === "INFO") {
|
if (level === "INFO") {
|
||||||
console.info(formattedMessage);
|
console.info(formattedMessage);
|
||||||
@ -303,3 +307,71 @@ function getMonitorRelativeURL(id) {
|
|||||||
return "/dashboard/" + id;
|
return "/dashboard/" + id;
|
||||||
}
|
}
|
||||||
exports.getMonitorRelativeURL = getMonitorRelativeURL;
|
exports.getMonitorRelativeURL = getMonitorRelativeURL;
|
||||||
|
function getMaintenanceRelativeURL(id) {
|
||||||
|
return "/maintenance/" + id;
|
||||||
|
}
|
||||||
|
exports.getMaintenanceRelativeURL = getMaintenanceRelativeURL;
|
||||||
|
/**
|
||||||
|
* Parse to Time Object that used in VueDatePicker
|
||||||
|
* @param {string} time E.g. 12:00
|
||||||
|
* @returns object
|
||||||
|
*/
|
||||||
|
function parseTimeObject(time) {
|
||||||
|
if (!time) {
|
||||||
|
return {
|
||||||
|
hours: 0,
|
||||||
|
minutes: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let array = time.split(":");
|
||||||
|
if (array.length < 2) {
|
||||||
|
throw new Error("parseVueDatePickerTimeFormat: Invalid Time");
|
||||||
|
}
|
||||||
|
let obj = {
|
||||||
|
hours: parseInt(array[0]),
|
||||||
|
minutes: parseInt(array[1]),
|
||||||
|
seconds: 0,
|
||||||
|
};
|
||||||
|
if (array.length >= 3) {
|
||||||
|
obj.seconds = parseInt(array[2]);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
exports.parseTimeObject = parseTimeObject;
|
||||||
|
/**
|
||||||
|
* @returns string e.g. 12:00
|
||||||
|
*/
|
||||||
|
function parseTimeFromTimeObject(obj) {
|
||||||
|
if (!obj) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
let result = "";
|
||||||
|
result += obj.hours.toString().padStart(2, "0") + ":" + obj.minutes.toString().padStart(2, "0");
|
||||||
|
if (obj.seconds) {
|
||||||
|
result += ":" + obj.seconds.toString().padStart(2, "0");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
|
||||||
|
function isoToUTCDateTime(input) {
|
||||||
|
return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
|
||||||
|
}
|
||||||
|
exports.isoToUTCDateTime = isoToUTCDateTime;
|
||||||
|
/**
|
||||||
|
* @param input
|
||||||
|
*/
|
||||||
|
function utcToISODateTime(input) {
|
||||||
|
return dayjs.utc(input).toISOString();
|
||||||
|
}
|
||||||
|
exports.utcToISODateTime = utcToISODateTime;
|
||||||
|
/**
|
||||||
|
* For SQL_DATETIME_FORMAT
|
||||||
|
*/
|
||||||
|
function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
|
||||||
|
return dayjs.utc(input).local().format(format);
|
||||||
|
}
|
||||||
|
exports.utcToLocal = utcToLocal;
|
||||||
|
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
|
||||||
|
return dayjs(input).utc().format(format);
|
||||||
|
}
|
||||||
|
exports.localToUTC = localToUTC;
|
||||||
|
89
src/util.ts
89
src/util.ts
@ -6,18 +6,25 @@
|
|||||||
// Backend uses the compiled file util.js
|
// Backend uses the compiled file util.js
|
||||||
// Frontend uses util.ts
|
// Frontend uses util.ts
|
||||||
|
|
||||||
import * as _dayjs from "dayjs";
|
import * as dayjs from "dayjs";
|
||||||
const dayjs = _dayjs;
|
import * as timezone from "dayjs/plugin/timezone";
|
||||||
|
import * as utc from "dayjs/plugin/utc";
|
||||||
|
|
||||||
export const isDev = process.env.NODE_ENV === "development";
|
export const isDev = process.env.NODE_ENV === "development";
|
||||||
export const appName = "Uptime Kuma";
|
export const appName = "Uptime Kuma";
|
||||||
export const DOWN = 0;
|
export const DOWN = 0;
|
||||||
export const UP = 1;
|
export const UP = 1;
|
||||||
export const PENDING = 2;
|
export const PENDING = 2;
|
||||||
|
export const MAINTENANCE = 3;
|
||||||
|
|
||||||
export const STATUS_PAGE_ALL_DOWN = 0;
|
export const STATUS_PAGE_ALL_DOWN = 0;
|
||||||
export const STATUS_PAGE_ALL_UP = 1;
|
export const STATUS_PAGE_ALL_UP = 1;
|
||||||
export const STATUS_PAGE_PARTIAL_DOWN = 2;
|
export const STATUS_PAGE_PARTIAL_DOWN = 2;
|
||||||
|
export const STATUS_PAGE_MAINTENANCE = 3;
|
||||||
|
|
||||||
|
export const SQL_DATE_FORMAT = "YYYY-MM-DD";
|
||||||
|
export const SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
|
||||||
|
export const SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
|
||||||
|
|
||||||
/** Flip the status of s */
|
/** Flip the status of s */
|
||||||
export function flipStatus(s: number) {
|
export function flipStatus(s: number) {
|
||||||
@ -112,7 +119,7 @@ class Logger {
|
|||||||
module = module.toUpperCase();
|
module = module.toUpperCase();
|
||||||
level = level.toUpperCase();
|
level = level.toUpperCase();
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = dayjs.tz(new Date()).format();
|
||||||
const formattedMessage = (typeof msg === "string") ? `${now} [${module}] ${level}: ${msg}` : msg;
|
const formattedMessage = (typeof msg === "string") ? `${now} [${module}] ${level}: ${msg}` : msg;
|
||||||
|
|
||||||
if (level === "INFO") {
|
if (level === "INFO") {
|
||||||
@ -336,3 +343,79 @@ export function genSecret(length = 64) {
|
|||||||
export function getMonitorRelativeURL(id: string) {
|
export function getMonitorRelativeURL(id: string) {
|
||||||
return "/dashboard/" + id;
|
return "/dashboard/" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMaintenanceRelativeURL(id: string) {
|
||||||
|
return "/maintenance/" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to Time Object that used in VueDatePicker
|
||||||
|
* @param {string} time E.g. 12:00
|
||||||
|
* @returns object
|
||||||
|
*/
|
||||||
|
export function parseTimeObject(time: string) {
|
||||||
|
if (!time) {
|
||||||
|
return {
|
||||||
|
hours: 0,
|
||||||
|
minutes: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let array = time.split(":");
|
||||||
|
|
||||||
|
if (array.length < 2) {
|
||||||
|
throw new Error("parseVueDatePickerTimeFormat: Invalid Time");
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj = {
|
||||||
|
hours: parseInt(array[0]),
|
||||||
|
minutes: parseInt(array[1]),
|
||||||
|
seconds: 0,
|
||||||
|
}
|
||||||
|
if (array.length >= 3) {
|
||||||
|
obj.seconds = parseInt(array[2]);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns string e.g. 12:00
|
||||||
|
*/
|
||||||
|
export function parseTimeFromTimeObject(obj : any) {
|
||||||
|
if (!obj) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = "";
|
||||||
|
|
||||||
|
result += obj.hours.toString().padStart(2, "0") + ":" + obj.minutes.toString().padStart(2, "0")
|
||||||
|
|
||||||
|
if (obj.seconds) {
|
||||||
|
result += ":" + obj.seconds.toString().padStart(2, "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function isoToUTCDateTime(input : string) {
|
||||||
|
return dayjs(input).utc().format(SQL_DATETIME_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param input
|
||||||
|
*/
|
||||||
|
export function utcToISODateTime(input : string) {
|
||||||
|
return dayjs.utc(input).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For SQL_DATETIME_FORMAT
|
||||||
|
*/
|
||||||
|
export function utcToLocal(input : string, format = SQL_DATETIME_FORMAT) {
|
||||||
|
return dayjs.utc(input).local().format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function localToUTC(input : string, format = SQL_DATETIME_FORMAT) {
|
||||||
|
return dayjs(input).utc().format(format);
|
||||||
|
}
|
||||||
|
@ -6,6 +6,9 @@ const { UptimeKumaServer } = require("../server/uptime-kuma-server");
|
|||||||
const Database = require("../server/database");
|
const Database = require("../server/database");
|
||||||
const {Settings} = require("../server/settings");
|
const {Settings} = require("../server/settings");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
dayjs.extend(require("dayjs/plugin/utc"));
|
||||||
|
dayjs.extend(require("dayjs/plugin/timezone"));
|
||||||
|
|
||||||
jest.mock("axios");
|
jest.mock("axios");
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user