mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-03-03 16:05:56 +00:00
Merge branch 'master' into zookeeper-check
This commit is contained in:
commit
719bcfc0fa
26 changed files with 758 additions and 129 deletions
26
.github/workflows/conflict_labeler.yml
vendored
Normal file
26
.github/workflows/conflict_labeler.yml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
name: Merge Conflict Labeler
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
label:
|
||||
name: Labeling
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'louislam/uptime-kuma' }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Apply label
|
||||
uses: eps1lon/actions-label-merge-conflict@v3
|
||||
with:
|
||||
dirtyLabel: 'needs:resolve-merge-conflict'
|
||||
removeOnDirtyLabel: 'needs:resolve-merge-conflict'
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
12
db/knex_migrations/2023-12-20-0000-alter-status-page.js
Normal file
12
db/knex_migrations/2023-12-20-0000-alter-status-page.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("status_page", function (table) {
|
||||
table.integer("auto_refresh_interval").defaultTo(300).unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("status_page", function (table) {
|
||||
table.dropColumn("auto_refresh_interval");
|
||||
});
|
||||
};
|
|
@ -824,15 +824,6 @@ class Monitor extends BeanModel {
|
|||
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1", mysqlPassword);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "mongodb") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
await mongodbPing(this.databaseConnectionString);
|
||||
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
} else if (this.type === "radius") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
|
@ -863,7 +854,7 @@ class Monitor extends BeanModel {
|
|||
} else if (this.type === "redis") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
bean.msg = await redisPingAsync(this.databaseConnectionString);
|
||||
bean.msg = await redisPingAsync(this.databaseConnectionString, !this.ignoreTls);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
|
|
|
@ -238,6 +238,7 @@ class StatusPage extends BeanModel {
|
|||
description: this.description,
|
||||
icon: this.getIcon(),
|
||||
theme: this.theme,
|
||||
autoRefreshInterval: this.autoRefreshInterval,
|
||||
published: !!this.published,
|
||||
showTags: !!this.show_tags,
|
||||
domainNameList: this.getDomainNameList(),
|
||||
|
@ -260,6 +261,7 @@ class StatusPage extends BeanModel {
|
|||
title: this.title,
|
||||
description: this.description,
|
||||
icon: this.getIcon(),
|
||||
autoRefreshInterval: this.autoRefreshInterval,
|
||||
theme: this.theme,
|
||||
published: !!this.published,
|
||||
showTags: !!this.show_tags,
|
||||
|
|
65
server/monitor-types/mongodb.js
Normal file
65
server/monitor-types/mongodb.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP } = require("../../src/util");
|
||||
const { MongoClient } = require("mongodb");
|
||||
const jsonata = require("jsonata");
|
||||
|
||||
class MongodbMonitorType extends MonitorType {
|
||||
|
||||
name = "mongodb";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
let command = { "ping": 1 };
|
||||
if (monitor.databaseQuery) {
|
||||
command = JSON.parse(monitor.databaseQuery);
|
||||
}
|
||||
|
||||
let result = await this.runMongodbCommand(monitor.databaseConnectionString, command);
|
||||
|
||||
if (result["ok"] !== 1) {
|
||||
throw new Error("MongoDB command failed");
|
||||
} else {
|
||||
heartbeat.msg = "Command executed successfully";
|
||||
}
|
||||
|
||||
if (monitor.jsonPath) {
|
||||
let expression = jsonata(monitor.jsonPath);
|
||||
result = await expression.evaluate(result);
|
||||
if (result) {
|
||||
heartbeat.msg = "Command executed successfully and the jsonata expression produces a result.";
|
||||
} else {
|
||||
throw new Error("Queried value not found.");
|
||||
}
|
||||
}
|
||||
|
||||
if (monitor.expectedValue) {
|
||||
if (result.toString() === monitor.expectedValue) {
|
||||
heartbeat.msg = "Command executed successfully and expected value was found";
|
||||
} else {
|
||||
throw new Error("Query executed, but value is not equal to expected value, value was: [" + JSON.stringify(result) + "]");
|
||||
}
|
||||
}
|
||||
|
||||
heartbeat.status = UP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to and run MongoDB command on a MongoDB database
|
||||
* @param {string} connectionString The database connection string
|
||||
* @param {object} command MongoDB command to run on the database
|
||||
* @returns {Promise<(string[] | object[] | object)>} Response from
|
||||
* server
|
||||
*/
|
||||
async runMongodbCommand(connectionString, command) {
|
||||
let client = await MongoClient.connect(connectionString);
|
||||
let result = await client.db().command(command);
|
||||
await client.close();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MongodbMonitorType,
|
||||
};
|
31
server/notification-providers/bitrix24.js
Normal file
31
server/notification-providers/bitrix24.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { UP } = require("../../src/util");
|
||||
|
||||
class Bitrix24 extends NotificationProvider {
|
||||
name = "Bitrix24";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
const params = {
|
||||
user_id: notification.bitrix24UserID,
|
||||
message: "[B]Uptime Kuma[/B]",
|
||||
"ATTACH[COLOR]": (heartbeatJSON ?? {})["status"] === UP ? "#b73419" : "#67b518",
|
||||
"ATTACH[BLOCKS][0][MESSAGE]": msg
|
||||
};
|
||||
|
||||
await axios.get(`${notification.bitrix24WebhookURL}/im.notify.system.add.json`, { params });
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bitrix24;
|
|
@ -13,6 +13,10 @@ class Discord extends NotificationProvider {
|
|||
|
||||
try {
|
||||
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
|
||||
const webhookUrl = new URL(notification.discordWebhookUrl);
|
||||
if (notification.discordChannelType === "postToThread") {
|
||||
webhookUrl.searchParams.append("thread_id", notification.threadId);
|
||||
}
|
||||
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if (heartbeatJSON == null) {
|
||||
|
@ -20,7 +24,12 @@ class Discord extends NotificationProvider {
|
|||
username: discordDisplayName,
|
||||
content: msg,
|
||||
};
|
||||
await axios.post(notification.discordWebhookUrl, discordtestdata);
|
||||
|
||||
if (notification.discordChannelType === "createNewForumPost") {
|
||||
discordtestdata.thread_name = notification.postName;
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl.toString(), discordtestdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
|
@ -72,12 +81,14 @@ class Discord extends NotificationProvider {
|
|||
],
|
||||
}],
|
||||
};
|
||||
|
||||
if (notification.discordChannelType === "createNewForumPost") {
|
||||
discorddowndata.thread_name = notification.postName;
|
||||
}
|
||||
if (notification.discordPrefixMessage) {
|
||||
discorddowndata.content = notification.discordPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(notification.discordWebhookUrl, discorddowndata);
|
||||
await axios.post(webhookUrl.toString(), discorddowndata);
|
||||
return okMsg;
|
||||
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
|
@ -108,11 +119,15 @@ class Discord extends NotificationProvider {
|
|||
}],
|
||||
};
|
||||
|
||||
if (notification.discordChannelType === "createNewForumPost") {
|
||||
discordupdata.thread_name = notification.postName;
|
||||
}
|
||||
|
||||
if (notification.discordPrefixMessage) {
|
||||
discordupdata.content = notification.discordPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(notification.discordWebhookUrl, discordupdata);
|
||||
await axios.post(webhookUrl.toString(), discordupdata);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
@ -62,6 +62,15 @@ class FlashDuty extends NotificationProvider {
|
|||
* @returns {string} Success message
|
||||
*/
|
||||
async postNotification(notification, title, body, monitorInfo, eventStatus) {
|
||||
let labels = {
|
||||
resource: this.genMonitorUrl(monitorInfo),
|
||||
check: monitorInfo.name,
|
||||
};
|
||||
if (monitorInfo.tags && monitorInfo.tags.length > 0) {
|
||||
for (let tag of monitorInfo.tags) {
|
||||
labels[tag.name] = tag.value;
|
||||
}
|
||||
}
|
||||
const options = {
|
||||
method: "POST",
|
||||
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
|
||||
|
@ -71,9 +80,7 @@ class FlashDuty extends NotificationProvider {
|
|||
title,
|
||||
event_status: eventStatus || "Info",
|
||||
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7),
|
||||
labels: monitorInfo?.tags?.reduce((acc, item) => ({ ...acc,
|
||||
[item.name]: item.value
|
||||
}), { resource: this.genMonitorUrl(monitorInfo) }),
|
||||
labels,
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ const AlertNow = require("./notification-providers/alertnow");
|
|||
const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||
const Apprise = require("./notification-providers/apprise");
|
||||
const Bark = require("./notification-providers/bark");
|
||||
const Bitrix24 = require("./notification-providers/bitrix24");
|
||||
const ClickSendSMS = require("./notification-providers/clicksendsms");
|
||||
const CallMeBot = require("./notification-providers/call-me-bot");
|
||||
const SMSC = require("./notification-providers/smsc");
|
||||
|
@ -83,6 +84,7 @@ class Notification {
|
|||
new AliyunSms(),
|
||||
new Apprise(),
|
||||
new Bark(),
|
||||
new Bitrix24(),
|
||||
new ClickSendSMS(),
|
||||
new CallMeBot(),
|
||||
new SMSC(),
|
||||
|
|
|
@ -149,6 +149,7 @@ const apicache = require("./modules/apicache");
|
|||
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
|
||||
const { EmbeddedMariaDB } = require("./embedded-mariadb");
|
||||
const { SetupDatabase } = require("./setup-database");
|
||||
const { chartSocketHandler } = require("./socket-handlers/chart-socket-handler");
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
|
@ -1530,6 +1531,7 @@ let needSetup = false;
|
|||
apiKeySocketHandler(socket);
|
||||
remoteBrowserSocketHandler(socket);
|
||||
generalSocketHandler(socket, server);
|
||||
chartSocketHandler(socket);
|
||||
|
||||
log.debug("server", "added all socket handlers");
|
||||
|
||||
|
|
38
server/socket-handlers/chart-socket-handler.js
Normal file
38
server/socket-handlers/chart-socket-handler.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
const { checkLogin } = require("../util-server");
|
||||
const { UptimeCalculator } = require("../uptime-calculator");
|
||||
const { log } = require("../../src/util");
|
||||
|
||||
module.exports.chartSocketHandler = (socket) => {
|
||||
socket.on("getMonitorChartData", async (monitorID, period, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
log.debug("monitor", `Get Monitor Chart Data: ${monitorID} User ID: ${socket.userID}`);
|
||||
|
||||
if (period == null) {
|
||||
throw new Error("Invalid period.");
|
||||
}
|
||||
|
||||
let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID);
|
||||
|
||||
let data;
|
||||
if (period <= 24) {
|
||||
data = uptimeCalculator.getDataArray(period * 60, "minute");
|
||||
} else if (period <= 720) {
|
||||
data = uptimeCalculator.getDataArray(period, "hour");
|
||||
} else {
|
||||
data = uptimeCalculator.getDataArray(period / 24, "day");
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
data,
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
|
@ -155,6 +155,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||
statusPage.title = config.title;
|
||||
statusPage.description = config.description;
|
||||
statusPage.icon = config.logo;
|
||||
statusPage.autoRefreshInterval = config.autoRefreshInterval,
|
||||
statusPage.theme = config.theme;
|
||||
//statusPage.published = ;
|
||||
//statusPage.search_engine_index = ;
|
||||
|
@ -280,6 +281,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||
statusPage.title = title;
|
||||
statusPage.theme = "auto";
|
||||
statusPage.icon = "";
|
||||
statusPage.autoRefreshInterval = 300;
|
||||
await R.store(statusPage);
|
||||
|
||||
callback({
|
||||
|
|
|
@ -290,7 +290,7 @@ class UptimeCalculator {
|
|||
dailyStatBean.pingMax = dailyData.maxPing;
|
||||
{
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { up, down, avgPing, minPing, maxPing, ...extras } = dailyData;
|
||||
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = dailyData;
|
||||
if (Object.keys(extras).length > 0) {
|
||||
dailyStatBean.extras = JSON.stringify(extras);
|
||||
}
|
||||
|
@ -305,7 +305,7 @@ class UptimeCalculator {
|
|||
hourlyStatBean.pingMax = hourlyData.maxPing;
|
||||
{
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { up, down, avgPing, minPing, maxPing, ...extras } = hourlyData;
|
||||
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = hourlyData;
|
||||
if (Object.keys(extras).length > 0) {
|
||||
hourlyStatBean.extras = JSON.stringify(extras);
|
||||
}
|
||||
|
@ -320,7 +320,7 @@ class UptimeCalculator {
|
|||
minutelyStatBean.pingMax = minutelyData.maxPing;
|
||||
{
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { up, down, avgPing, minPing, maxPing, ...extras } = minutelyData;
|
||||
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = minutelyData;
|
||||
if (Object.keys(extras).length > 0) {
|
||||
minutelyStatBean.extras = JSON.stringify(extras);
|
||||
}
|
||||
|
|
|
@ -113,6 +113,7 @@ class UptimeKumaServer {
|
|||
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
||||
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
|
||||
|
||||
// Allow all CORS origins (polling) in development
|
||||
let cors = undefined;
|
||||
|
@ -516,3 +517,4 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor
|
|||
const { TailscalePing } = require("./monitor-types/tailscale-ping");
|
||||
const { DnsMonitorType } = require("./monitor-types/dns");
|
||||
const { MqttMonitorType } = require("./monitor-types/mqtt");
|
||||
const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
||||
|
|
|
@ -11,7 +11,6 @@ const mssql = require("mssql");
|
|||
const { Client } = require("pg");
|
||||
const postgresConParse = require("pg-connection-string").parse;
|
||||
const mysql = require("mysql2");
|
||||
const { MongoClient } = require("mongodb");
|
||||
const { NtlmClient } = require("axios-ntlm");
|
||||
const { Settings } = require("./settings");
|
||||
const grpc = require("@grpc/grpc-js");
|
||||
|
@ -438,24 +437,6 @@ exports.mysqlQuery = function (connectionString, query, password = undefined) {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect to and ping a MongoDB database
|
||||
* @param {string} connectionString The database connection string
|
||||
* @returns {Promise<(string[] | object[] | object)>} Response from
|
||||
* server
|
||||
*/
|
||||
exports.mongodbPing = async function (connectionString) {
|
||||
let client = await MongoClient.connect(connectionString);
|
||||
let dbPing = await client.db().command({ ping: 1 });
|
||||
await client.close();
|
||||
|
||||
if (dbPing["ok"] === 1) {
|
||||
return "UP";
|
||||
} else {
|
||||
throw Error("failed");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Query radius server
|
||||
* @param {string} hostname Hostname of radius server
|
||||
|
@ -506,12 +487,16 @@ exports.radius = function (
|
|||
/**
|
||||
* Redis server ping
|
||||
* @param {string} dsn The redis connection string
|
||||
* @returns {Promise<any>} Response from redis server
|
||||
* @param {boolean} rejectUnauthorized If false, allows unverified server certificates.
|
||||
* @returns {Promise<any>} Response from server
|
||||
*/
|
||||
exports.redisPingAsync = function (dsn) {
|
||||
exports.redisPingAsync = function (dsn, rejectUnauthorized) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = redis.createClient({
|
||||
url: dsn
|
||||
url: dsn,
|
||||
socket: {
|
||||
rejectUnauthorized
|
||||
}
|
||||
});
|
||||
client.on("error", (err) => {
|
||||
if (client.isOpen) {
|
||||
|
|
|
@ -114,6 +114,7 @@ export default {
|
|||
"AlertNow": "AlertNow",
|
||||
"apprise": this.$t("apprise"),
|
||||
"Bark": "Bark",
|
||||
"Bitrix24": "Bitrix24",
|
||||
"clicksendsms": "ClickSend SMS",
|
||||
"CallMeBot": "CallMeBot (WhatsApp, Telegram Call, Facebook Messanger)",
|
||||
"discord": "Discord",
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="period-options">
|
||||
<button type="button" class="btn btn-light dropdown-toggle btn-period-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<button
|
||||
type="button" class="btn btn-light dropdown-toggle btn-period-toggle" data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{{ chartPeriodOptions[chartPeriodHrs] }}
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li v-for="(item, key) in chartPeriodOptions" :key="key">
|
||||
<a class="dropdown-item" :class="{ active: chartPeriodHrs == key }" href="#" @click="chartPeriodHrs = key">{{ item }}</a>
|
||||
<button
|
||||
type="button" class="dropdown-item" :class="{ active: chartPeriodHrs == key }"
|
||||
@click="chartPeriodHrs = key"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="chart-wrapper" :class="{ loading : loading}">
|
||||
<div class="chart-wrapper" :class="{ loading: loading }">
|
||||
<Line :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,9 +27,8 @@
|
|||
<script lang="js">
|
||||
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
|
||||
import "chartjs-adapter-dayjs-4";
|
||||
import dayjs from "dayjs";
|
||||
import { Line } from "vue-chartjs";
|
||||
import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts";
|
||||
import { UP, DOWN, PENDING, MAINTENANCE } from "../util.ts";
|
||||
|
||||
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
|
||||
|
||||
|
@ -39,8 +46,9 @@ export default {
|
|||
|
||||
loading: false,
|
||||
|
||||
// Configurable filtering on top of the returned data
|
||||
chartPeriodHrs: 0,
|
||||
// Time period for the chart to display, in hours
|
||||
// Initial value is 0 as a workaround for triggering a data fetch on created()
|
||||
chartPeriodHrs: "0",
|
||||
|
||||
chartPeriodOptions: {
|
||||
0: this.$t("recent"),
|
||||
|
@ -50,9 +58,8 @@ export default {
|
|||
168: "1w",
|
||||
},
|
||||
|
||||
// A heartbeatList for 3h, 6h, 24h, 1w
|
||||
// Uses the $root.heartbeatList when value is null
|
||||
heartbeatList: null
|
||||
chartRawData: null,
|
||||
chartDataFetchInterval: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -157,34 +164,197 @@ export default {
|
|||
};
|
||||
},
|
||||
chartData() {
|
||||
if (this.chartPeriodHrs === "0") {
|
||||
return this.getChartDatapointsFromHeartbeatList();
|
||||
} else {
|
||||
return this.getChartDatapointsFromStats();
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// Update chart data when the selected chart period changes
|
||||
chartPeriodHrs: function (newPeriod) {
|
||||
if (this.chartDataFetchInterval) {
|
||||
clearInterval(this.chartDataFetchInterval);
|
||||
this.chartDataFetchInterval = null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (newPeriod == "0") {
|
||||
this.heartbeatList = null;
|
||||
this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
|
||||
} else {
|
||||
this.loading = true;
|
||||
|
||||
let period;
|
||||
try {
|
||||
period = parseInt(newPeriod);
|
||||
} catch (e) {
|
||||
// Invalid period
|
||||
period = 24;
|
||||
}
|
||||
|
||||
this.$root.getMonitorChartData(this.monitorId, period, (res) => {
|
||||
if (!res.ok) {
|
||||
this.$root.toastError(res.msg);
|
||||
} else {
|
||||
this.chartRawData = res.data;
|
||||
this.$root.storage()[`chart-period-${this.monitorId}`] = newPeriod;
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
this.chartDataFetchInterval = setInterval(() => {
|
||||
this.$root.getMonitorChartData(this.monitorId, period, (res) => {
|
||||
if (res.ok) {
|
||||
this.chartRawData = res.data;
|
||||
}
|
||||
});
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// Load chart period from storage if saved
|
||||
let period = this.$root.storage()[`chart-period-${this.monitorId}`];
|
||||
if (period != null) {
|
||||
// Has this ever been not a string?
|
||||
if (typeof period !== "string") {
|
||||
period = period.toString();
|
||||
}
|
||||
this.chartPeriodHrs = period;
|
||||
} else {
|
||||
this.chartPeriodHrs = "24";
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.chartDataFetchInterval) {
|
||||
clearInterval(this.chartDataFetchInterval);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Get color of bar chart for this datapoint
|
||||
getBarColorForDatapoint(datapoint) {
|
||||
if (datapoint.maintenance != null) {
|
||||
// Target is in maintenance
|
||||
return "rgba(23,71,245,0.41)";
|
||||
} else if (datapoint.down === 0) {
|
||||
// Target is up, no need to display a bar
|
||||
return "#000";
|
||||
} else if (datapoint.up === 0) {
|
||||
// Target is down
|
||||
return "rgba(220, 53, 69, 0.41)";
|
||||
} else {
|
||||
// Show yellow for mixed status
|
||||
return "rgba(245, 182, 23, 0.41)";
|
||||
}
|
||||
},
|
||||
// push datapoint to chartData
|
||||
pushDatapoint(datapoint, avgPingData, minPingData, maxPingData, downData, colorData) {
|
||||
const x = this.$root.unixToDateTime(datapoint.timestamp);
|
||||
|
||||
// Show ping values if it was up in this period
|
||||
avgPingData.push({
|
||||
x,
|
||||
y: datapoint.up > 0 && datapoint.avgPing > 0 ? datapoint.avgPing : null,
|
||||
});
|
||||
minPingData.push({
|
||||
x,
|
||||
y: datapoint.up > 0 && datapoint.avgPing > 0 ? datapoint.minPing : null,
|
||||
});
|
||||
maxPingData.push({
|
||||
x,
|
||||
y: datapoint.up > 0 && datapoint.avgPing > 0 ? datapoint.maxPing : null,
|
||||
});
|
||||
downData.push({
|
||||
x,
|
||||
y: datapoint.down + (datapoint.maintenance || 0),
|
||||
});
|
||||
|
||||
colorData.push(this.getBarColorForDatapoint(datapoint));
|
||||
},
|
||||
// get the average of a set of datapoints
|
||||
getAverage(datapoints) {
|
||||
const totalUp = datapoints.reduce((total, current) => total + current.up, 0);
|
||||
const totalDown = datapoints.reduce((total, current) => total + current.down, 0);
|
||||
const totalMaintenance = datapoints.reduce((total, current) => total + (current.maintenance || 0), 0);
|
||||
const totalPing = datapoints.reduce((total, current) => total + current.avgPing * current.up, 0);
|
||||
const minPing = datapoints.reduce((min, current) => Math.min(min, current.minPing), Infinity);
|
||||
const maxPing = datapoints.reduce((max, current) => Math.max(max, current.maxPing), 0);
|
||||
|
||||
// Find the middle timestamp to use
|
||||
let midpoint = Math.floor(datapoints.length / 2);
|
||||
|
||||
return {
|
||||
timestamp: datapoints[midpoint].timestamp,
|
||||
up: totalUp,
|
||||
down: totalDown,
|
||||
maintenance: totalMaintenance > 0 ? totalMaintenance : undefined,
|
||||
avgPing: totalUp > 0 ? totalPing / totalUp : 0,
|
||||
minPing,
|
||||
maxPing,
|
||||
};
|
||||
},
|
||||
getChartDatapointsFromHeartbeatList() {
|
||||
// Render chart using heartbeatList
|
||||
let lastHeartbeatTime;
|
||||
const monitorInterval = this.$root.monitorList[this.monitorId]?.interval;
|
||||
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 (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 ||
|
||||
(this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
|
||||
[];
|
||||
let heartbeatList = (this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) || [];
|
||||
|
||||
heartbeatList
|
||||
.filter(
|
||||
// Filtering as data gets appended
|
||||
// not the most efficient, but works for now
|
||||
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(
|
||||
dayjs().subtract(Math.max(this.chartPeriodHrs, 6), "hours")
|
||||
)
|
||||
)
|
||||
.map((beat) => {
|
||||
const x = this.$root.datetime(beat.time);
|
||||
pingData.push({
|
||||
x,
|
||||
y: beat.ping,
|
||||
});
|
||||
downData.push({
|
||||
x,
|
||||
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"));
|
||||
for (const beat of heartbeatList) {
|
||||
const beatTime = this.$root.toDayjs(beat.time);
|
||||
const x = beatTime.format("YYYY-MM-DD HH:mm:ss");
|
||||
|
||||
// Insert empty datapoint to separate big gaps
|
||||
if (lastHeartbeatTime && monitorInterval) {
|
||||
const diff = Math.abs(beatTime.diff(lastHeartbeatTime));
|
||||
if (diff > monitorInterval * 1000 * 10) {
|
||||
// Big gap detected
|
||||
const gapX = [
|
||||
lastHeartbeatTime.add(monitorInterval, "second").format("YYYY-MM-DD HH:mm:ss"),
|
||||
beatTime.subtract(monitorInterval, "second").format("YYYY-MM-DD HH:mm:ss")
|
||||
];
|
||||
|
||||
for (const x of gapX) {
|
||||
pingData.push({
|
||||
x,
|
||||
y: null,
|
||||
});
|
||||
downData.push({
|
||||
x,
|
||||
y: null,
|
||||
});
|
||||
colorData.push("#000");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
pingData.push({
|
||||
x,
|
||||
y: beat.status === UP ? beat.ping : null,
|
||||
});
|
||||
downData.push({
|
||||
x,
|
||||
y: (beat.status === DOWN || beat.status === MAINTENANCE || beat.status === PENDING) ? 1 : 0,
|
||||
});
|
||||
switch (beat.status) {
|
||||
case MAINTENANCE:
|
||||
colorData.push("rgba(23 ,71, 245, 0.41)");
|
||||
break;
|
||||
case PENDING:
|
||||
colorData.push("rgba(245, 182, 23, 0.41)");
|
||||
break;
|
||||
default:
|
||||
colorData.push("rgba(220, 53, 69, 0.41)");
|
||||
}
|
||||
|
||||
lastHeartbeatTime = beatTime;
|
||||
}
|
||||
|
||||
return {
|
||||
datasets: [
|
||||
|
@ -214,54 +384,155 @@ export default {
|
|||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// Update chart data when the selected chart period changes
|
||||
chartPeriodHrs: function (newPeriod) {
|
||||
getChartDatapointsFromStats() {
|
||||
// Render chart using UptimeCalculator data
|
||||
let lastHeartbeatTime;
|
||||
const monitorInterval = this.$root.monitorList[this.monitorId]?.interval;
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (newPeriod == "0") {
|
||||
this.heartbeatList = null;
|
||||
this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
|
||||
} else {
|
||||
this.loading = true;
|
||||
let avgPingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
||||
let minPingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
||||
let maxPingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
||||
let downData = []; // Down Data for Bar Chart, y-axis is number of down datapoints in this period
|
||||
let colorData = []; // Color Data for Bar Chart
|
||||
|
||||
this.$root.getMonitorBeats(this.monitorId, newPeriod, (res) => {
|
||||
if (!res.ok) {
|
||||
this.$root.toastError(res.msg);
|
||||
const period = parseInt(this.chartPeriodHrs);
|
||||
let aggregatePoints = period > 6 ? 12 : 4;
|
||||
|
||||
let aggregateBuffer = [];
|
||||
|
||||
if (this.chartRawData) {
|
||||
for (const datapoint of this.chartRawData) {
|
||||
// Empty datapoints are ignored
|
||||
if (datapoint.up === 0 && datapoint.down === 0 && datapoint.maintenance === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const beatTime = this.$root.unixToDayjs(datapoint.timestamp);
|
||||
|
||||
// Insert empty datapoint to separate big gaps
|
||||
if (lastHeartbeatTime && monitorInterval) {
|
||||
const diff = Math.abs(beatTime.diff(lastHeartbeatTime));
|
||||
const oneSecond = 1000;
|
||||
const oneMinute = oneSecond * 60;
|
||||
const oneHour = oneMinute * 60;
|
||||
if ((period <= 24 && diff > Math.max(oneMinute * 10, monitorInterval * oneSecond * 10)) ||
|
||||
(period > 24 && diff > Math.max(oneHour * 10, monitorInterval * oneSecond * 10))) {
|
||||
// Big gap detected
|
||||
// Clear the aggregate buffer
|
||||
if (aggregateBuffer.length > 0) {
|
||||
const average = this.getAverage(aggregateBuffer);
|
||||
this.pushDatapoint(average, avgPingData, minPingData, maxPingData, downData, colorData);
|
||||
aggregateBuffer = [];
|
||||
}
|
||||
|
||||
const gapX = [
|
||||
lastHeartbeatTime.subtract(monitorInterval, "second").format("YYYY-MM-DD HH:mm:ss"),
|
||||
this.$root.unixToDateTime(datapoint.timestamp + 60),
|
||||
];
|
||||
|
||||
for (const x of gapX) {
|
||||
avgPingData.push({
|
||||
x,
|
||||
y: null,
|
||||
});
|
||||
minPingData.push({
|
||||
x,
|
||||
y: null,
|
||||
});
|
||||
maxPingData.push({
|
||||
x,
|
||||
y: null,
|
||||
});
|
||||
downData.push({
|
||||
x,
|
||||
y: null,
|
||||
});
|
||||
colorData.push("#000");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (datapoint.up > 0 && this.chartRawData.length > aggregatePoints * 2) {
|
||||
// Aggregate Up data using a sliding window
|
||||
aggregateBuffer.push(datapoint);
|
||||
|
||||
if (aggregateBuffer.length === aggregatePoints) {
|
||||
const average = this.getAverage(aggregateBuffer);
|
||||
this.pushDatapoint(average, avgPingData, minPingData, maxPingData, downData, colorData);
|
||||
// Remove the first half of the buffer
|
||||
aggregateBuffer = aggregateBuffer.slice(Math.floor(aggregatePoints / 2));
|
||||
}
|
||||
} else {
|
||||
this.heartbeatList = res.data;
|
||||
this.$root.storage()[`chart-period-${this.monitorId}`] = newPeriod;
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// Setup Watcher on the root heartbeatList,
|
||||
// And mirror latest change to this.heartbeatList
|
||||
this.$watch(() => this.$root.heartbeatList[this.monitorId],
|
||||
(heartbeatList) => {
|
||||
// datapoint is fully down or too few datapoints, no need to aggregate
|
||||
// Clear the aggregate buffer
|
||||
if (aggregateBuffer.length > 0) {
|
||||
const average = this.getAverage(aggregateBuffer);
|
||||
this.pushDatapoint(average, avgPingData, minPingData, maxPingData, downData, colorData);
|
||||
aggregateBuffer = [];
|
||||
}
|
||||
|
||||
log.debug("ping_chart", `this.chartPeriodHrs type ${typeof this.chartPeriodHrs}, value: ${this.chartPeriodHrs}`);
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.chartPeriodHrs != "0") {
|
||||
const newBeat = heartbeatList.at(-1);
|
||||
if (newBeat && dayjs.utc(newBeat.time) > dayjs.utc(this.heartbeatList.at(-1)?.time)) {
|
||||
this.heartbeatList.push(heartbeatList.at(-1));
|
||||
this.pushDatapoint(datapoint, avgPingData, minPingData, maxPingData, downData, colorData);
|
||||
}
|
||||
|
||||
lastHeartbeatTime = beatTime;
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
// Clear the aggregate buffer if there are still datapoints
|
||||
if (aggregateBuffer.length > 0) {
|
||||
const average = this.getAverage(aggregateBuffer);
|
||||
this.pushDatapoint(average, avgPingData, minPingData, maxPingData, downData, colorData);
|
||||
aggregateBuffer = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Load chart period from storage if saved
|
||||
let period = this.$root.storage()[`chart-period-${this.monitorId}`];
|
||||
if (period != null) {
|
||||
this.chartPeriodHrs = Math.min(period, 6);
|
||||
}
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
// average ping chart
|
||||
data: avgPingData,
|
||||
fill: "origin",
|
||||
tension: 0.2,
|
||||
borderColor: "#5CDD8B",
|
||||
backgroundColor: "#5CDD8B06",
|
||||
yAxisID: "y",
|
||||
label: "avg-ping",
|
||||
},
|
||||
{
|
||||
// minimum ping chart
|
||||
data: minPingData,
|
||||
fill: "origin",
|
||||
tension: 0.2,
|
||||
borderColor: "#3CBD6B38",
|
||||
backgroundColor: "#5CDD8B06",
|
||||
yAxisID: "y",
|
||||
label: "min-ping",
|
||||
},
|
||||
{
|
||||
// maximum ping chart
|
||||
data: maxPingData,
|
||||
fill: "origin",
|
||||
tension: 0.2,
|
||||
borderColor: "#7CBD6B38",
|
||||
backgroundColor: "#5CDD8B06",
|
||||
yAxisID: "y",
|
||||
label: "max-ping",
|
||||
},
|
||||
{
|
||||
// Bar Chart
|
||||
type: "bar",
|
||||
data: downData,
|
||||
borderColor: "#00000000",
|
||||
backgroundColor: colorData,
|
||||
yAxisID: "y1",
|
||||
barThickness: "flex",
|
||||
barPercentage: 1,
|
||||
categoryPercentage: 1,
|
||||
inflateAmount: 0.05,
|
||||
label: "status",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -296,6 +567,7 @@ export default {
|
|||
|
||||
.dark & {
|
||||
background: $dark-bg;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.dark &:hover {
|
||||
|
|
24
src/components/notifications/Bitrix24.vue
Normal file
24
src/components/notifications/Bitrix24.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="bitrix24-webhook-url" class="form-label">{{ $t("Bitrix24 Webhook URL") }}</label>
|
||||
<HiddenInput id="bitrix24-webhook-url" v-model="$parent.notification.bitrix24WebhookURL" :required="true" autocomplete="new-password"></HiddenInput>
|
||||
<i18n-t tag="div" keypath="wayToGetBitrix24Webhook" class="form-text">
|
||||
<a href="https://helpdesk.bitrix24.com/open/12357038/" target="_blank">https://helpdesk.bitrix24.com/open/12357038/</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="bitrix24-user-id" class="form-label">{{ $t("User ID") }}</label>
|
||||
<input id="bitrix24-user-id" v-model="$parent.notification.bitrix24UserID" type="text" class="form-control" required>
|
||||
<div class="form-text">{{ $t("bitrix24SupportUserID") }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -16,4 +16,50 @@
|
|||
<label for="discord-prefix-message" class="form-label">{{ $t("Prefix Custom Message") }}</label>
|
||||
<input id="discord-prefix-message" v-model="$parent.notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" :placeholder="$t('Hello @everyone is...')">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="discord-message-type" class="form-label">{{ $t("Select message type") }}</label>
|
||||
<select id="discord-message-type" v-model="$parent.notification.discordChannelType" class="form-select">
|
||||
<option value="channel">{{ $t("Send to channel") }}</option>
|
||||
<option value="createNewForumPost">{{ $t("Create new forum post") }}</option>
|
||||
<option value="postToThread">{{ $t("postToExistingThread") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="$parent.notification.discordChannelType === 'createNewForumPost'">
|
||||
<div class="mb-3">
|
||||
<label for="discord-target" class="form-label">
|
||||
{{ $t("forumPostName") }}
|
||||
</label>
|
||||
<input id="discord-target" v-model="$parent.notification.postName" type="text" class="form-control" autocomplete="false">
|
||||
<div class="form-text">
|
||||
{{ $t("whatHappensAtForumPost", { option: $t("postToExistingThread") }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$parent.notification.discordChannelType === 'postToThread'">
|
||||
<div class="mb-3">
|
||||
<label for="discord-target" class="form-label">
|
||||
{{ $t("threadForumPostID") }}
|
||||
</label>
|
||||
<input id="discord-target" v-model="$parent.notification.threadId" type="text" class="form-control" autocomplete="false" :placeholder="$t('e.g. {discordThreadID}', { discordThreadID: 1177566663751782411 })">
|
||||
<div class="form-text">
|
||||
<i18n-t keypath="wayToGetDiscordThreadId">
|
||||
<a
|
||||
href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-"
|
||||
target="_blank"
|
||||
>{{ $t("here") }}</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
mounted() {
|
||||
if (!this.$parent.notification.discordChannelType) {
|
||||
this.$parent.notification.discordChannelType = "channel";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -3,6 +3,7 @@ import AlertNow from "./AlertNow.vue";
|
|||
import AliyunSMS from "./AliyunSms.vue";
|
||||
import Apprise from "./Apprise.vue";
|
||||
import Bark from "./Bark.vue";
|
||||
import Bitrix24 from "./Bitrix24.vue";
|
||||
import ClickSendSMS from "./ClickSendSMS.vue";
|
||||
import CallMeBot from "./CallMeBot.vue";
|
||||
import SMSC from "./SMSC.vue";
|
||||
|
@ -70,6 +71,7 @@ const NotificationFormList = {
|
|||
"AliyunSMS": AliyunSMS,
|
||||
"apprise": Apprise,
|
||||
"Bark": Bark,
|
||||
"Bitrix24": Bitrix24,
|
||||
"clicksendsms": ClickSendSMS,
|
||||
"CallMeBot": CallMeBot,
|
||||
"smsc": SMSC,
|
||||
|
|
|
@ -80,6 +80,7 @@
|
|||
"resendDisabled": "Resend disabled",
|
||||
"retriesDescription": "Maximum retries before the service is marked as down and a notification is sent",
|
||||
"ignoreTLSError": "Ignore TLS/SSL errors for HTTPS websites",
|
||||
"ignoreTLSErrorGeneral": "Ignore TLS/SSL error for connection",
|
||||
"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.",
|
||||
"Upside Down Mode": "Upside Down Mode",
|
||||
|
@ -361,6 +362,8 @@
|
|||
"Proxy": "Proxy",
|
||||
"Date Created": "Date Created",
|
||||
"Footer Text": "Footer Text",
|
||||
"Refresh Interval": "Refresh Interval",
|
||||
"Refresh Interval Description": "The status page will do a full site refresh every {0} seconds",
|
||||
"Show Powered By": "Show Powered By",
|
||||
"Domain Names": "Domain Names",
|
||||
"signedInDisp": "Signed in as {0}",
|
||||
|
@ -527,6 +530,15 @@
|
|||
"Bot Display Name": "Bot Display Name",
|
||||
"Prefix Custom Message": "Prefix Custom Message",
|
||||
"Hello @everyone is...": "Hello {'@'}everyone is…",
|
||||
"Select message type": "Select message type",
|
||||
"Send to channel": "Send to channel",
|
||||
"Create new forum post": "Create new forum post",
|
||||
"postToExistingThread": "Post to existing thread / forum post",
|
||||
"forumPostName": "Forum post name",
|
||||
"threadForumPostID": "Thread / Forum post ID",
|
||||
"e.g. {discordThreadID}": "e.g. {discordThreadID}",
|
||||
"whatHappensAtForumPost": "Create a new forum post. This does NOT post messages in existing post. To post in existing post use \"{option}\"",
|
||||
"wayToGetDiscordThreadId": "Getting a thread / forum post id is similar to getting a channel id. Read more about how to get ids {0}",
|
||||
"wayToGetTeamsURL": "You can learn how to create a webhook URL {0}.",
|
||||
"wayToGetZohoCliqURL": "You can learn how to create a webhook URL {0}.",
|
||||
"needSignalAPI": "You need to have a signal client with REST API.",
|
||||
|
@ -851,7 +863,7 @@
|
|||
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
|
||||
"Close": "Close",
|
||||
"Request Body": "Request Body",
|
||||
"wayToGetFlashDutyKey": "You can go to Channel -> (Select a Channel) -> Integrations -> Add a new integration' page, add a 'Custom Event' to get a push address, copy the Integration Key in the address. For more information, please visit",
|
||||
"wayToGetFlashDutyKey": "You can go to Channel -> (Select a Channel) -> Integrations -> Add a new integration' page, add a 'Uptime Kuma' to get a push address, copy the Integration Key in the address. For more information, please visit",
|
||||
"FlashDuty Severity": "Severity",
|
||||
"nostrRelays": "Nostr relays",
|
||||
"nostrRelaysHelp": "One relay URL per line",
|
||||
|
@ -862,6 +874,9 @@
|
|||
"noOrBadCertificate": "No/Bad Certificate",
|
||||
"gamedigGuessPort": "Gamedig: Guess Port",
|
||||
"gamedigGuessPortDescription": "The port used by Valve Server Query Protocol may be different from the client port. Try this if the monitor cannot connect to your server.",
|
||||
"Bitrix24 Webhook URL": "Bitrix24 Webhook URL",
|
||||
"wayToGetBitrix24Webhook": "You can create a webhook by following the steps at {0}",
|
||||
"bitrix24SupportUserID": "Enter your user ID in Bitrix24. You can find out the ID from the link by going to the user's profile.",
|
||||
"Saved.": "Saved.",
|
||||
"authUserInactiveOrDeleted": "The user is inactive or deleted.",
|
||||
"authInvalidToken": "Invalid Token.",
|
||||
|
@ -893,6 +908,8 @@
|
|||
"Browser Screenshot": "Browser Screenshot",
|
||||
"Zookeeper Host": "Zookeeper Host",
|
||||
"Zookeeper Timeout": "Zookeeper Connect Timeout (ms)",
|
||||
"Command": "Command",
|
||||
"mongodbCommandDescription": "Run a MongoDB command against the database. For information about the available commands check out the {documentation}",
|
||||
"wayToGetSevenIOApiKey": "Visit the dashboard under app.seven.io > developer > api key > the green add button",
|
||||
"senderSevenIO": "Sending number or name",
|
||||
"receiverSevenIO": "Receiving number",
|
||||
|
|
|
@ -41,6 +41,33 @@ export default {
|
|||
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts a Unix timestamp to a formatted date and time string.
|
||||
* @param {number} value - The Unix timestamp to convert.
|
||||
* @returns {string} The formatted date and time string.
|
||||
*/
|
||||
unixToDateTime(value) {
|
||||
return dayjs.unix(value).tz(this.timezone).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts a Unix timestamp to a dayjs object.
|
||||
* @param {number} value - The Unix timestamp to convert.
|
||||
* @returns {dayjs.Dayjs} The dayjs object representing the given timestamp.
|
||||
*/
|
||||
unixToDayjs(value) {
|
||||
return dayjs.unix(value).tz(this.timezone);
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts the given value to a dayjs object.
|
||||
* @param {string} value - the value to be converted
|
||||
* @returns {dayjs.Dayjs} a dayjs object in the timezone of this instance
|
||||
*/
|
||||
toDayjs(value) {
|
||||
return dayjs.utc(value).tz(this.timezone);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get time for maintenance
|
||||
* @param {string | number | Date | dayjs.Dayjs} value Time to
|
||||
|
|
|
@ -673,6 +673,17 @@ export default {
|
|||
getMonitorBeats(monitorID, period, callback) {
|
||||
socket.emit("getMonitorBeats", monitorID, period, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves monitor chart data.
|
||||
* @param {string} monitorID - The ID of the monitor.
|
||||
* @param {number} period - The time period for the chart data, in hours.
|
||||
* @param {socketCB} callback - The callback function to handle the chart data.
|
||||
* @returns {void}
|
||||
*/
|
||||
getMonitorChartData(monitorID, period, callback) {
|
||||
socket.emit("getMonitorChartData", monitorID, period, callback);
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
|
@ -9,15 +9,30 @@
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>{{ $t("Up") }}</h3>
|
||||
<span class="num">{{ $root.stats.up }}</span>
|
||||
<span
|
||||
class="num"
|
||||
:class="$root.stats.up === 0 && 'text-secondary'"
|
||||
>
|
||||
{{ $root.stats.up }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("Down") }}</h3>
|
||||
<span class="num text-danger">{{ $root.stats.down }}</span>
|
||||
<span
|
||||
class="num"
|
||||
:class="$root.stats.down > 0 ? 'text-danger' : 'text-secondary'"
|
||||
>
|
||||
{{ $root.stats.down }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("Maintenance") }}</h3>
|
||||
<span class="num text-maintenance">{{ $root.stats.maintenance }}</span>
|
||||
<span
|
||||
class="num"
|
||||
:class="$root.stats.maintenance > 0 ? 'text-maintenance' : 'text-secondary'"
|
||||
>
|
||||
{{ $root.stats.maintenance }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("Unknown") }}</h3>
|
||||
|
|
|
@ -450,6 +450,32 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- MongoDB -->
|
||||
<template v-if="monitor.type === 'mongodb'">
|
||||
<div class="my-3">
|
||||
<label for="mongodbCommand" class="form-label">{{ $t("Command") }}</label>
|
||||
<textarea id="mongodbCommand" v-model="monitor.databaseQuery" class="form-control" :placeholder="$t('Example:', [ '{ "ping": 1 }' ])"></textarea>
|
||||
<i18n-t tag="div" class="form-text" keypath="mongodbCommandDescription">
|
||||
<template #documentation>
|
||||
<a href="https://www.mongodb.com/docs/manual/reference/command/">{{ $t('documentationOf', ['MongoDB']) }}</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
|
||||
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control">
|
||||
|
||||
<i18n-t tag="div" class="form-text" keypath="jsonQueryDescription">
|
||||
<a href="https://jsonata.org/">jsonata.org</a>
|
||||
<a href="https://try.jsonata.org/">{{ $t('here') }}</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
|
||||
<input id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Interval -->
|
||||
<div class="my-3">
|
||||
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
|
||||
|
@ -498,10 +524,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check">
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'redis' " class="my-3 form-check">
|
||||
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="ignore-tls">
|
||||
{{ $t("ignoreTLSError") }}
|
||||
{{ monitor.type === "redis" ? $t("ignoreTLSErrorGeneral") : $t("ignoreTLSError") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -34,6 +34,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="auto-refresh-interval" class="form-label">{{ $t("Refresh Interval") }}</label>
|
||||
<input id="auto-refresh-interval" v-model="config.autoRefreshInterval" type="number" class="form-control" :min="5">
|
||||
<div class="form-text">
|
||||
{{ $t("Refresh Interval Description", [config.autoRefreshInterval]) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="switch-theme" class="form-label">{{ $t("Theme") }}</label>
|
||||
<select id="switch-theme" v-model="config.theme" class="form-select">
|
||||
|
@ -438,7 +446,6 @@ export default {
|
|||
baseURL: "",
|
||||
clickedEditButton: false,
|
||||
maintenanceList: [],
|
||||
autoRefreshInterval: 5,
|
||||
lastUpdateTime: dayjs(),
|
||||
updateCountdown: null,
|
||||
updateCountdownText: null,
|
||||
|
@ -708,6 +715,13 @@ export default {
|
|||
this.$root.publicGroupList = res.data.publicGroupList;
|
||||
|
||||
this.loading = false;
|
||||
|
||||
// Configure auto-refresh loop
|
||||
feedInterval = setInterval(() => {
|
||||
this.updateHeartbeatList();
|
||||
}, (this.config.autoRefreshInterval + 10) * 1000);
|
||||
|
||||
this.updateUpdateTimer();
|
||||
}).catch( function (error) {
|
||||
if (error.response.status === 404) {
|
||||
location.href = "/page-not-found";
|
||||
|
@ -715,13 +729,7 @@ export default {
|
|||
console.log(error);
|
||||
});
|
||||
|
||||
// Configure auto-refresh loop
|
||||
this.updateHeartbeatList();
|
||||
feedInterval = setInterval(() => {
|
||||
this.updateHeartbeatList();
|
||||
}, (this.autoRefreshInterval * 60 + 10) * 1000);
|
||||
|
||||
this.updateUpdateTimer();
|
||||
|
||||
// Go to edit page if ?edit present
|
||||
// null means ?edit present, but no value
|
||||
|
@ -797,7 +805,7 @@ export default {
|
|||
clearInterval(this.updateCountdown);
|
||||
|
||||
this.updateCountdown = setInterval(() => {
|
||||
const countdown = dayjs.duration(this.lastUpdateTime.add(this.autoRefreshInterval, "minutes").add(10, "seconds").diff(dayjs()));
|
||||
const countdown = dayjs.duration(this.lastUpdateTime.add(this.config.autoRefreshInterval, "seconds").add(10, "seconds").diff(dayjs()));
|
||||
if (countdown.as("seconds") < 0) {
|
||||
clearInterval(this.updateCountdown);
|
||||
} else {
|
||||
|
|
Loading…
Add table
Reference in a new issue