mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-06-09 06:12:33 +02:00
Merge branch 'master' into fix-weblate-conflict
# Conflicts: # src/lang/bg-BG.json # src/lang/de-CH.json # src/lang/de-DE.json # src/lang/fi.json # src/lang/fr-FR.json # src/lang/it-IT.json # src/lang/ja.json # src/lang/ko-KR.json # src/lang/nl-NL.json # src/lang/pl.json # src/lang/pt-BR.json # src/lang/ru-RU.json # src/lang/uk-UA.json # src/lang/zh-CN.json
This commit is contained in:
commit
87308a7778
44 changed files with 2227 additions and 1157 deletions
12
db/knex_migrations/2025-01-01-0000-add-smtp.js
Normal file
12
db/knex_migrations/2025-01-01-0000-add-smtp.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor", function (table) {
|
||||||
|
table.string("smtp_security").defaultTo(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.alterTable("monitor", function (table) {
|
||||||
|
table.dropColumn("smtp_security");
|
||||||
|
});
|
||||||
|
};
|
24
db/knex_migrations/2025-03-04-0000-ping-advanced-options.js
Normal file
24
db/knex_migrations/2025-03-04-0000-ping-advanced-options.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/* SQL:
|
||||||
|
ALTER TABLE monitor ADD ping_count INTEGER default 1 not null;
|
||||||
|
ALTER TABLE monitor ADD ping_numeric BOOLEAN default true not null;
|
||||||
|
ALTER TABLE monitor ADD ping_per_request_timeout INTEGER default 2 not null;
|
||||||
|
*/
|
||||||
|
exports.up = function (knex) {
|
||||||
|
// Add new columns to table monitor
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor", function (table) {
|
||||||
|
table.integer("ping_count").defaultTo(1).notNullable();
|
||||||
|
table.boolean("ping_numeric").defaultTo(true).notNullable();
|
||||||
|
table.integer("ping_per_request_timeout").defaultTo(2).notNullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor", function (table) {
|
||||||
|
table.dropColumn("ping_count");
|
||||||
|
table.dropColumn("ping_numeric");
|
||||||
|
table.dropColumn("ping_per_request_timeout");
|
||||||
|
});
|
||||||
|
};
|
13
db/knex_migrations/2025-05-09-0000-add-custom-url.js
Normal file
13
db/knex_migrations/2025-05-09-0000-add-custom-url.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// Add column custom_url to monitor_group table
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor_group", function (table) {
|
||||||
|
table.text("custom_url", "text");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.alterTable("monitor_group", function (table) {
|
||||||
|
table.dropColumn("custom_url");
|
||||||
|
});
|
||||||
|
};
|
2029
package-lock.json
generated
2029
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "2.0.0-beta.2",
|
"version": "2.0.0-beta.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
"test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063",
|
"test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063",
|
||||||
"playwright-codegen": "playwright codegen localhost:3000 --save-storage=./private/e2e-auth.json",
|
"playwright-codegen": "playwright codegen localhost:3000 --save-storage=./private/e2e-auth.json",
|
||||||
"playwright-show-report": "playwright show-report ./private/playwright-report",
|
"playwright-show-report": "playwright show-report ./private/playwright-report",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc --project ./tsconfig-backend.json",
|
||||||
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
||||||
"build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2 --target base2 . --push",
|
"build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2 --target base2 . --push",
|
||||||
"build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2-slim --target base2-slim . --push",
|
"build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2-slim --target base2-slim . --push",
|
||||||
|
|
|
@ -736,7 +736,7 @@ class Database {
|
||||||
if (Database.dbConfig.type === "sqlite") {
|
if (Database.dbConfig.type === "sqlite") {
|
||||||
return "DATETIME('now', ? || ' hours')";
|
return "DATETIME('now', ? || ' hours')";
|
||||||
} else {
|
} else {
|
||||||
return "DATE_ADD(NOW(), INTERVAL ? HOUR)";
|
return "DATE_ADD(UTC_TIMESTAMP(), INTERVAL ? HOUR)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ class Group extends BeanModel {
|
||||||
*/
|
*/
|
||||||
async getMonitorList() {
|
async getMonitorList() {
|
||||||
return R.convertToBeans("monitor", await R.getAll(`
|
return R.convertToBeans("monitor", await R.getAll(`
|
||||||
SELECT monitor.*, monitor_group.send_url FROM monitor, monitor_group
|
SELECT monitor.*, monitor_group.send_url, monitor_group.custom_url FROM monitor, monitor_group
|
||||||
WHERE monitor.id = monitor_group.monitor_id
|
WHERE monitor.id = monitor_group.monitor_id
|
||||||
AND group_id = ?
|
AND group_id = ?
|
||||||
ORDER BY monitor_group.weight
|
ORDER BY monitor_group.weight
|
||||||
|
|
|
@ -2,7 +2,11 @@ const dayjs = require("dayjs");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
||||||
SQL_DATETIME_FORMAT, evaluateJsonQuery
|
SQL_DATETIME_FORMAT, evaluateJsonQuery,
|
||||||
|
PING_PACKET_SIZE_MIN, PING_PACKET_SIZE_MAX, PING_PACKET_SIZE_DEFAULT,
|
||||||
|
PING_GLOBAL_TIMEOUT_MIN, PING_GLOBAL_TIMEOUT_MAX, PING_GLOBAL_TIMEOUT_DEFAULT,
|
||||||
|
PING_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT,
|
||||||
|
PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT
|
||||||
} = require("../../src/util");
|
} = require("../../src/util");
|
||||||
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
|
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
|
||||||
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
||||||
|
@ -53,7 +57,7 @@ class Monitor extends BeanModel {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.sendUrl) {
|
if (this.sendUrl) {
|
||||||
obj.url = this.url;
|
obj.url = this.customUrl ?? this.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showTags) {
|
if (showTags) {
|
||||||
|
@ -153,8 +157,14 @@ class Monitor extends BeanModel {
|
||||||
snmpOid: this.snmpOid,
|
snmpOid: this.snmpOid,
|
||||||
jsonPathOperator: this.jsonPathOperator,
|
jsonPathOperator: this.jsonPathOperator,
|
||||||
snmpVersion: this.snmpVersion,
|
snmpVersion: this.snmpVersion,
|
||||||
|
smtpSecurity: this.smtpSecurity,
|
||||||
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
|
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
|
||||||
conditions: JSON.parse(this.conditions),
|
conditions: JSON.parse(this.conditions),
|
||||||
|
|
||||||
|
// ping advanced options
|
||||||
|
ping_numeric: this.isPingNumeric(),
|
||||||
|
ping_count: this.ping_count,
|
||||||
|
ping_per_request_timeout: this.ping_per_request_timeout,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeSensitiveData) {
|
if (includeSensitiveData) {
|
||||||
|
@ -247,6 +257,14 @@ class Monitor extends BeanModel {
|
||||||
return Boolean(this.expiryNotification);
|
return Boolean(this.expiryNotification);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if ping should use numeric output only
|
||||||
|
* @returns {boolean} True if IP addresses will be output instead of symbolic hostnames
|
||||||
|
*/
|
||||||
|
isPingNumeric() {
|
||||||
|
return Boolean(this.ping_numeric);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse to boolean
|
* Parse to boolean
|
||||||
* @returns {boolean} Should TLS errors be ignored?
|
* @returns {boolean} Should TLS errors be ignored?
|
||||||
|
@ -584,7 +602,7 @@ class Monitor extends BeanModel {
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
|
|
||||||
} else if (this.type === "ping") {
|
} else if (this.type === "ping") {
|
||||||
bean.ping = await ping(this.hostname, this.packetSize);
|
bean.ping = await ping(this.hostname, this.ping_count, "", this.ping_numeric, this.packetSize, this.timeout, this.ping_per_request_timeout);
|
||||||
bean.msg = "";
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else if (this.type === "push") { // Type: Push
|
} else if (this.type === "push") { // Type: Push
|
||||||
|
@ -656,7 +674,7 @@ class Monitor extends BeanModel {
|
||||||
bean.msg = res.data.response.servers[0].name;
|
bean.msg = res.data.response.servers[0].name;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
bean.ping = await ping(this.hostname, this.packetSize);
|
bean.ping = await ping(this.hostname, PING_COUNT_DEFAULT, "", true, this.packetSize, PING_GLOBAL_TIMEOUT_DEFAULT, PING_PER_REQUEST_TIMEOUT_DEFAULT);
|
||||||
} catch (_) { }
|
} catch (_) { }
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Server not found on Steam");
|
throw new Error("Server not found on Steam");
|
||||||
|
@ -1294,7 +1312,8 @@ class Monitor extends BeanModel {
|
||||||
try {
|
try {
|
||||||
const heartbeatJSON = bean.toJSON();
|
const heartbeatJSON = bean.toJSON();
|
||||||
const monitorData = [{ id: monitor.id,
|
const monitorData = [{ id: monitor.id,
|
||||||
active: monitor.active
|
active: monitor.active,
|
||||||
|
name: monitor.name
|
||||||
}];
|
}];
|
||||||
const preloadData = await Monitor.preparePreloadData(monitorData);
|
const preloadData = await Monitor.preparePreloadData(monitorData);
|
||||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||||
|
@ -1467,6 +1486,31 @@ class Monitor extends BeanModel {
|
||||||
if (this.interval < MIN_INTERVAL_SECOND) {
|
if (this.interval < MIN_INTERVAL_SECOND) {
|
||||||
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
|
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.type === "ping") {
|
||||||
|
// ping parameters validation
|
||||||
|
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
|
||||||
|
throw new Error(`Packet size must be between ${PING_PACKET_SIZE_MIN} and ${PING_PACKET_SIZE_MAX} (default: ${PING_PACKET_SIZE_DEFAULT})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.ping_per_request_timeout && (this.ping_per_request_timeout < PING_PER_REQUEST_TIMEOUT_MIN || this.ping_per_request_timeout > PING_PER_REQUEST_TIMEOUT_MAX)) {
|
||||||
|
throw new Error(`Per-ping timeout must be between ${PING_PER_REQUEST_TIMEOUT_MIN} and ${PING_PER_REQUEST_TIMEOUT_MAX} seconds (default: ${PING_PER_REQUEST_TIMEOUT_DEFAULT})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.ping_count && (this.ping_count < PING_COUNT_MIN || this.ping_count > PING_COUNT_MAX)) {
|
||||||
|
throw new Error(`Echo requests count must be between ${PING_COUNT_MIN} and ${PING_COUNT_MAX} (default: ${PING_COUNT_DEFAULT})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.timeout) {
|
||||||
|
const pingGlobalTimeout = Math.round(Number(this.timeout));
|
||||||
|
|
||||||
|
if (pingGlobalTimeout < this.ping_per_request_timeout || pingGlobalTimeout < PING_GLOBAL_TIMEOUT_MIN || pingGlobalTimeout > PING_GLOBAL_TIMEOUT_MAX) {
|
||||||
|
throw new Error(`Timeout must be between ${PING_GLOBAL_TIMEOUT_MIN} and ${PING_GLOBAL_TIMEOUT_MAX} seconds (default: ${PING_GLOBAL_TIMEOUT_DEFAULT})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeout = pingGlobalTimeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
35
server/monitor-types/smtp.js
Normal file
35
server/monitor-types/smtp.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
const { MonitorType } = require("./monitor-type");
|
||||||
|
const { UP } = require("../../src/util");
|
||||||
|
const nodemailer = require("nodemailer");
|
||||||
|
|
||||||
|
class SMTPMonitorType extends MonitorType {
|
||||||
|
name = "smtp";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async check(monitor, heartbeat, _server) {
|
||||||
|
let options = {
|
||||||
|
port: monitor.port || 25,
|
||||||
|
host: monitor.hostname,
|
||||||
|
secure: monitor.smtpSecurity === "secure", // use SMTPS (not STARTTLS)
|
||||||
|
ignoreTLS: monitor.smtpSecurity === "nostarttls", // don't use STARTTLS even if it's available
|
||||||
|
requireTLS: monitor.smtpSecurity === "starttls", // use STARTTLS or fail
|
||||||
|
};
|
||||||
|
let transporter = nodemailer.createTransport(options);
|
||||||
|
try {
|
||||||
|
await transporter.verify();
|
||||||
|
|
||||||
|
heartbeat.status = UP;
|
||||||
|
heartbeat.msg = "SMTP connection verifies successfully";
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`SMTP connection doesn't verify: ${e}`);
|
||||||
|
} finally {
|
||||||
|
transporter.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
SMTPMonitorType,
|
||||||
|
};
|
|
@ -46,10 +46,10 @@ class Discord extends NotificationProvider {
|
||||||
name: "Service Name",
|
name: "Service Name",
|
||||||
value: monitorJSON["name"],
|
value: monitorJSON["name"],
|
||||||
},
|
},
|
||||||
{
|
...(!notification.disableUrl ? [{
|
||||||
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||||
value: this.extractAddress(monitorJSON),
|
value: this.extractAddress(monitorJSON),
|
||||||
},
|
}] : []),
|
||||||
{
|
{
|
||||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||||
value: heartbeatJSON["localDateTime"],
|
value: heartbeatJSON["localDateTime"],
|
||||||
|
@ -83,10 +83,10 @@ class Discord extends NotificationProvider {
|
||||||
name: "Service Name",
|
name: "Service Name",
|
||||||
value: monitorJSON["name"],
|
value: monitorJSON["name"],
|
||||||
},
|
},
|
||||||
{
|
...(!notification.disableUrl ? [{
|
||||||
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||||
value: this.extractAddress(monitorJSON),
|
value: this.extractAddress(monitorJSON),
|
||||||
},
|
}] : []),
|
||||||
{
|
{
|
||||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||||
value: heartbeatJSON["localDateTime"],
|
value: heartbeatJSON["localDateTime"],
|
||||||
|
|
|
@ -73,13 +73,13 @@ class FlashDuty extends NotificationProvider {
|
||||||
}
|
}
|
||||||
const options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
|
url: notification.flashdutyIntegrationKey.startsWith("http") ? notification.flashdutyIntegrationKey : "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
data: {
|
data: {
|
||||||
description: `[${title}] [${monitorInfo.name}] ${body}`,
|
description: `[${title}] [${monitorInfo.name}] ${body}`,
|
||||||
title,
|
title,
|
||||||
event_status: eventStatus || "Info",
|
event_status: eventStatus || "Info",
|
||||||
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7),
|
alert_key: monitorInfo.id ? String(monitorInfo.id) : Math.random().toString(36).substring(7),
|
||||||
labels,
|
labels,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -79,13 +79,11 @@ class Mattermost extends NotificationProvider {
|
||||||
fallback:
|
fallback:
|
||||||
"Your " +
|
"Your " +
|
||||||
monitorJSON.pathName +
|
monitorJSON.pathName +
|
||||||
monitorJSON.name +
|
|
||||||
" service went " +
|
" service went " +
|
||||||
statusText,
|
statusText,
|
||||||
color: color,
|
color: color,
|
||||||
title:
|
title:
|
||||||
monitorJSON.pathName +
|
monitorJSON.pathName +
|
||||||
monitorJSON.name +
|
|
||||||
" service went " +
|
" service went " +
|
||||||
statusText,
|
statusText,
|
||||||
title_link: monitorJSON.url,
|
title_link: monitorJSON.url,
|
||||||
|
|
53
server/notification-providers/notifery.js
Normal file
53
server/notification-providers/notifery.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
const { getMonitorRelativeURL, UP } = require("../../src/util");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class Notifery extends NotificationProvider {
|
||||||
|
name = "notifery";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
const okMsg = "Sent Successfully.";
|
||||||
|
const url = "https://api.notifery.com/event";
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
title: notification.notiferyTitle || "Uptime Kuma Alert",
|
||||||
|
message: msg,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notification.notiferyGroup) {
|
||||||
|
data.group = notification.notiferyGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link to the monitor
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL && monitorJSON) {
|
||||||
|
data.message += `\n\nMonitor: ${baseURL}${getMonitorRelativeURL(monitorJSON.id)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON) {
|
||||||
|
data.code = heartbeatJSON.status === UP ? 0 : 1;
|
||||||
|
|
||||||
|
if (heartbeatJSON.ping) {
|
||||||
|
data.duration = heartbeatJSON.ping;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": notification.notiferyApiKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(url, data, { headers });
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Notifery;
|
73
server/notification-providers/onechat.js
Normal file
73
server/notification-providers/onechat.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class OneChat extends NotificationProvider {
|
||||||
|
name = "OneChat";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
const okMsg = "Sent Successfully.";
|
||||||
|
const url = "https://chat-api.one.th/message/api/v1/push_message";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + notification.accessToken,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
const testMessage = {
|
||||||
|
to: notification.recieverId,
|
||||||
|
bot_id: notification.botId,
|
||||||
|
type: "text",
|
||||||
|
message: "Test Successful!",
|
||||||
|
};
|
||||||
|
await axios.post(url, testMessage, config);
|
||||||
|
} else if (heartbeatJSON["status"] === DOWN) {
|
||||||
|
const downMessage = {
|
||||||
|
to: notification.recieverId,
|
||||||
|
bot_id: notification.botId,
|
||||||
|
type: "text",
|
||||||
|
message:
|
||||||
|
`UptimeKuma Alert:
|
||||||
|
[🔴 Down]
|
||||||
|
Name: ${monitorJSON["name"]}
|
||||||
|
${heartbeatJSON["msg"]}
|
||||||
|
Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||||
|
};
|
||||||
|
await axios.post(url, downMessage, config);
|
||||||
|
} else if (heartbeatJSON["status"] === UP) {
|
||||||
|
const upMessage = {
|
||||||
|
to: notification.recieverId,
|
||||||
|
bot_id: notification.botId,
|
||||||
|
type: "text",
|
||||||
|
message:
|
||||||
|
`UptimeKuma Alert:
|
||||||
|
[🟢 Up]
|
||||||
|
Name: ${monitorJSON["name"]}
|
||||||
|
${heartbeatJSON["msg"]}
|
||||||
|
Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||||
|
};
|
||||||
|
await axios.post(url, upMessage, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
// Handle errors and throw a descriptive message
|
||||||
|
if (error.response) {
|
||||||
|
const errorMessage =
|
||||||
|
error.response.data?.message ||
|
||||||
|
"Unknown API error occurred.";
|
||||||
|
throw new Error(`OneChat API Error: ${errorMessage}`);
|
||||||
|
} else {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = OneChat;
|
|
@ -145,6 +145,7 @@ class Slack extends NotificationProvider {
|
||||||
|
|
||||||
const title = "Uptime Kuma Alert";
|
const title = "Uptime Kuma Alert";
|
||||||
let data = {
|
let data = {
|
||||||
|
"text": msg,
|
||||||
"channel": notification.slackchannel,
|
"channel": notification.slackchannel,
|
||||||
"username": notification.slackusername,
|
"username": notification.slackusername,
|
||||||
"icon_emoji": notification.slackiconemo,
|
"icon_emoji": notification.slackiconemo,
|
||||||
|
|
|
@ -11,59 +11,127 @@ class SMSEagle extends NotificationProvider {
|
||||||
const okMsg = "Sent Successfully.";
|
const okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let config = {
|
if (notification.smseagleApiType === "smseagle-apiv1") { // according to https://www.smseagle.eu/apiv1/
|
||||||
headers: {
|
let config = {
|
||||||
"Content-Type": "application/json",
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let sendMethod;
|
||||||
|
let recipientType;
|
||||||
|
let duration;
|
||||||
|
let voiceId;
|
||||||
|
|
||||||
|
if (notification.smseagleRecipientType === "smseagle-contact") {
|
||||||
|
recipientType = "contactname";
|
||||||
|
sendMethod = "/send_tocontact";
|
||||||
|
} else if (notification.smseagleRecipientType === "smseagle-group") {
|
||||||
|
recipientType = "groupname";
|
||||||
|
sendMethod = "/send_togroup";
|
||||||
|
} else if (notification.smseagleRecipientType === "smseagle-to") {
|
||||||
|
recipientType = "to";
|
||||||
|
sendMethod = "/send_sms";
|
||||||
|
if (notification.smseagleMsgType !== "smseagle-sms") {
|
||||||
|
duration = notification.smseagleDuration ?? 10;
|
||||||
|
|
||||||
|
if (notification.smseagleMsgType === "smseagle-ring") {
|
||||||
|
sendMethod = "/ring_call";
|
||||||
|
} else if (notification.smseagleMsgType === "smseagle-tts") {
|
||||||
|
sendMethod = "/tts_call";
|
||||||
|
} else if (notification.smseagleMsgType === "smseagle-tts-advanced") {
|
||||||
|
sendMethod = "/tts_adv_call";
|
||||||
|
voiceId = notification.smseagleTtsModel ? notification.smseagleTtsModel : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let postData;
|
const url = new URL(notification.smseagleUrl + "/http_api" + sendMethod);
|
||||||
let sendMethod;
|
|
||||||
let recipientType;
|
|
||||||
|
|
||||||
let encoding = (notification.smseagleEncoding) ? "1" : "0";
|
url.searchParams.append("access_token", notification.smseagleToken);
|
||||||
let priority = (notification.smseaglePriority) ? notification.smseaglePriority : "0";
|
url.searchParams.append(recipientType, notification.smseagleRecipient);
|
||||||
|
if (!notification.smseagleRecipientType || notification.smseagleRecipientType === "smseagle-sms") {
|
||||||
if (notification.smseagleRecipientType === "smseagle-contact") {
|
url.searchParams.append("unicode", (notification.smseagleEncoding) ? "1" : "0");
|
||||||
recipientType = "contactname";
|
url.searchParams.append("highpriority", notification.smseaglePriority ?? "0");
|
||||||
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 {
|
} else {
|
||||||
error = "SMSEagle API returned an unexpected response";
|
url.searchParams.append("duration", duration);
|
||||||
|
}
|
||||||
|
if (notification.smseagleRecipientType !== "smseagle-ring") {
|
||||||
|
url.searchParams.append("message", msg);
|
||||||
|
}
|
||||||
|
if (voiceId) {
|
||||||
|
url.searchParams.append("voice_id", voiceId);
|
||||||
}
|
}
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return okMsg;
|
let resp = await axios.get(url.toString(), config);
|
||||||
|
|
||||||
|
if (resp.data.indexOf("OK") === -1) {
|
||||||
|
let error = `SMSEagle API returned error: ${resp.data}`;
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
} else if (notification.smseagleApiType === "smseagle-apiv2") { // according to https://www.smseagle.eu/docs/apiv2/
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"access-token": notification.smseagleToken,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoding = (notification.smseagleEncoding) ? "unicode" : "standard";
|
||||||
|
let priority = (notification.smseaglePriority) ?? 0;
|
||||||
|
|
||||||
|
let postData = {
|
||||||
|
text: msg,
|
||||||
|
encoding: encoding,
|
||||||
|
priority: priority
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notification.smseagleRecipientContact) {
|
||||||
|
postData["contacts"] = notification.smseagleRecipientContact.split(",").map(Number);
|
||||||
|
}
|
||||||
|
if (notification.smseagleRecipientGroup) {
|
||||||
|
postData["groups"] = notification.smseagleRecipientGroup.split(",").map(Number);
|
||||||
|
}
|
||||||
|
if (notification.smseagleRecipientTo) {
|
||||||
|
postData["to"] = notification.smseagleRecipientTo.split(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
let endpoint = "/messages/sms";
|
||||||
|
|
||||||
|
if (notification.smseagleMsgType !== "smseagle-sms") {
|
||||||
|
|
||||||
|
postData["duration"] = notification.smseagleDuration ?? 10;
|
||||||
|
|
||||||
|
if (notification.smseagleMsgType === "smseagle-ring") {
|
||||||
|
endpoint = "/calls/ring";
|
||||||
|
} else if (notification.smseagleMsgType === "smseagle-tts") {
|
||||||
|
endpoint = "/calls/tts";
|
||||||
|
} else if (notification.smseagleMsgType === "smseagle-tts-advanced") {
|
||||||
|
endpoint = "/calls/tts_advanced";
|
||||||
|
postData["voice_id"] = notification.smseagleTtsModel ?? 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = await axios.post(notification.smseagleUrl + "/api/v2" + endpoint, postData, config);
|
||||||
|
|
||||||
|
const queuedCount = resp.data.filter(x => x.status === "queued").length;
|
||||||
|
const unqueuedCount = resp.data.length - queuedCount;
|
||||||
|
|
||||||
|
if (resp.status !== 200 || queuedCount === 0) {
|
||||||
|
if (!resp.data.length) {
|
||||||
|
throw new Error("SMSEagle API returned an empty response");
|
||||||
|
}
|
||||||
|
throw new Error(`SMSEagle API returned error: ${JSON.stringify(resp.data)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unqueuedCount) {
|
||||||
|
return `Sent ${queuedCount}/${resp.data.length} Messages Successfully.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.throwGeneralAxiosError(error);
|
this.throwGeneralAxiosError(error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ class SMTP extends NotificationProvider {
|
||||||
// default values in case the user does not want to template
|
// default values in case the user does not want to template
|
||||||
let subject = msg;
|
let subject = msg;
|
||||||
let body = msg;
|
let body = msg;
|
||||||
|
let useHTMLBody = false;
|
||||||
if (heartbeatJSON) {
|
if (heartbeatJSON) {
|
||||||
body = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
|
body = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
|
||||||
}
|
}
|
||||||
|
@ -50,11 +51,11 @@ class SMTP extends NotificationProvider {
|
||||||
// cannot end with whitespace as this often raises spam scores
|
// cannot end with whitespace as this often raises spam scores
|
||||||
const customSubject = notification.customSubject?.trim() || "";
|
const customSubject = notification.customSubject?.trim() || "";
|
||||||
const customBody = notification.customBody?.trim() || "";
|
const customBody = notification.customBody?.trim() || "";
|
||||||
|
|
||||||
if (customSubject !== "") {
|
if (customSubject !== "") {
|
||||||
subject = await this.renderTemplate(customSubject, msg, monitorJSON, heartbeatJSON);
|
subject = await this.renderTemplate(customSubject, msg, monitorJSON, heartbeatJSON);
|
||||||
}
|
}
|
||||||
if (customBody !== "") {
|
if (customBody !== "") {
|
||||||
|
useHTMLBody = notification.htmlBody || false;
|
||||||
body = await this.renderTemplate(customBody, msg, monitorJSON, heartbeatJSON);
|
body = await this.renderTemplate(customBody, msg, monitorJSON, heartbeatJSON);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,7 +68,8 @@ class SMTP extends NotificationProvider {
|
||||||
bcc: notification.smtpBCC,
|
bcc: notification.smtpBCC,
|
||||||
to: notification.smtpTo,
|
to: notification.smtpTo,
|
||||||
subject: subject,
|
subject: subject,
|
||||||
text: body,
|
// If the email body is custom, and the user wants it, set the email body as HTML
|
||||||
|
[useHTMLBody ? "html" : "text"]: body
|
||||||
});
|
});
|
||||||
|
|
||||||
return okMsg;
|
return okMsg;
|
||||||
|
|
37
server/notification-providers/spugpush.js
Normal file
37
server/notification-providers/spugpush.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class SpugPush extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "SpugPush";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
let formData = {
|
||||||
|
title: "Uptime Kuma Message",
|
||||||
|
content: msg
|
||||||
|
};
|
||||||
|
if (heartbeatJSON) {
|
||||||
|
if (heartbeatJSON["status"] === UP) {
|
||||||
|
formData.title = `UptimeKuma 「${monitorJSON["name"]}」 is Up`;
|
||||||
|
formData.content = `[✅ Up] ${heartbeatJSON["msg"]}`;
|
||||||
|
} else if (heartbeatJSON["status"] === DOWN) {
|
||||||
|
formData.title = `UptimeKuma 「${monitorJSON["name"]}」 is Down`;
|
||||||
|
formData.content = `[🔴 Down] ${heartbeatJSON["msg"]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const apiUrl = `https://push.spug.cc/send/${notification.templateKey}`;
|
||||||
|
await axios.post(apiUrl, formData);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SpugPush;
|
|
@ -13,6 +13,7 @@ const DingDing = require("./notification-providers/dingding");
|
||||||
const Discord = require("./notification-providers/discord");
|
const Discord = require("./notification-providers/discord");
|
||||||
const Elks = require("./notification-providers/46elks");
|
const Elks = require("./notification-providers/46elks");
|
||||||
const Feishu = require("./notification-providers/feishu");
|
const Feishu = require("./notification-providers/feishu");
|
||||||
|
const Notifery = require("./notification-providers/notifery");
|
||||||
const FreeMobile = require("./notification-providers/freemobile");
|
const FreeMobile = require("./notification-providers/freemobile");
|
||||||
const GoogleChat = require("./notification-providers/google-chat");
|
const GoogleChat = require("./notification-providers/google-chat");
|
||||||
const Gorush = require("./notification-providers/gorush");
|
const Gorush = require("./notification-providers/gorush");
|
||||||
|
@ -30,6 +31,7 @@ const Mattermost = require("./notification-providers/mattermost");
|
||||||
const Nostr = require("./notification-providers/nostr");
|
const Nostr = require("./notification-providers/nostr");
|
||||||
const Ntfy = require("./notification-providers/ntfy");
|
const Ntfy = require("./notification-providers/ntfy");
|
||||||
const Octopush = require("./notification-providers/octopush");
|
const Octopush = require("./notification-providers/octopush");
|
||||||
|
const OneChat = require("./notification-providers/onechat");
|
||||||
const OneBot = require("./notification-providers/onebot");
|
const OneBot = require("./notification-providers/onebot");
|
||||||
const Opsgenie = require("./notification-providers/opsgenie");
|
const Opsgenie = require("./notification-providers/opsgenie");
|
||||||
const PagerDuty = require("./notification-providers/pagerduty");
|
const PagerDuty = require("./notification-providers/pagerduty");
|
||||||
|
@ -74,6 +76,7 @@ const Wpush = require("./notification-providers/wpush");
|
||||||
const SendGrid = require("./notification-providers/send-grid");
|
const SendGrid = require("./notification-providers/send-grid");
|
||||||
const YZJ = require("./notification-providers/yzj");
|
const YZJ = require("./notification-providers/yzj");
|
||||||
const SMSPlanet = require("./notification-providers/sms-planet");
|
const SMSPlanet = require("./notification-providers/sms-planet");
|
||||||
|
const SpugPush = require("./notification-providers/spugpush");
|
||||||
|
|
||||||
class Notification {
|
class Notification {
|
||||||
|
|
||||||
|
@ -121,6 +124,7 @@ class Notification {
|
||||||
new Nostr(),
|
new Nostr(),
|
||||||
new Ntfy(),
|
new Ntfy(),
|
||||||
new Octopush(),
|
new Octopush(),
|
||||||
|
new OneChat(),
|
||||||
new OneBot(),
|
new OneBot(),
|
||||||
new Onesender(),
|
new Onesender(),
|
||||||
new Opsgenie(),
|
new Opsgenie(),
|
||||||
|
@ -165,6 +169,8 @@ class Notification {
|
||||||
new SendGrid(),
|
new SendGrid(),
|
||||||
new YZJ(),
|
new YZJ(),
|
||||||
new SMSPlanet(),
|
new SMSPlanet(),
|
||||||
|
new SpugPush(),
|
||||||
|
new Notifery(),
|
||||||
];
|
];
|
||||||
for (let item of list) {
|
for (let item of list) {
|
||||||
if (! item.name) {
|
if (! item.name) {
|
||||||
|
|
|
@ -50,7 +50,7 @@ router.all("/api/push/:pushToken", async (request, response) => {
|
||||||
let msg = request.query.msg || "OK";
|
let msg = request.query.msg || "OK";
|
||||||
let ping = parseFloat(request.query.ping) || null;
|
let ping = parseFloat(request.query.ping) || null;
|
||||||
let statusString = request.query.status || "up";
|
let statusString = request.query.status || "up";
|
||||||
let status = (statusString === "up") ? UP : DOWN;
|
const statusFromParam = (statusString === "up") ? UP : DOWN;
|
||||||
|
|
||||||
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
||||||
pushToken
|
pushToken
|
||||||
|
@ -80,7 +80,7 @@ router.all("/api/push/:pushToken", async (request, response) => {
|
||||||
msg = "Monitor under maintenance";
|
msg = "Monitor under maintenance";
|
||||||
bean.status = MAINTENANCE;
|
bean.status = MAINTENANCE;
|
||||||
} else {
|
} else {
|
||||||
determineStatus(status, previousHeartbeat, monitor.maxretries, monitor.isUpsideDown(), bean);
|
determineStatus(statusFromParam, previousHeartbeat, monitor.maxretries, monitor.isUpsideDown(), bean);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate uptime
|
// Calculate uptime
|
||||||
|
@ -92,21 +92,21 @@ router.all("/api/push/:pushToken", async (request, response) => {
|
||||||
log.debug("router", "PreviousStatus: " + previousHeartbeat?.status);
|
log.debug("router", "PreviousStatus: " + previousHeartbeat?.status);
|
||||||
log.debug("router", "Current Status: " + bean.status);
|
log.debug("router", "Current Status: " + bean.status);
|
||||||
|
|
||||||
bean.important = Monitor.isImportantBeat(isFirstBeat, previousHeartbeat?.status, status);
|
bean.important = Monitor.isImportantBeat(isFirstBeat, previousHeartbeat?.status, bean.status);
|
||||||
|
|
||||||
if (Monitor.isImportantForNotification(isFirstBeat, previousHeartbeat?.status, status)) {
|
if (Monitor.isImportantForNotification(isFirstBeat, previousHeartbeat?.status, bean.status)) {
|
||||||
// Reset down count
|
// Reset down count
|
||||||
bean.downCount = 0;
|
bean.downCount = 0;
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] sendNotification`);
|
log.debug("monitor", `[${monitor.name}] sendNotification`);
|
||||||
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
||||||
} else {
|
} else {
|
||||||
if (bean.status === DOWN && this.resendInterval > 0) {
|
if (bean.status === DOWN && monitor.resendInterval > 0) {
|
||||||
++bean.downCount;
|
++bean.downCount;
|
||||||
if (bean.downCount >= this.resendInterval) {
|
if (bean.downCount >= monitor.resendInterval) {
|
||||||
// Send notification again, because we are still DOWN
|
// Send notification again, because we are still DOWN
|
||||||
log.debug("monitor", `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
log.debug("monitor", `[${monitor.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${monitor.resendInterval}`);
|
||||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
||||||
|
|
||||||
// Reset down count
|
// Reset down count
|
||||||
bean.downCount = 0;
|
bean.downCount = 0;
|
||||||
|
|
|
@ -866,6 +866,7 @@ let needSetup = false;
|
||||||
monitor.kafkaProducerAllowAutoTopicCreation;
|
monitor.kafkaProducerAllowAutoTopicCreation;
|
||||||
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
|
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
|
||||||
bean.remote_browser = monitor.remote_browser;
|
bean.remote_browser = monitor.remote_browser;
|
||||||
|
bean.smtpSecurity = monitor.smtpSecurity;
|
||||||
bean.snmpVersion = monitor.snmpVersion;
|
bean.snmpVersion = monitor.snmpVersion;
|
||||||
bean.snmpOid = monitor.snmpOid;
|
bean.snmpOid = monitor.snmpOid;
|
||||||
bean.jsonPathOperator = monitor.jsonPathOperator;
|
bean.jsonPathOperator = monitor.jsonPathOperator;
|
||||||
|
@ -875,6 +876,11 @@ let needSetup = false;
|
||||||
bean.rabbitmqPassword = monitor.rabbitmqPassword;
|
bean.rabbitmqPassword = monitor.rabbitmqPassword;
|
||||||
bean.conditions = JSON.stringify(monitor.conditions);
|
bean.conditions = JSON.stringify(monitor.conditions);
|
||||||
|
|
||||||
|
// ping advanced options
|
||||||
|
bean.ping_numeric = monitor.ping_numeric;
|
||||||
|
bean.ping_count = monitor.ping_count;
|
||||||
|
bean.ping_per_request_timeout = monitor.ping_per_request_timeout;
|
||||||
|
|
||||||
bean.validate();
|
bean.validate();
|
||||||
|
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
|
@ -91,6 +91,18 @@ module.exports.generalSocketHandler = (socket, server) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("getPushExample", (language, callback) => {
|
socket.on("getPushExample", (language, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
if (!/^[a-z-]+$/.test(language)) {
|
||||||
|
throw new Error("Invalid language");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let dir = path.join("./extra/push-examples", language);
|
let dir = path.join("./extra/push-examples", language);
|
||||||
|
|
|
@ -211,6 +211,10 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
relationBean.send_url = monitor.sendUrl;
|
relationBean.send_url = monitor.sendUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (monitor.url !== undefined) {
|
||||||
|
relationBean.custom_url = monitor.url;
|
||||||
|
}
|
||||||
|
|
||||||
await R.store(relationBean);
|
await R.store(relationBean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,7 @@ class UptimeKumaServer {
|
||||||
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
||||||
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
|
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
|
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
|
||||||
|
UptimeKumaServer.monitorTypeList["smtp"] = new SMTPMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["group"] = new GroupMonitorType();
|
UptimeKumaServer.monitorTypeList["group"] = new GroupMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
|
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
|
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
|
||||||
|
@ -552,6 +553,7 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor
|
||||||
const { TailscalePing } = require("./monitor-types/tailscale-ping");
|
const { TailscalePing } = require("./monitor-types/tailscale-ping");
|
||||||
const { DnsMonitorType } = require("./monitor-types/dns");
|
const { DnsMonitorType } = require("./monitor-types/dns");
|
||||||
const { MqttMonitorType } = require("./monitor-types/mqtt");
|
const { MqttMonitorType } = require("./monitor-types/mqtt");
|
||||||
|
const { SMTPMonitorType } = require("./monitor-types/smtp");
|
||||||
const { GroupMonitorType } = require("./monitor-types/group");
|
const { GroupMonitorType } = require("./monitor-types/group");
|
||||||
const { SNMPMonitorType } = require("./monitor-types/snmp");
|
const { SNMPMonitorType } = require("./monitor-types/snmp");
|
||||||
const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
const tcpp = require("tcp-ping");
|
const tcpp = require("tcp-ping");
|
||||||
const ping = require("@louislam/ping");
|
const ping = require("@louislam/ping");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { log, genSecret, badgeConstants } = require("../src/util");
|
const {
|
||||||
|
log, genSecret, badgeConstants,
|
||||||
|
PING_PACKET_SIZE_DEFAULT, PING_GLOBAL_TIMEOUT_DEFAULT,
|
||||||
|
PING_COUNT_DEFAULT, PING_PER_REQUEST_TIMEOUT_DEFAULT
|
||||||
|
} = require("../src/util");
|
||||||
const passwordHash = require("./password-hash");
|
const passwordHash = require("./password-hash");
|
||||||
const { Resolver } = require("dns");
|
const { Resolver } = require("dns");
|
||||||
const iconv = require("iconv-lite");
|
const iconv = require("iconv-lite");
|
||||||
|
@ -118,20 +122,33 @@ exports.tcping = function (hostname, port) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ping the specified machine
|
* Ping the specified machine
|
||||||
* @param {string} hostname Hostname / address of machine
|
* @param {string} destAddr Hostname / IP address of machine to ping
|
||||||
* @param {number} size Size of packet to send
|
* @param {number} count Number of packets to send before stopping
|
||||||
|
* @param {string} sourceAddr Source address for sending/receiving echo requests
|
||||||
|
* @param {boolean} numeric If true, IP addresses will be output instead of symbolic hostnames
|
||||||
|
* @param {number} size Size (in bytes) of echo request to send
|
||||||
|
* @param {number} deadline Maximum time in seconds before ping stops, regardless of packets sent
|
||||||
|
* @param {number} timeout Maximum time in seconds to wait for each response
|
||||||
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
|
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
|
||||||
*/
|
*/
|
||||||
exports.ping = async (hostname, size = 56) => {
|
exports.ping = async (
|
||||||
|
destAddr,
|
||||||
|
count = PING_COUNT_DEFAULT,
|
||||||
|
sourceAddr = "",
|
||||||
|
numeric = true,
|
||||||
|
size = PING_PACKET_SIZE_DEFAULT,
|
||||||
|
deadline = PING_GLOBAL_TIMEOUT_DEFAULT,
|
||||||
|
timeout = PING_PER_REQUEST_TIMEOUT_DEFAULT,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await exports.pingAsync(hostname, false, size);
|
return await exports.pingAsync(destAddr, false, count, sourceAddr, numeric, size, deadline, timeout);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If the host cannot be resolved, try again with ipv6
|
// If the host cannot be resolved, try again with ipv6
|
||||||
log.debug("ping", "IPv6 error message: " + e.message);
|
log.debug("ping", "IPv6 error message: " + e.message);
|
||||||
|
|
||||||
// As node-ping does not report a specific error for this, try again if it is an empty message with ipv6 no matter what.
|
// As node-ping does not report a specific error for this, try again if it is an empty message with ipv6 no matter what.
|
||||||
if (!e.message) {
|
if (!e.message) {
|
||||||
return await exports.pingAsync(hostname, true, size);
|
return await exports.pingAsync(destAddr, true, count, sourceAddr, numeric, size, deadline, timeout);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -140,18 +157,35 @@ exports.ping = async (hostname, size = 56) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ping the specified machine
|
* Ping the specified machine
|
||||||
* @param {string} hostname Hostname / address of machine to ping
|
* @param {string} destAddr Hostname / IP address of machine to ping
|
||||||
* @param {boolean} ipv6 Should IPv6 be used?
|
* @param {boolean} ipv6 Should IPv6 be used?
|
||||||
* @param {number} size Size of ping packet to send
|
* @param {number} count Number of packets to send before stopping
|
||||||
|
* @param {string} sourceAddr Source address for sending/receiving echo requests
|
||||||
|
* @param {boolean} numeric If true, IP addresses will be output instead of symbolic hostnames
|
||||||
|
* @param {number} size Size (in bytes) of echo request to send
|
||||||
|
* @param {number} deadline Maximum time in seconds before ping stops, regardless of packets sent
|
||||||
|
* @param {number} timeout Maximum time in seconds to wait for each response
|
||||||
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
|
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
|
||||||
*/
|
*/
|
||||||
exports.pingAsync = function (hostname, ipv6 = false, size = 56) {
|
exports.pingAsync = function (
|
||||||
|
destAddr,
|
||||||
|
ipv6 = false,
|
||||||
|
count = PING_COUNT_DEFAULT,
|
||||||
|
sourceAddr = "",
|
||||||
|
numeric = true,
|
||||||
|
size = PING_PACKET_SIZE_DEFAULT,
|
||||||
|
deadline = PING_GLOBAL_TIMEOUT_DEFAULT,
|
||||||
|
timeout = PING_PER_REQUEST_TIMEOUT_DEFAULT,
|
||||||
|
) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ping.promise.probe(hostname, {
|
ping.promise.probe(destAddr, {
|
||||||
v6: ipv6,
|
v6: ipv6,
|
||||||
min_reply: 1,
|
min_reply: count,
|
||||||
deadline: 10,
|
sourceAddr: sourceAddr,
|
||||||
|
numeric: numeric,
|
||||||
packetSize: size,
|
packetSize: size,
|
||||||
|
deadline: deadline,
|
||||||
|
timeout: timeout
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
// If ping failed, it will set field to unknown
|
// If ping failed, it will set field to unknown
|
||||||
if (res.alive) {
|
if (res.alive) {
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="my-3 form-check">
|
<div class="my-3 form-check">
|
||||||
<input id="show-clickable-link" v-model="monitor.isClickAble" class="form-check-input" type="checkbox" @click="toggleLink(monitor.group_index, monitor.monitor_index)" />
|
<input id="show-clickable-link" v-model="monitor.isClickAble" class="form-check-input" type="checkbox" data-testid="show-clickable-link" @click="toggleLink(monitor.group_index, monitor.monitor_index)" />
|
||||||
<label class="form-check-label" for="show-clickable-link">
|
<label class="form-check-label" for="show-clickable-link">
|
||||||
{{ $t("Show Clickable Link") }}
|
{{ $t("Show Clickable Link") }}
|
||||||
</label>
|
</label>
|
||||||
|
@ -19,6 +19,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom URL -->
|
||||||
|
<template v-if="monitor.isClickAble">
|
||||||
|
<label for="customUrl" class="form-label">{{ $t("Custom URL") }}</label>
|
||||||
|
<input id="customUrl" :value="monitor.url" type="url" class="form-control" data-testid="custom-url-input" @input="e => changeUrl(monitor.group_index, monitor.monitor_index, e.target!.value)">
|
||||||
|
|
||||||
|
<div class="form-text mb-3">
|
||||||
|
{{ $t("customUrlDescription") }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-add-group me-2"
|
class="btn btn-primary btn-add-group me-2"
|
||||||
@click="$refs.badgeGeneratorDialog.show(monitor.id, monitor.name)"
|
@click="$refs.badgeGeneratorDialog.show(monitor.id, monitor.name)"
|
||||||
|
@ -29,7 +39,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal">
|
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal" data-testid="monitor-settings-close">
|
||||||
{{ $t("Close") }}
|
{{ $t("Close") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,6 +88,7 @@ export default {
|
||||||
monitor_index: monitor.index,
|
monitor_index: monitor.index,
|
||||||
group_index: group.index,
|
group_index: group.index,
|
||||||
isClickAble: this.showLink(monitor),
|
isClickAble: this.showLink(monitor),
|
||||||
|
url: monitor.element.url,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.MonitorSettingDialog.show();
|
this.MonitorSettingDialog.show();
|
||||||
|
@ -110,6 +121,17 @@ export default {
|
||||||
}
|
}
|
||||||
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
|
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the value of sendUrl
|
||||||
|
* @param {number} groupIndex Index of group monitor is member of
|
||||||
|
* @param {number} index Index of monitor within group
|
||||||
|
* @param {string} value The new value of the url
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
changeUrl(groupIndex, index, value) {
|
||||||
|
this.$root.publicGroupList[groupIndex].monitorList[index].url = value;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -135,6 +135,7 @@ export default {
|
||||||
"nostr": "Nostr",
|
"nostr": "Nostr",
|
||||||
"ntfy": "Ntfy",
|
"ntfy": "Ntfy",
|
||||||
"octopush": "Octopush",
|
"octopush": "Octopush",
|
||||||
|
"OneChat": "OneChat",
|
||||||
"OneBot": "OneBot",
|
"OneBot": "OneBot",
|
||||||
"Onesender": "Onesender",
|
"Onesender": "Onesender",
|
||||||
"Opsgenie": "Opsgenie",
|
"Opsgenie": "Opsgenie",
|
||||||
|
@ -167,7 +168,8 @@ export default {
|
||||||
"waha": "WhatsApp (WAHA)",
|
"waha": "WhatsApp (WAHA)",
|
||||||
"gtxmessaging": "GtxMessaging",
|
"gtxmessaging": "GtxMessaging",
|
||||||
"Cellsynt": "Cellsynt",
|
"Cellsynt": "Cellsynt",
|
||||||
"SendGrid": "SendGrid"
|
"SendGrid": "SendGrid",
|
||||||
|
"notifery": "Notifery"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Put notifications here if it's not supported in most regions or its documentation is not in English
|
// Put notifications here if it's not supported in most regions or its documentation is not in English
|
||||||
|
@ -184,6 +186,7 @@ export default {
|
||||||
"WeCom": "WeCom (企业微信群机器人)",
|
"WeCom": "WeCom (企业微信群机器人)",
|
||||||
"ServerChan": "ServerChan (Server酱)",
|
"ServerChan": "ServerChan (Server酱)",
|
||||||
"PushPlus": "PushPlus (推送加)",
|
"PushPlus": "PushPlus (推送加)",
|
||||||
|
"SpugPush": "SpugPush(Spug推送助手)",
|
||||||
"smsc": "SMSC",
|
"smsc": "SMSC",
|
||||||
"WPush": "WPush(wpush.cn)",
|
"WPush": "WPush(wpush.cn)",
|
||||||
"YZJ": "YZJ (云之家自定义机器人)",
|
"YZJ": "YZJ (云之家自定义机器人)",
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
v-if="editMode"
|
v-if="editMode"
|
||||||
:class="{'link-active': true, 'btn-link': true}"
|
:class="{'link-active': true, 'btn-link': true}"
|
||||||
icon="cog" class="action me-3"
|
icon="cog" class="action me-3"
|
||||||
|
data-testid="monitor-settings"
|
||||||
@click="$refs.monitorSettingDialog.show(group, monitor)"
|
@click="$refs.monitorSettingDialog.show(group, monitor)"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -53,6 +53,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input id="discord-disable-url" v-model="$parent.notification.disableUrl" class="form-check-input" type="checkbox" role="switch">
|
||||||
|
<label class="form-check-label" for="discord-disable-url">{{ $t("Disable URL in Notification") }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
@ -60,6 +67,9 @@ export default {
|
||||||
if (!this.$parent.notification.discordChannelType) {
|
if (!this.$parent.notification.discordChannelType) {
|
||||||
this.$parent.notification.discordChannelType = "channel";
|
this.$parent.notification.discordChannelType = "channel";
|
||||||
}
|
}
|
||||||
|
if (this.$parent.notification.disableUrl === undefined) {
|
||||||
|
this.$parent.notification.disableUrl = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="flashduty-integration-url" class="form-label">Integration Key</label>
|
<label for="flashduty-integration-url" class="form-label">{{ $t("FlashDuty Push URL") }} <span style="color: red;"><sup>*</sup></span></label>
|
||||||
<HiddenInput id="flashduty-integration-url" v-model="$parent.notification.flashdutyIntegrationKey" autocomplete="false"></HiddenInput>
|
<HiddenInput id="flashduty-integration-url" v-model="$parent.notification.flashdutyIntegrationKey" autocomplete="false" :placeholder="$t('FlashDuty Push URL Placeholder')" />
|
||||||
|
<div class="form-text">
|
||||||
|
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
||||||
|
</div>
|
||||||
<i18n-t tag="div" keypath="wayToGetFlashDutyKey" class="form-text">
|
<i18n-t tag="div" keypath="wayToGetFlashDutyKey" class="form-text">
|
||||||
<a href="https://flashcat.cloud/product/flashduty?from=kuma" target="_blank">{{ $t("here") }}</a>
|
<a href="https://flashcat.cloud/product/flashduty?from=kuma" target="_blank">{{ $t("here") }}</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
|
@ -18,7 +21,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import HiddenInput from "../HiddenInput.vue";
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
HiddenInput,
|
HiddenInput,
|
||||||
|
|
49
src/components/notifications/Notifery.vue
Normal file
49
src/components/notifications/Notifery.vue
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="notifery-api-key" class="form-label">{{
|
||||||
|
$t("API Key")
|
||||||
|
}}</label>
|
||||||
|
<HiddenInput
|
||||||
|
id="notifery-api-key"
|
||||||
|
v-model="$parent.notification.notiferyApiKey"
|
||||||
|
:required="true"
|
||||||
|
autocomplete="new-password"
|
||||||
|
></HiddenInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="notifery-title" class="form-label">{{ $t("Title") }}</label>
|
||||||
|
<input
|
||||||
|
id="notifery-title"
|
||||||
|
v-model="$parent.notification.notiferyTitle"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Uptime Kuma Alert"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="notifery-group" class="form-label">{{ $t("Group") }}</label>
|
||||||
|
<input
|
||||||
|
id="notifery-group"
|
||||||
|
v-model="$parent.notification.notiferyGroup"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('Optional')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||||
|
<a href="https://docs.notifery.com/api/event/" target="_blank">https://docs.notifery.com/api/event/</a>
|
||||||
|
</i18n-t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
64
src/components/notifications/OneChat.vue
Normal file
64
src/components/notifications/OneChat.vue
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<!-- Access Token Input -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="onechat-access-token" class="form-label">
|
||||||
|
OneChat Access Token<span style="color: red;"><sup>*</sup></span>
|
||||||
|
</label>
|
||||||
|
<HiddenInput
|
||||||
|
id="onechat-access-token"
|
||||||
|
v-model="$parent.notification.accessToken"
|
||||||
|
:required="true"
|
||||||
|
>
|
||||||
|
</HiddenInput>
|
||||||
|
<div class="form-text">
|
||||||
|
<p>{{ $t("OneChatAccessToken") }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Receiver ID Input -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="onechat-receiver-id" class="form-label">
|
||||||
|
{{ $t("OneChatUserIdOrGroupId") }}<span style="color: red;"><sup>*</sup></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="onechat-receiver-id"
|
||||||
|
v-model="$parent.notification.recieverId"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bot ID Input -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="onechat-bot-id" class="form-label">
|
||||||
|
{{ $t("OneChatBotId") }}<span style="color: red;"><sup>*</sup></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="onechat-bot-id"
|
||||||
|
v-model="$parent.notification.botId"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Document Link -->
|
||||||
|
<div class="form-text">
|
||||||
|
<i18n-t tag="p" keypath="Read more:">
|
||||||
|
<a href="https://chat-develop.one.th/docs" target="_blank">https://chat-develop.one.th/docs</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,31 +1,123 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="smseagle-url" class="form-label">{{ $t("smseagleUrl") }}</label>
|
<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>
|
<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>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="smseagle-token" class="form-label">{{ $t("smseagleToken") }}</label>
|
<label for="smseagle-token" class="form-label">{{ $t("smseagleToken") }}</label>
|
||||||
<HiddenInput id="smseagle-token" v-model="$parent.notification.smseagleToken" :required="true"></HiddenInput>
|
<HiddenInput id="smseagle-token" v-model="$parent.notification.smseagleToken" :required="true"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="smseagle-recipient-type" class="form-label">{{ $t("smseagleRecipientType") }}</label>
|
<label for="smseagle-api-type" class="form-label">{{ $t("smseagleApiType") }} </label>
|
||||||
<select id="smseagle-recipient-type" v-model="$parent.notification.smseagleRecipientType" class="form-select">
|
<select id="smseagle-api-type" v-model="$parent.notification.smseagleApiType" class="form-select">
|
||||||
<option value="smseagle-to" selected>{{ $t("smseagleTo") }}</option>
|
<option value="smseagle-apiv1" selected>{{ $t("smseagleApiv1") }} </option>
|
||||||
<option value="smseagle-group">{{ $t("smseagleGroup") }}</option>
|
<option value="smseagle-apiv2">{{ $t("smseagleApiv2") }} </option>
|
||||||
<option value="smseagle-contact">{{ $t("smseagleContact") }}</option>
|
|
||||||
</select>
|
</select>
|
||||||
|
<i18n-t tag="div" keypath="smseagleDocs" class="form-text">
|
||||||
|
<a href="https://www.smseagle.eu/api/" target="_blank">https://www.smseagle.eu/api/</a>
|
||||||
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div v-if="$parent.notification.smseagleApiType === 'smseagle-apiv1'" class="mb-3">
|
||||||
<label for="smseagle-recipient" class="form-label">{{ $t("smseagleRecipient") }}</label>
|
<div class="mb-3">
|
||||||
<input id="smseagle-recipient" v-model="$parent.notification.smseagleRecipient" type="text" class="form-control" required>
|
<label for="smseagle-recipient-type" class="form-label">{{ $t("smseagleRecipientType") }}</label>
|
||||||
|
<select
|
||||||
|
id="smseagle-recipient-type" v-model="$parent.notification.smseagleRecipientType"
|
||||||
|
class="form-select"
|
||||||
|
>
|
||||||
|
<!-- phone number -->
|
||||||
|
<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
|
||||||
|
v-if="$parent.notification.smseagleMsgType === 'smseagle-sms'
|
||||||
|
|| $parent.notification.smseagleRecipientType !== 'smseagle-to'" 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" required>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="$parent.notification.smseagleMsgType === 'smseagle-sms'
|
||||||
|
|| $parent.notification.smseagleRecipientType !== 'smseagle-to'" 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>
|
||||||
|
<div v-if="$parent.notification.smseagleRecipientType === 'smseagle-to'" class="mb-3">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smseagle-msg-type" class="form-label">{{ $t("smseagleMsgType") }} </label>
|
||||||
|
<select id="smseagle-msg-type" v-model="$parent.notification.smseagleMsgType" class="form-select">
|
||||||
|
<option value="smseagle-sms" selected>{{ $t("smseagleMsgSms") }} </option>
|
||||||
|
<option value="smseagle-ring">{{ $t("smseagleMsgRing") }} </option>
|
||||||
|
<option value="smseagle-tts">{{ $t("smseagleMsgTts") }} </option>
|
||||||
|
<option value="smseagle-tts-advanced">{{ $t("smseagleMsgTtsAdvanced") }} </option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="$parent.notification.smseagleMsgType === 'smseagle-ring'
|
||||||
|
|| $parent.notification.smseagleMsgType === 'smseagle-tts'
|
||||||
|
|| $parent.notification.smseagleMsgType === 'smseagle-tts-advanced'" class="mb-3"
|
||||||
|
>
|
||||||
|
<label for="smseagle-duration" class="form-label">{{ $t("smseagleDuration") }}</label>
|
||||||
|
<input id="smseagle-duration" v-model="$parent.notification.smseagleDuration" type="number" class="form-control" min="0" max="30" step="1" placeholder="10">
|
||||||
|
</div>
|
||||||
|
<div v-if="$parent.notification.smseagleMsgType === 'smseagle-tts-advanced'" class="mb-3">
|
||||||
|
<label for="smseagle-tts-model" class="form-label">{{ $t("smseagleTtsModel") }} </label>
|
||||||
|
<input id="smseagle-tts-model" v-model="$parent.notification.smseagleTtsModel" type="number" class="form-control" placeholder="1" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="smseagle-priority" class="form-label">{{ $t("smseaglePriority") }}</label>
|
<div v-if="$parent.notification.smseagleApiType === 'smseagle-apiv2'" class="mb-3">
|
||||||
<input id="smseagle-priority" v-model="$parent.notification.smseaglePriority" type="number" class="form-control" min="0" max="9" step="1" placeholder="0">
|
<div class="mb-3">
|
||||||
</div>
|
<!-- phone number -->
|
||||||
<div class="mb-3 form-check form-switch">
|
<label for="smseagle-recipient-to" class="form-label">{{ $t("smseagleTo") }}</label>
|
||||||
<label for="smseagle-encoding" class="form-label">{{ $t("smseagleEncoding") }}</label>
|
<input id="smseagle-recipient-to" v-model="$parent.notification.smseagleRecipientTo" type="text" class="form-control">
|
||||||
<input id="smseagle-encoding" v-model="$parent.notification.smseagleEncoding" type="checkbox" class="form-check-input">
|
<i18n-t tag="div" keypath="smseagleComma" class="form-text" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smseagle-recipient-group" class="form-label">{{ $t("smseagleGroupV2") }}</label>
|
||||||
|
<input id="smseagle-recipient-group" v-model="$parent.notification.smseagleRecipientGroup" type="text" class="form-control">
|
||||||
|
<i18n-t tag="div" keypath="smseagleComma" class="form-text" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smseagle-recipient-contact" class="form-label">{{ $t("smseagleContactV2") }}</label>
|
||||||
|
<input id="smseagle-recipient-contact" v-model="$parent.notification.smseagleRecipientContact" type="text" class="form-control">
|
||||||
|
<i18n-t tag="div" keypath="smseagleComma" class="form-text" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smseagle-priority-v2" class="form-label">{{ $t("smseaglePriority") }}</label>
|
||||||
|
<input id="smseagle-priority-v2" 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-v2" class="form-label">{{ $t("smseagleEncoding") }}</label>
|
||||||
|
<input id="smseagle-encoding-v2" v-model="$parent.notification.smseagleEncoding" type="checkbox" class="form-check-input">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smseagle-msg-type-v2" class="form-label">{{ $t("smseagleMsgType") }} </label>
|
||||||
|
<select id="smseagle-msg-type-v2" v-model="$parent.notification.smseagleMsgType" class="form-select">
|
||||||
|
<option value="smseagle-sms" selected>{{ $t("smseagleMsgSms") }} </option>
|
||||||
|
<option value="smseagle-ring">{{ $t("smseagleMsgRing") }} </option>
|
||||||
|
<option value="smseagle-tts">{{ $t("smseagleMsgTts") }} </option>
|
||||||
|
<option value="smseagle-tts-advanced">{{ $t("smseagleMsgTtsAdvanced") }} </option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div v-if="$parent.notification.smseagleMsgType && $parent.notification.smseagleMsgType !== 'smseagle-sms'" class="mb-3">
|
||||||
|
<label for="smseagle-duration-v2" class="form-label">{{ $t("smseagleDuration") }}</label>
|
||||||
|
<input id="smseagle-duration-v2" v-model="$parent.notification.smseagleDuration" type="number" class="form-control" min="0" max="30" step="1" placeholder="10">
|
||||||
|
</div>
|
||||||
|
<div v-if="$parent.notification.smseagleMsgType === 'smseagle-tts-advanced'" class="mb-3">
|
||||||
|
<label for="smseagle-tts-model-v2" class="form-label">{{ $t("smseagleTtsModel") }} </label>
|
||||||
|
<input id="smseagle-tts-model-v2" v-model="$parent.notification.smseagleTtsModel" type="number" class="form-control" placeholder="1" required>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -36,5 +128,16 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
HiddenInput,
|
HiddenInput,
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
if (!this.$parent.notification.smseagleApiType) {
|
||||||
|
this.$parent.notification.smseagleApiType = "smseagle-apiv1";
|
||||||
|
}
|
||||||
|
if (!this.$parent.notification.smseagleMsgType) {
|
||||||
|
this.$parent.notification.smseagleMsgType = "smseagle-sms";
|
||||||
|
}
|
||||||
|
if (!this.$parent.notification.smseagleRecipientType) {
|
||||||
|
this.$parent.notification.smseagleRecipientType = "smseagle-to";
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -79,6 +79,15 @@
|
||||||
<div class="form-text">{{ $t("leave blank for default body") }}</div>
|
<div class="form-text">{{ $t("leave blank for default body") }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input id="use-html-body" v-model="$parent.notification.htmlBody" class="form-check-input" type="checkbox" value="">
|
||||||
|
<label class="form-check-label" for="use-html-body">
|
||||||
|
{{ $t("Use HTML for custom E-mail body") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ToggleSection :heading="$t('smtpDkimSettings')">
|
<ToggleSection :heading="$t('smtpDkimSettings')">
|
||||||
<i18n-t tag="div" keypath="smtpDkimDesc" class="form-text mb-3">
|
<i18n-t tag="div" keypath="smtpDkimDesc" class="form-text mb-3">
|
||||||
<a href="https://nodemailer.com/dkim/" target="_blank">{{ $t("documentation") }}</a>
|
<a href="https://nodemailer.com/dkim/" target="_blank">{{ $t("documentation") }}</a>
|
||||||
|
|
19
src/components/notifications/SpugPush.vue
Normal file
19
src/components/notifications/SpugPush.vue
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="spugpush-templateKey" class="form-label">{{ $t("SpugPush Template Code") }}</label>
|
||||||
|
<HiddenInput id="spugpush-templateKey" v-model="$parent.notification.templateKey" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||||
|
<a href="https://push.spug.cc/guide/plugin/kuma" rel="noopener noreferrer" target="_blank">https://push.spug.cc</a>
|
||||||
|
</i18n-t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -4,6 +4,7 @@ import AliyunSMS from "./AliyunSms.vue";
|
||||||
import Apprise from "./Apprise.vue";
|
import Apprise from "./Apprise.vue";
|
||||||
import Bark from "./Bark.vue";
|
import Bark from "./Bark.vue";
|
||||||
import Bitrix24 from "./Bitrix24.vue";
|
import Bitrix24 from "./Bitrix24.vue";
|
||||||
|
import Notifery from "./Notifery.vue";
|
||||||
import ClickSendSMS from "./ClickSendSMS.vue";
|
import ClickSendSMS from "./ClickSendSMS.vue";
|
||||||
import CallMeBot from "./CallMeBot.vue";
|
import CallMeBot from "./CallMeBot.vue";
|
||||||
import SMSC from "./SMSC.vue";
|
import SMSC from "./SMSC.vue";
|
||||||
|
@ -29,6 +30,7 @@ import Mattermost from "./Mattermost.vue";
|
||||||
import Nostr from "./Nostr.vue";
|
import Nostr from "./Nostr.vue";
|
||||||
import Ntfy from "./Ntfy.vue";
|
import Ntfy from "./Ntfy.vue";
|
||||||
import Octopush from "./Octopush.vue";
|
import Octopush from "./Octopush.vue";
|
||||||
|
import OneChat from "./OneChat.vue";
|
||||||
import OneBot from "./OneBot.vue";
|
import OneBot from "./OneBot.vue";
|
||||||
import Onesender from "./Onesender.vue";
|
import Onesender from "./Onesender.vue";
|
||||||
import Opsgenie from "./Opsgenie.vue";
|
import Opsgenie from "./Opsgenie.vue";
|
||||||
|
@ -63,6 +65,7 @@ import WeCom from "./WeCom.vue";
|
||||||
import GoAlert from "./GoAlert.vue";
|
import GoAlert from "./GoAlert.vue";
|
||||||
import ZohoCliq from "./ZohoCliq.vue";
|
import ZohoCliq from "./ZohoCliq.vue";
|
||||||
import Splunk from "./Splunk.vue";
|
import Splunk from "./Splunk.vue";
|
||||||
|
import SpugPush from "./SpugPush.vue";
|
||||||
import SevenIO from "./SevenIO.vue";
|
import SevenIO from "./SevenIO.vue";
|
||||||
import Whapi from "./Whapi.vue";
|
import Whapi from "./Whapi.vue";
|
||||||
import WAHA from "./WAHA.vue";
|
import WAHA from "./WAHA.vue";
|
||||||
|
@ -108,6 +111,7 @@ const NotificationFormList = {
|
||||||
"nostr": Nostr,
|
"nostr": Nostr,
|
||||||
"ntfy": Ntfy,
|
"ntfy": Ntfy,
|
||||||
"octopush": Octopush,
|
"octopush": Octopush,
|
||||||
|
"OneChat": OneChat,
|
||||||
"OneBot": OneBot,
|
"OneBot": OneBot,
|
||||||
"Onesender": Onesender,
|
"Onesender": Onesender,
|
||||||
"Opsgenie": Opsgenie,
|
"Opsgenie": Opsgenie,
|
||||||
|
@ -138,6 +142,7 @@ const NotificationFormList = {
|
||||||
"threema": Threema,
|
"threema": Threema,
|
||||||
"twilio": Twilio,
|
"twilio": Twilio,
|
||||||
"Splunk": Splunk,
|
"Splunk": Splunk,
|
||||||
|
"SpugPush": SpugPush,
|
||||||
"webhook": Webhook,
|
"webhook": Webhook,
|
||||||
"WeCom": WeCom,
|
"WeCom": WeCom,
|
||||||
"GoAlert": GoAlert,
|
"GoAlert": GoAlert,
|
||||||
|
@ -145,6 +150,7 @@ const NotificationFormList = {
|
||||||
"ZohoCliq": ZohoCliq,
|
"ZohoCliq": ZohoCliq,
|
||||||
"SevenIO": SevenIO,
|
"SevenIO": SevenIO,
|
||||||
"whapi": Whapi,
|
"whapi": Whapi,
|
||||||
|
"notifery": Notifery,
|
||||||
"waha": WAHA,
|
"waha": WAHA,
|
||||||
"gtxmessaging": GtxMessaging,
|
"gtxmessaging": GtxMessaging,
|
||||||
"Cellsynt": Cellsynt,
|
"Cellsynt": Cellsynt,
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
"Expected Value": "Expected Value",
|
"Expected Value": "Expected Value",
|
||||||
"Json Query Expression": "Json Query Expression",
|
"Json Query Expression": "Json Query Expression",
|
||||||
"Friendly Name": "Friendly Name",
|
"Friendly Name": "Friendly Name",
|
||||||
|
"defaultFriendlyName": "New Monitor",
|
||||||
"URL": "URL",
|
"URL": "URL",
|
||||||
"Hostname": "Hostname",
|
"Hostname": "Hostname",
|
||||||
"Host URL": "Host URL",
|
"Host URL": "Host URL",
|
||||||
|
@ -517,6 +518,7 @@
|
||||||
"Clone": "Clone",
|
"Clone": "Clone",
|
||||||
"cloneOf": "Clone of {0}",
|
"cloneOf": "Clone of {0}",
|
||||||
"smtp": "Email (SMTP)",
|
"smtp": "Email (SMTP)",
|
||||||
|
"Use HTML for custom E-mail body": "Use HTML for custom E-mail body",
|
||||||
"secureOptionNone": "None / STARTTLS (25, 587)",
|
"secureOptionNone": "None / STARTTLS (25, 587)",
|
||||||
"secureOptionTLS": "TLS (465)",
|
"secureOptionTLS": "TLS (465)",
|
||||||
"Ignore TLS Error": "Ignore TLS Error",
|
"Ignore TLS Error": "Ignore TLS Error",
|
||||||
|
@ -757,12 +759,26 @@
|
||||||
"smseagleTo": "Phone number(s)",
|
"smseagleTo": "Phone number(s)",
|
||||||
"smseagleGroup": "Phonebook group name(s)",
|
"smseagleGroup": "Phonebook group name(s)",
|
||||||
"smseagleContact": "Phonebook contact name(s)",
|
"smseagleContact": "Phonebook contact name(s)",
|
||||||
|
"smseagleGroupV2": "Phonebook group ID(s)",
|
||||||
|
"smseagleContactV2": "Phonebook contact ID(s)",
|
||||||
"smseagleRecipientType": "Recipient type",
|
"smseagleRecipientType": "Recipient type",
|
||||||
"smseagleRecipient": "Recipient(s) (multiple must be separated with comma)",
|
"smseagleRecipient": "Recipient(s) (multiple must be separated with comma)",
|
||||||
"smseagleToken": "API Access token",
|
"smseagleToken": "API Access token",
|
||||||
"smseagleUrl": "Your SMSEagle device URL",
|
"smseagleUrl": "Your SMSEagle device URL",
|
||||||
"smseagleEncoding": "Send as Unicode",
|
"smseagleEncoding": "Send as Unicode (default=GSM-7)",
|
||||||
"smseaglePriority": "Message priority (0-9, default = 0)",
|
"smseaglePriority": "Message priority (0-9, highest priority = 9)",
|
||||||
|
"smseagleMsgType": "Message type",
|
||||||
|
"smseagleMsgSms": "Sms message (default)",
|
||||||
|
"smseagleMsgRing": "Ring call",
|
||||||
|
"smseagleMsgTts": "Text-to-speech call",
|
||||||
|
"smseagleMsgTtsAdvanced": "Text-to-speech Advanced call",
|
||||||
|
"smseagleDuration": "Duration (in seconds)",
|
||||||
|
"smseagleTtsModel": "Text-to-speech model ID",
|
||||||
|
"smseagleApiType": "API version",
|
||||||
|
"smseagleApiv1": "APIv1 (for existing projects and backward compatibility)",
|
||||||
|
"smseagleApiv2": "APIv2 (recommended for new integrations)",
|
||||||
|
"smseagleDocs": "Check documentation or APIv2 availability: {0}",
|
||||||
|
"smseagleComma": "Multiple must be separated with comma",
|
||||||
"smspartnerApiurl": "You can find your API key in your dashboard at {0}",
|
"smspartnerApiurl": "You can find your API key in your dashboard at {0}",
|
||||||
"smspartnerPhoneNumber": "Phone number(s)",
|
"smspartnerPhoneNumber": "Phone number(s)",
|
||||||
"smspartnerPhoneNumberHelptext": "The number must be in the international format {0}, {1}. Multiple numbers must be separated by {2}",
|
"smspartnerPhoneNumberHelptext": "The number must be in the international format {0}, {1}. Multiple numbers must be separated by {2}",
|
||||||
|
@ -784,6 +800,7 @@
|
||||||
"PushDeer Server": "PushDeer Server",
|
"PushDeer Server": "PushDeer Server",
|
||||||
"pushDeerServerDescription": "Leave blank to use the official server",
|
"pushDeerServerDescription": "Leave blank to use the official server",
|
||||||
"PushDeer Key": "PushDeer Key",
|
"PushDeer Key": "PushDeer Key",
|
||||||
|
"SpugPush Template Code": "Template Code",
|
||||||
"wayToGetClickSendSMSToken": "You can get API Username and API Key from {0} .",
|
"wayToGetClickSendSMSToken": "You can get API Username and API Key from {0} .",
|
||||||
"Custom Monitor Type": "Custom Monitor Type",
|
"Custom Monitor Type": "Custom Monitor Type",
|
||||||
"Google Analytics ID": "Google Analytics ID",
|
"Google Analytics ID": "Google Analytics ID",
|
||||||
|
@ -878,8 +895,10 @@
|
||||||
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
|
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
|
||||||
"Close": "Close",
|
"Close": "Close",
|
||||||
"Request Body": "Request Body",
|
"Request Body": "Request Body",
|
||||||
"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",
|
"wayToGetFlashDutyKey": "To integrate Uptime Kuma with Flashduty: Go to Channels > Select a channel > Integrations > Add a new integration, choose Uptime Kuma, and copy the Push URL.",
|
||||||
"FlashDuty Severity": "Severity",
|
"FlashDuty Severity": "Severity",
|
||||||
|
"FlashDuty Push URL": "Push URL",
|
||||||
|
"FlashDuty Push URL Placeholder": "Copy from the alerting integration page",
|
||||||
"nostrRelays": "Nostr relays",
|
"nostrRelays": "Nostr relays",
|
||||||
"nostrRelaysHelp": "One relay URL per line",
|
"nostrRelaysHelp": "One relay URL per line",
|
||||||
"nostrSender": "Sender Private Key (nsec)",
|
"nostrSender": "Sender Private Key (nsec)",
|
||||||
|
@ -1057,6 +1076,21 @@
|
||||||
"rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.",
|
"rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.",
|
||||||
"SendGrid API Key": "SendGrid API Key",
|
"SendGrid API Key": "SendGrid API Key",
|
||||||
"Separate multiple email addresses with commas": "Separate multiple email addresses with commas",
|
"Separate multiple email addresses with commas": "Separate multiple email addresses with commas",
|
||||||
|
"pingCountLabel": "Max Packets",
|
||||||
|
"pingCountDescription": "Number of packets to send before stopping",
|
||||||
|
"pingNumericLabel": "Numeric Output",
|
||||||
|
"pingNumericDescription": "If checked, IP addresses will be output instead of symbolic hostnames",
|
||||||
|
"pingGlobalTimeoutLabel": "Global Timeout",
|
||||||
|
"pingGlobalTimeoutDescription": "Total time in seconds before ping stops, regardless of packets sent",
|
||||||
|
"pingPerRequestTimeoutLabel": "Per-Ping Timeout",
|
||||||
|
"pingPerRequestTimeoutDescription": "This is the maximum waiting time (in seconds) before considering a single ping packet lost",
|
||||||
|
"pingIntervalAdjustedInfo": "Interval adjusted based on packet count, global timeout and per-ping timeout",
|
||||||
|
"smtpHelpText": "'SMTPS' tests that SMTP/TLS is working; 'Ignore TLS' connects over plaintext; 'STARTTLS' connects, issues a STARTTLS command and verifies the server certificate. None of these send an email.",
|
||||||
|
"Custom URL": "Custom URL",
|
||||||
|
"customUrlDescription": "Will be used as the clickable URL instead of the monitor's one.",
|
||||||
|
"OneChatAccessToken": "OneChat Access Token",
|
||||||
|
"OneChatUserIdOrGroupId": "OneChat User ID or Group ID",
|
||||||
|
"OneChatBotId": "OneChat Bot ID",
|
||||||
"wahaSession": "Session",
|
"wahaSession": "Session",
|
||||||
"wahaChatId": "Chat ID (Phone Number / Contact ID / Group ID)",
|
"wahaChatId": "Chat ID (Phone Number / Contact ID / Group ID)",
|
||||||
"wayToGetWahaApiUrl": "Your WAHA Instance URL.",
|
"wayToGetWahaApiUrl": "Your WAHA Instance URL.",
|
||||||
|
@ -1074,5 +1108,6 @@
|
||||||
"the smsplanet documentation": "the smsplanet documentation",
|
"the smsplanet documentation": "the smsplanet documentation",
|
||||||
"Phone numbers": "Phone numbers",
|
"Phone numbers": "Phone numbers",
|
||||||
"Sender name": "Sender name",
|
"Sender name": "Sender name",
|
||||||
"smsplanetNeedToApproveName": "Needs to be approved in the client panel"
|
"smsplanetNeedToApproveName": "Needs to be approved in the client panel",
|
||||||
|
"Disable URL in Notification": "Disable URL in Notification"
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,9 @@
|
||||||
<option value="ping">
|
<option value="ping">
|
||||||
Ping
|
Ping
|
||||||
</option>
|
</option>
|
||||||
|
<option value="smtp">
|
||||||
|
SMTP
|
||||||
|
</option>
|
||||||
<option value="snmp">
|
<option value="snmp">
|
||||||
SNMP
|
SNMP
|
||||||
</option>
|
</option>
|
||||||
|
@ -109,7 +112,7 @@
|
||||||
<!-- Friendly Name -->
|
<!-- Friendly Name -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
|
<label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
|
||||||
<input id="name" v-model="monitor.name" type="text" class="form-control" required data-testid="friendly-name-input">
|
<input id="name" v-model="monitor.name" type="text" class="form-control" data-testid="friendly-name-input" :placeholder="defaultFriendlyName">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- URL -->
|
<!-- URL -->
|
||||||
|
@ -281,8 +284,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Hostname -->
|
<!-- Hostname -->
|
||||||
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / SNMP only -->
|
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / SNMP / SMTP only -->
|
||||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'snmp'" class="my-3">
|
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'smtp' || monitor.type === 'snmp'" class="my-3">
|
||||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||||
<input
|
<input
|
||||||
id="hostname"
|
id="hostname"
|
||||||
|
@ -297,7 +300,7 @@
|
||||||
|
|
||||||
<!-- Port -->
|
<!-- Port -->
|
||||||
<!-- For TCP Port / Steam / MQTT / Radius Type / SNMP -->
|
<!-- For TCP Port / Steam / MQTT / Radius Type / SNMP -->
|
||||||
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'snmp'" class="my-3">
|
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'smtp' || monitor.type === 'snmp'" class="my-3">
|
||||||
<label for="port" class="form-label">{{ $t("Port") }}</label>
|
<label for="port" class="form-label">{{ $t("Port") }}</label>
|
||||||
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
|
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||||
</div>
|
</div>
|
||||||
|
@ -329,6 +332,18 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="monitor.type === 'smtp'" class="my-3">
|
||||||
|
<label for="smtp_security" class="form-label">{{ $t("SMTP Security") }}</label>
|
||||||
|
<select id="smtp_security" v-model="monitor.smtpSecurity" class="form-select">
|
||||||
|
<option value="secure">SMTPS</option>
|
||||||
|
<option value="nostarttls">Ignore STARTTLS</option>
|
||||||
|
<option value="starttls">Use STARTTLS</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("smtpHelpText") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Json Query -->
|
<!-- Json Query -->
|
||||||
<!-- For Json Query / SNMP -->
|
<!-- For Json Query / SNMP -->
|
||||||
<div v-if="monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3">
|
<div v-if="monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3">
|
||||||
|
@ -595,10 +610,14 @@
|
||||||
<input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required :min="minInterval" step="1">
|
<input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required :min="minInterval" step="1">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Timeout: HTTP / Keyword / SNMP only -->
|
<!-- Timeout: HTTP / JSON query / Keyword / Ping / RabbitMQ / SNMP only -->
|
||||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'snmp' || monitor.type === 'rabbitmq'" class="my-3">
|
<div v-if="monitor.type === 'http' || monitor.type === 'json-query' || monitor.type === 'keyword' || monitor.type === 'ping' || monitor.type === 'rabbitmq' || monitor.type === 'snmp'" class="my-3">
|
||||||
<label for="timeout" class="form-label">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label>
|
<label for="timeout" class="form-label">
|
||||||
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
|
{{ monitor.type === 'ping' ? $t("pingGlobalTimeoutLabel") : $t("Request Timeout") }}
|
||||||
|
<span v-if="monitor.type !== 'ping'">({{ $t("timeoutAfter", [monitor.timeout || clampTimeout(monitor.interval)]) }})</span>
|
||||||
|
</label>
|
||||||
|
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" :min="timeoutMin" :max="timeoutMax" :step="timeoutStep" required>
|
||||||
|
<div v-if="monitor.type === 'ping'" class="form-text">{{ $t("pingGlobalTimeoutDescription") }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
|
@ -660,10 +679,39 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ping packet size -->
|
<!-- Max Packets / Count -->
|
||||||
|
<div v-if="monitor.type === 'ping'" class="my-3">
|
||||||
|
<label for="ping-count" class="form-label">{{ $t("pingCountLabel") }}</label>
|
||||||
|
<input id="ping-count" v-model="monitor.ping_count" type="number" class="form-control" required min="1" max="100" step="1">
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("pingCountDescription") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Numeric Output -->
|
||||||
|
<div v-if="monitor.type === 'ping'" class="my-3 form-check">
|
||||||
|
<input id="ping_numeric" v-model="monitor.ping_numeric" type="checkbox" class="form-check-input" :checked="monitor.ping_numeric">
|
||||||
|
<label class="form-check-label" for="ping_numeric">
|
||||||
|
{{ $t("pingNumericLabel") }}
|
||||||
|
</label>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("pingNumericDescription") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Packet size -->
|
||||||
<div v-if="monitor.type === 'ping'" class="my-3">
|
<div v-if="monitor.type === 'ping'" class="my-3">
|
||||||
<label for="packet-size" class="form-label">{{ $t("Packet Size") }}</label>
|
<label for="packet-size" class="form-label">{{ $t("Packet Size") }}</label>
|
||||||
<input id="packet-size" v-model="monitor.packetSize" type="number" class="form-control" required min="1" max="65500" step="1">
|
<input id="packet-size" v-model="monitor.packetSize" type="number" class="form-control" required min="1" :max="65500" step="1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- per-request timeout -->
|
||||||
|
<div v-if="monitor.type === 'ping'" class="my-3">
|
||||||
|
<label for="ping_per_request_timeout" class="form-label">{{ $t("pingPerRequestTimeoutLabel") }}</label>
|
||||||
|
<input id="ping_per_request_timeout" v-model="monitor.ping_per_request_timeout" type="number" class="form-control" required min="0" max="300" step="1">
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("pingPerRequestTimeoutDescription") }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- HTTP / Keyword only -->
|
<!-- HTTP / Keyword only -->
|
||||||
|
@ -1060,7 +1108,13 @@ import DockerHostDialog from "../components/DockerHostDialog.vue";
|
||||||
import RemoteBrowserDialog from "../components/RemoteBrowserDialog.vue";
|
import RemoteBrowserDialog from "../components/RemoteBrowserDialog.vue";
|
||||||
import ProxyDialog from "../components/ProxyDialog.vue";
|
import ProxyDialog from "../components/ProxyDialog.vue";
|
||||||
import TagsManager from "../components/TagsManager.vue";
|
import TagsManager from "../components/TagsManager.vue";
|
||||||
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, sleep } from "../util.ts";
|
import {
|
||||||
|
genSecret,
|
||||||
|
isDev,
|
||||||
|
MAX_INTERVAL_SECOND,
|
||||||
|
MIN_INTERVAL_SECOND,
|
||||||
|
sleep,
|
||||||
|
} from "../util.ts";
|
||||||
import { hostNameRegexPattern } from "../util-frontend";
|
import { hostNameRegexPattern } from "../util-frontend";
|
||||||
import HiddenInput from "../components/HiddenInput.vue";
|
import HiddenInput from "../components/HiddenInput.vue";
|
||||||
import EditMonitorConditions from "../components/EditMonitorConditions.vue";
|
import EditMonitorConditions from "../components/EditMonitorConditions.vue";
|
||||||
|
@ -1082,7 +1136,6 @@ const monitorDefaults = {
|
||||||
notificationIDList: {},
|
notificationIDList: {},
|
||||||
ignoreTls: false,
|
ignoreTls: false,
|
||||||
upsideDown: false,
|
upsideDown: false,
|
||||||
packetSize: 56,
|
|
||||||
expiryNotification: false,
|
expiryNotification: false,
|
||||||
maxredirects: 10,
|
maxredirects: 10,
|
||||||
accepted_statuscodes: [ "200-299" ],
|
accepted_statuscodes: [ "200-299" ],
|
||||||
|
@ -1157,6 +1210,48 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
timeoutStep() {
|
||||||
|
return this.monitor.type === "ping" ? 1 : 0.1;
|
||||||
|
},
|
||||||
|
|
||||||
|
timeoutMin() {
|
||||||
|
return this.monitor.type === "ping" ? 1 : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
timeoutMax() {
|
||||||
|
return this.monitor.type === "ping" ? 60 : undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
timeoutLabel() {
|
||||||
|
return this.monitor.type === "ping" ? this.$t("pingTimeoutLabel") : this.$t("Request Timeout");
|
||||||
|
},
|
||||||
|
|
||||||
|
timeoutDescription() {
|
||||||
|
if (this.monitor.type === "ping") {
|
||||||
|
return this.$t("pingTimeoutDescription");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultFriendlyName() {
|
||||||
|
if (this.monitor.hostname) {
|
||||||
|
return this.monitor.hostname;
|
||||||
|
}
|
||||||
|
if (this.monitor.url) {
|
||||||
|
if (this.monitor.url !== "http://" && this.monitor.url !== "https://") {
|
||||||
|
// Ensure monitor without a URL is not affected by invisible URL.
|
||||||
|
try {
|
||||||
|
const url = new URL(this.monitor.url);
|
||||||
|
return url.hostname;
|
||||||
|
} catch (e) {
|
||||||
|
return this.monitor.url.replace(/https?:\/\//, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default placeholder if neither hostname nor URL is available
|
||||||
|
return this.$t("defaultFriendlyName");
|
||||||
|
},
|
||||||
|
|
||||||
ipRegex() {
|
ipRegex() {
|
||||||
|
|
||||||
// Allow to test with simple dns server with port (127.0.0.1:5300)
|
// Allow to test with simple dns server with port (127.0.0.1:5300)
|
||||||
|
@ -1175,6 +1270,7 @@ export default {
|
||||||
}
|
}
|
||||||
return this.$t(name);
|
return this.$t(name);
|
||||||
},
|
},
|
||||||
|
|
||||||
remoteBrowsersOptions() {
|
remoteBrowsersOptions() {
|
||||||
return this.$root.remoteBrowserList.map(browser => {
|
return this.$root.remoteBrowserList.map(browser => {
|
||||||
return {
|
return {
|
||||||
|
@ -1183,6 +1279,7 @@ export default {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
remoteBrowsersToggle: {
|
remoteBrowsersToggle: {
|
||||||
get() {
|
get() {
|
||||||
return this.remoteBrowsersEnabled || this.monitor.remote_browser != null;
|
return this.remoteBrowsersEnabled || this.monitor.remote_browser != null;
|
||||||
|
@ -1200,6 +1297,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
isAdd() {
|
isAdd() {
|
||||||
return this.$route.path === "/add";
|
return this.$route.path === "/add";
|
||||||
},
|
},
|
||||||
|
@ -1250,6 +1348,7 @@ message HealthCheckResponse {
|
||||||
}
|
}
|
||||||
` ]);
|
` ]);
|
||||||
},
|
},
|
||||||
|
|
||||||
bodyPlaceholder() {
|
bodyPlaceholder() {
|
||||||
if (this.monitor && this.monitor.httpBodyEncoding && this.monitor.httpBodyEncoding === "xml") {
|
if (this.monitor && this.monitor.httpBodyEncoding && this.monitor.httpBodyEncoding === "xml") {
|
||||||
return this.$t("Example:", [ `
|
return this.$t("Example:", [ `
|
||||||
|
@ -1415,9 +1514,25 @@ message HealthCheckResponse {
|
||||||
},
|
},
|
||||||
|
|
||||||
"monitor.timeout"(value, oldValue) {
|
"monitor.timeout"(value, oldValue) {
|
||||||
// keep timeout within 80% range
|
if (this.monitor.type === "ping") {
|
||||||
if (value && value !== oldValue) {
|
this.finishUpdateInterval();
|
||||||
this.monitor.timeout = this.clampTimeout(value);
|
} else {
|
||||||
|
// keep timeout within 80% range
|
||||||
|
if (value && value !== oldValue) {
|
||||||
|
this.monitor.timeout = this.clampTimeout(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"monitor.ping_count"() {
|
||||||
|
if (this.monitor.type === "ping") {
|
||||||
|
this.finishUpdateInterval();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"monitor.ping_per_request_timeout"() {
|
||||||
|
if (this.monitor.type === "ping") {
|
||||||
|
this.finishUpdateInterval();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1446,8 +1561,10 @@ message HealthCheckResponse {
|
||||||
// Set a default timeout if the monitor type has changed or if it's a new monitor
|
// Set a default timeout if the monitor type has changed or if it's a new monitor
|
||||||
if (oldType || this.isAdd) {
|
if (oldType || this.isAdd) {
|
||||||
if (this.monitor.type === "snmp") {
|
if (this.monitor.type === "snmp") {
|
||||||
// snmp is not expected to be executed via the internet => we can choose a lower default timeout
|
// snmp is not expected to be executed via the internet => we can choose a lower default timeout
|
||||||
this.monitor.timeout = 5;
|
this.monitor.timeout = 5;
|
||||||
|
} else if (this.monitor.type === "ping") {
|
||||||
|
this.monitor.timeout = 10;
|
||||||
} else {
|
} else {
|
||||||
this.monitor.timeout = 48;
|
this.monitor.timeout = 48;
|
||||||
}
|
}
|
||||||
|
@ -1564,7 +1681,11 @@ message HealthCheckResponse {
|
||||||
if (this.isAdd) {
|
if (this.isAdd) {
|
||||||
|
|
||||||
this.monitor = {
|
this.monitor = {
|
||||||
...monitorDefaults
|
...monitorDefaults,
|
||||||
|
ping_count: 3,
|
||||||
|
ping_numeric: true,
|
||||||
|
packetSize: 56,
|
||||||
|
ping_per_request_timeout: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.$root.proxyList && !this.monitor.proxyId) {
|
if (this.$root.proxyList && !this.monitor.proxyId) {
|
||||||
|
@ -1627,7 +1748,12 @@ message HealthCheckResponse {
|
||||||
}
|
}
|
||||||
// Handling for monitors that are missing/zeroed timeout
|
// Handling for monitors that are missing/zeroed timeout
|
||||||
if (!this.monitor.timeout) {
|
if (!this.monitor.timeout) {
|
||||||
this.monitor.timeout = ~~(this.monitor.interval * 8) / 10;
|
if (this.monitor.type === "ping") {
|
||||||
|
// set to default
|
||||||
|
this.monitor.timeout = 10;
|
||||||
|
} else {
|
||||||
|
this.monitor.timeout = ~~(this.monitor.interval * 8) / 10;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$root.toastError(res.msg);
|
this.$root.toastError(res.msg);
|
||||||
|
@ -1700,6 +1826,10 @@ message HealthCheckResponse {
|
||||||
|
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
|
if (!this.monitor.name) {
|
||||||
|
this.monitor.name = this.defaultFriendlyName;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.isInputValid()) {
|
if (!this.isInputValid()) {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
return;
|
return;
|
||||||
|
@ -1840,11 +1970,48 @@ message HealthCheckResponse {
|
||||||
return Number.isFinite(clamped) ? clamped : maxTimeout;
|
return Number.isFinite(clamped) ? clamped : maxTimeout;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
calculatePingInterval() {
|
||||||
|
// If monitor.type is not "ping", simply return the configured interval
|
||||||
|
if (this.monitor.type !== "ping") {
|
||||||
|
return this.monitor.interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the maximum theoretical time needed if every ping request times out
|
||||||
|
const theoreticalTotal = this.monitor.ping_count * this.monitor.ping_per_request_timeout;
|
||||||
|
|
||||||
|
// The global timeout (aka deadline) forces ping to terminate, so the effective limit
|
||||||
|
// is the smaller value between deadline and theoreticalTotal
|
||||||
|
const effectiveLimit = Math.min(this.monitor.timeout, theoreticalTotal);
|
||||||
|
|
||||||
|
// Add a 10% margin to the effective limit to ensure proper handling
|
||||||
|
const adjustedLimit = Math.ceil(effectiveLimit * 1.1);
|
||||||
|
|
||||||
|
// If the calculated limit is lower than the minimum allowed interval, use the minimum interval
|
||||||
|
if (adjustedLimit < this.minInterval) {
|
||||||
|
return this.minInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
return adjustedLimit;
|
||||||
|
},
|
||||||
|
|
||||||
finishUpdateInterval() {
|
finishUpdateInterval() {
|
||||||
// Update timeout if it is greater than the clamp timeout
|
if (this.monitor.type === "ping") {
|
||||||
let clampedValue = this.clampTimeout(this.monitor.interval);
|
// Calculate the minimum required interval based on ping configuration
|
||||||
if (this.monitor.timeout > clampedValue) {
|
const calculatedPingInterval = this.calculatePingInterval();
|
||||||
this.monitor.timeout = clampedValue;
|
|
||||||
|
// If the configured interval is too small, adjust it to the minimum required value
|
||||||
|
if (this.monitor.interval < calculatedPingInterval) {
|
||||||
|
this.monitor.interval = calculatedPingInterval;
|
||||||
|
|
||||||
|
// Notify the user that the interval has been automatically adjusted
|
||||||
|
toast.info(this.$t("pingIntervalAdjustedInfo"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Update timeout if it is greater than the clamp timeout
|
||||||
|
let clampedValue = this.clampTimeout(this.monitor.interval);
|
||||||
|
if (this.monitor.timeout > clampedValue) {
|
||||||
|
this.monitor.timeout = clampedValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
42
src/util.js
42
src/util.js
|
@ -8,15 +8,10 @@
|
||||||
// Backend uses the compiled file util.js
|
// Backend uses the compiled file util.js
|
||||||
// Frontend uses util.ts
|
// Frontend uses util.ts
|
||||||
*/
|
*/
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
||||||
};
|
|
||||||
var _a;
|
var _a;
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = 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.isNode = exports.isDev = void 0;
|
exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_MAX = exports.PING_PER_REQUEST_TIMEOUT_MIN = exports.PING_COUNT_DEFAULT = exports.PING_COUNT_MAX = exports.PING_COUNT_MIN = exports.PING_GLOBAL_TIMEOUT_DEFAULT = exports.PING_GLOBAL_TIMEOUT_MAX = exports.PING_GLOBAL_TIMEOUT_MIN = exports.PING_PACKET_SIZE_DEFAULT = exports.PING_PACKET_SIZE_MAX = exports.PING_PACKET_SIZE_MIN = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = 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.isNode = exports.isDev = void 0;
|
||||||
exports.evaluateJsonQuery = exports.intHash = 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 = void 0;
|
exports.evaluateJsonQuery = exports.intHash = 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.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = void 0;
|
||||||
exports.intHash = 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 = void 0;
|
|
||||||
const dayjs_1 = __importDefault(require("dayjs"));
|
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const jsonata = require("jsonata");
|
const jsonata = require("jsonata");
|
||||||
exports.isDev = process.env.NODE_ENV === "development";
|
exports.isDev = process.env.NODE_ENV === "development";
|
||||||
|
@ -35,6 +30,18 @@ exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
|
||||||
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
|
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
|
||||||
exports.MAX_INTERVAL_SECOND = 2073600;
|
exports.MAX_INTERVAL_SECOND = 2073600;
|
||||||
exports.MIN_INTERVAL_SECOND = 20;
|
exports.MIN_INTERVAL_SECOND = 20;
|
||||||
|
exports.PING_PACKET_SIZE_MIN = 1;
|
||||||
|
exports.PING_PACKET_SIZE_MAX = 65500;
|
||||||
|
exports.PING_PACKET_SIZE_DEFAULT = 56;
|
||||||
|
exports.PING_GLOBAL_TIMEOUT_MIN = 1;
|
||||||
|
exports.PING_GLOBAL_TIMEOUT_MAX = 300;
|
||||||
|
exports.PING_GLOBAL_TIMEOUT_DEFAULT = 10;
|
||||||
|
exports.PING_COUNT_MIN = 1;
|
||||||
|
exports.PING_COUNT_MAX = 100;
|
||||||
|
exports.PING_COUNT_DEFAULT = 1;
|
||||||
|
exports.PING_PER_REQUEST_TIMEOUT_MIN = 1;
|
||||||
|
exports.PING_PER_REQUEST_TIMEOUT_MAX = 60;
|
||||||
|
exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
|
||||||
exports.CONSOLE_STYLE_Reset = "\x1b[0m";
|
exports.CONSOLE_STYLE_Reset = "\x1b[0m";
|
||||||
exports.CONSOLE_STYLE_Bright = "\x1b[1m";
|
exports.CONSOLE_STYLE_Bright = "\x1b[1m";
|
||||||
exports.CONSOLE_STYLE_Dim = "\x1b[2m";
|
exports.CONSOLE_STYLE_Dim = "\x1b[2m";
|
||||||
|
@ -66,7 +73,6 @@ exports.CONSOLE_STYLE_BgMagenta = "\x1b[45m";
|
||||||
exports.CONSOLE_STYLE_BgCyan = "\x1b[46m";
|
exports.CONSOLE_STYLE_BgCyan = "\x1b[46m";
|
||||||
exports.CONSOLE_STYLE_BgWhite = "\x1b[47m";
|
exports.CONSOLE_STYLE_BgWhite = "\x1b[47m";
|
||||||
exports.CONSOLE_STYLE_BgGray = "\x1b[100m";
|
exports.CONSOLE_STYLE_BgGray = "\x1b[100m";
|
||||||
|
|
||||||
const consoleModuleColors = [
|
const consoleModuleColors = [
|
||||||
exports.CONSOLE_STYLE_FgCyan,
|
exports.CONSOLE_STYLE_FgCyan,
|
||||||
exports.CONSOLE_STYLE_FgGreen,
|
exports.CONSOLE_STYLE_FgGreen,
|
||||||
|
@ -159,11 +165,11 @@ class Logger {
|
||||||
module = module.toUpperCase();
|
module = module.toUpperCase();
|
||||||
level = level.toUpperCase();
|
level = level.toUpperCase();
|
||||||
let now;
|
let now;
|
||||||
if (dayjs_1.default.tz) {
|
if (dayjs.tz) {
|
||||||
now = dayjs_1.default.tz(new Date()).format();
|
now = dayjs.tz(new Date()).format();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
now = (0, dayjs_1.default)().format();
|
now = dayjs().format();
|
||||||
}
|
}
|
||||||
const levelColor = consoleLevelColors[level];
|
const levelColor = consoleLevelColors[level];
|
||||||
const moduleColor = consoleModuleColors[intHash(module, consoleModuleColors.length)];
|
const moduleColor = consoleModuleColors[intHash(module, consoleModuleColors.length)];
|
||||||
|
@ -264,11 +270,11 @@ function polyfill() {
|
||||||
exports.polyfill = polyfill;
|
exports.polyfill = polyfill;
|
||||||
class TimeLogger {
|
class TimeLogger {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.startTime = (0, dayjs_1.default)().valueOf();
|
this.startTime = dayjs().valueOf();
|
||||||
}
|
}
|
||||||
print(name) {
|
print(name) {
|
||||||
if (exports.isDev && process.env.TIMELOGGER === "1") {
|
if (exports.isDev && process.env.TIMELOGGER === "1") {
|
||||||
console.log(name + ": " + ((0, dayjs_1.default)().valueOf() - this.startTime) + "ms");
|
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -380,19 +386,19 @@ function parseTimeFromTimeObject(obj) {
|
||||||
}
|
}
|
||||||
exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
|
exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
|
||||||
function isoToUTCDateTime(input) {
|
function isoToUTCDateTime(input) {
|
||||||
return (0, dayjs_1.default)(input).utc().format(exports.SQL_DATETIME_FORMAT);
|
return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
|
||||||
}
|
}
|
||||||
exports.isoToUTCDateTime = isoToUTCDateTime;
|
exports.isoToUTCDateTime = isoToUTCDateTime;
|
||||||
function utcToISODateTime(input) {
|
function utcToISODateTime(input) {
|
||||||
return dayjs_1.default.utc(input).toISOString();
|
return dayjs.utc(input).toISOString();
|
||||||
}
|
}
|
||||||
exports.utcToISODateTime = utcToISODateTime;
|
exports.utcToISODateTime = utcToISODateTime;
|
||||||
function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
|
function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
|
||||||
return dayjs_1.default.utc(input).local().format(format);
|
return dayjs.utc(input).local().format(format);
|
||||||
}
|
}
|
||||||
exports.utcToLocal = utcToLocal;
|
exports.utcToLocal = utcToLocal;
|
||||||
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
|
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
|
||||||
return (0, dayjs_1.default)(input).utc().format(format);
|
return dayjs(input).utc().format(format);
|
||||||
}
|
}
|
||||||
exports.localToUTC = localToUTC;
|
exports.localToUTC = localToUTC;
|
||||||
function intHash(str, length = 10) {
|
function intHash(str, length = 10) {
|
||||||
|
@ -458,4 +464,4 @@ async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue
|
||||||
throw new Error(`Error evaluating JSON query: ${err.message}. Response from server was: ${response}`);
|
throw new Error(`Error evaluating JSON query: ${err.message}. Response from server was: ${response}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exports.evaluateJsonQuery = evaluateJsonQuery;
|
exports.evaluateJsonQuery = evaluateJsonQuery;
|
||||||
|
|
22
src/util.ts
22
src/util.ts
|
@ -9,7 +9,7 @@
|
||||||
// Frontend uses util.ts
|
// Frontend uses util.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import dayjs from "dayjs";
|
import * as dayjs from "dayjs";
|
||||||
|
|
||||||
// For loading dayjs plugins, don't remove event though it is not used in this file
|
// For loading dayjs plugins, don't remove event though it is not used in this file
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
@ -39,6 +39,26 @@ export const SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
|
||||||
export const MAX_INTERVAL_SECOND = 2073600; // 24 days
|
export const MAX_INTERVAL_SECOND = 2073600; // 24 days
|
||||||
export const MIN_INTERVAL_SECOND = 20; // 20 seconds
|
export const MIN_INTERVAL_SECOND = 20; // 20 seconds
|
||||||
|
|
||||||
|
// Packet Size limits
|
||||||
|
export const PING_PACKET_SIZE_MIN = 1;
|
||||||
|
export const PING_PACKET_SIZE_MAX = 65500;
|
||||||
|
export const PING_PACKET_SIZE_DEFAULT = 56;
|
||||||
|
|
||||||
|
// Global timeout (aka deadline) limits in seconds
|
||||||
|
export const PING_GLOBAL_TIMEOUT_MIN = 1;
|
||||||
|
export const PING_GLOBAL_TIMEOUT_MAX = 300;
|
||||||
|
export const PING_GLOBAL_TIMEOUT_DEFAULT = 10;
|
||||||
|
|
||||||
|
// Ping count limits
|
||||||
|
export const PING_COUNT_MIN = 1;
|
||||||
|
export const PING_COUNT_MAX = 100;
|
||||||
|
export const PING_COUNT_DEFAULT = 1;
|
||||||
|
|
||||||
|
// per-request timeout (aka timeout) limits in seconds
|
||||||
|
export const PING_PER_REQUEST_TIMEOUT_MIN = 1;
|
||||||
|
export const PING_PER_REQUEST_TIMEOUT_MAX = 60;
|
||||||
|
export const PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
|
||||||
|
|
||||||
// Console colors
|
// Console colors
|
||||||
// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
|
// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
|
||||||
export const CONSOLE_STYLE_Reset = "\x1b[0m";
|
export const CONSOLE_STYLE_Reset = "\x1b[0m";
|
||||||
|
|
83
test/e2e/specs/fridendly-name.spec.js
Normal file
83
test/e2e/specs/fridendly-name.spec.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
|
||||||
|
|
||||||
|
test.describe("Friendly Name Tests", () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await restoreSqliteSnapshot(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hostname", async ({ page }, testInfo) => {
|
||||||
|
// Test DNS monitor with hostname
|
||||||
|
await page.goto("./add");
|
||||||
|
await login(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
await page.getByTestId("monitor-type-select").selectOption("dns");
|
||||||
|
await page.getByTestId("hostname-input").fill("example.com");
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
await page.getByTestId("save-button").click();
|
||||||
|
await page.waitForURL("/dashboard/*");
|
||||||
|
|
||||||
|
expect(page.getByTestId("monitor-list")).toContainText("example.com");
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("URL hostname", async ({ page }, testInfo) => {
|
||||||
|
// Test HTTP monitor with URL
|
||||||
|
await page.goto("./add");
|
||||||
|
await login(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
await page.getByTestId("monitor-type-select").selectOption("http");
|
||||||
|
await page.getByTestId("url-input").fill("https://www.example.com/");
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
await page.getByTestId("save-button").click();
|
||||||
|
await page.waitForURL("/dashboard/*");
|
||||||
|
|
||||||
|
expect(page.getByTestId("monitor-list")).toContainText("www.example.com");
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("custom friendly name", async ({ page }, testInfo) => {
|
||||||
|
// Test custom friendly name for HTTP monitor
|
||||||
|
await page.goto("./add");
|
||||||
|
await login(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
await page.getByTestId("monitor-type-select").selectOption("http");
|
||||||
|
await page.getByTestId("url-input").fill("https://www.example.com/");
|
||||||
|
|
||||||
|
// Check if the friendly name placeholder is set to the hostname
|
||||||
|
const friendlyNameInput = page.getByTestId("friendly-name-input");
|
||||||
|
expect(friendlyNameInput).toHaveAttribute("placeholder", "www.example.com");
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
const customName = "Example Monitor";
|
||||||
|
await friendlyNameInput.fill(customName);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
await page.getByTestId("save-button").click();
|
||||||
|
await page.waitForURL("/dashboard/*");
|
||||||
|
|
||||||
|
expect(page.getByTestId("monitor-list")).toContainText(customName);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("default friendly name", async ({ page }, testInfo) => {
|
||||||
|
// Test default friendly name when no custom name is provided
|
||||||
|
await page.goto("./add");
|
||||||
|
await login(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
await page.getByTestId("monitor-type-select").selectOption("group");
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
await page.getByTestId("save-button").click();
|
||||||
|
await page.waitForURL("/dashboard/*");
|
||||||
|
|
||||||
|
expect(page.getByTestId("monitor-list")).toContainText("New Monitor");
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
});
|
|
@ -12,6 +12,8 @@ test.describe("Status Page", () => {
|
||||||
const monitorName = "Monitor for Status Page";
|
const monitorName = "Monitor for Status Page";
|
||||||
const tagName = "Client";
|
const tagName = "Client";
|
||||||
const tagValue = "Acme Inc";
|
const tagValue = "Acme Inc";
|
||||||
|
const monitorUrl = "https://www.example.com/status";
|
||||||
|
const monitorCustomUrl = "https://www.example.com";
|
||||||
|
|
||||||
// Status Page
|
// Status Page
|
||||||
const footerText = "This is footer text.";
|
const footerText = "This is footer text.";
|
||||||
|
@ -30,7 +32,7 @@ test.describe("Status Page", () => {
|
||||||
await expect(page.getByTestId("monitor-type-select")).toBeVisible();
|
await expect(page.getByTestId("monitor-type-select")).toBeVisible();
|
||||||
await page.getByTestId("monitor-type-select").selectOption("http");
|
await page.getByTestId("monitor-type-select").selectOption("http");
|
||||||
await page.getByTestId("friendly-name-input").fill(monitorName);
|
await page.getByTestId("friendly-name-input").fill(monitorName);
|
||||||
await page.getByTestId("url-input").fill("https://www.example.com/");
|
await page.getByTestId("url-input").fill(monitorUrl);
|
||||||
await page.getByTestId("add-tag-button").click();
|
await page.getByTestId("add-tag-button").click();
|
||||||
await page.getByTestId("tag-name-input").fill(tagName);
|
await page.getByTestId("tag-name-input").fill(tagName);
|
||||||
await page.getByTestId("tag-value-input").fill(tagValue);
|
await page.getByTestId("tag-value-input").fill(tagValue);
|
||||||
|
@ -79,6 +81,13 @@ test.describe("Status Page", () => {
|
||||||
await page.getByTestId("monitor-select").getByRole("option", { name: monitorName }).click();
|
await page.getByTestId("monitor-select").getByRole("option", { name: monitorName }).click();
|
||||||
await expect(page.getByTestId("monitor")).toHaveCount(1);
|
await expect(page.getByTestId("monitor")).toHaveCount(1);
|
||||||
await expect(page.getByTestId("monitor-name")).toContainText(monitorName);
|
await expect(page.getByTestId("monitor-name")).toContainText(monitorName);
|
||||||
|
await expect(page.getByTestId("monitor-name")).not.toHaveAttribute("href");
|
||||||
|
|
||||||
|
// Set public url on
|
||||||
|
await page.getByTestId("monitor-settings").click();
|
||||||
|
await page.getByTestId("show-clickable-link").check();
|
||||||
|
await page.getByTestId("custom-url-input").fill(monitorCustomUrl);
|
||||||
|
await page.getByTestId("monitor-settings-close").click();
|
||||||
|
|
||||||
// Save the changes
|
// Save the changes
|
||||||
await screenshot(testInfo, page);
|
await screenshot(testInfo, page);
|
||||||
|
@ -94,6 +103,8 @@ test.describe("Status Page", () => {
|
||||||
await expect(page.getByTestId("footer-text")).toContainText(footerText);
|
await expect(page.getByTestId("footer-text")).toContainText(footerText);
|
||||||
await expect(page.getByTestId("powered-by")).toHaveCount(0);
|
await expect(page.getByTestId("powered-by")).toHaveCount(0);
|
||||||
|
|
||||||
|
await expect(page.getByTestId("monitor-name")).toHaveAttribute("href", monitorCustomUrl);
|
||||||
|
|
||||||
await expect(page.getByTestId("update-countdown-text")).toContainText("00:");
|
await expect(page.getByTestId("update-countdown-text")).toContainText("00:");
|
||||||
const updateCountdown = Number((await page.getByTestId("update-countdown-text").textContent()).match(/(\d+):(\d+)/)[2]);
|
const updateCountdown = Number((await page.getByTestId("update-countdown-text").textContent()).match(/(\d+):(\d+)/)[2]);
|
||||||
expect(updateCountdown).toBeGreaterThanOrEqual(refreshInterval); // cant be certain when the timer will start, so ensure it's within expected range
|
expect(updateCountdown).toBeGreaterThanOrEqual(refreshInterval); // cant be certain when the timer will start, so ensure it's within expected range
|
||||||
|
|
9
tsconfig-backend.json
Normal file
9
tsconfig-backend.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"esModuleInterop": false
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./src/util.ts"
|
||||||
|
]
|
||||||
|
}
|
|
@ -16,6 +16,6 @@
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"./src/util.ts"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue