mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-27 16:54:04 +00:00
Merge remote-tracking branch 'origin/master' into master-weblate
# Conflicts: # src/lang/pl.json # src/lang/uk-UA.json
This commit is contained in:
commit
362a890bc3
82 changed files with 2629 additions and 299 deletions
4
.github/workflows/stale-bot.yml
vendored
4
.github/workflows/stale-bot.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
stale-issue-message: |-
|
stale-issue-message: |-
|
||||||
We are clearing up our old `help`-issues and your issue has been open for 60 days with no activity.
|
We are clearing up our old `help`-issues and your issue has been open for 60 days with no activity.
|
||||||
|
@ -21,7 +21,7 @@ jobs:
|
||||||
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request'
|
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request'
|
||||||
exempt-issue-assignees: 'louislam'
|
exempt-issue-assignees: 'louislam'
|
||||||
operations-per-run: 200
|
operations-per-run: 200
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
stale-issue-message: |-
|
stale-issue-message: |-
|
||||||
This issue was marked as `cannot-reproduce` by a maintainer.
|
This issue was marked as `cannot-reproduce` by a maintainer.
|
||||||
|
|
|
@ -127,7 +127,7 @@ Different guidelines exist for different types of pull requests (PRs):
|
||||||
- `server/monitor-types/MONITORING_TYPE.js` is the core of each monitor.
|
- `server/monitor-types/MONITORING_TYPE.js` is the core of each monitor.
|
||||||
the `async check(...)`-function should:
|
the `async check(...)`-function should:
|
||||||
- throw an error for each fault that is detected with an actionable error message
|
- throw an error for each fault that is detected with an actionable error message
|
||||||
- in the happy-path, you should set `heartbeat.msg` to a successfull message and set `heartbeat.status = UP`
|
- in the happy-path, you should set `heartbeat.msg` to a successful message and set `heartbeat.status = UP`
|
||||||
- `server/uptime-kuma-server.js` is where the monitoring backend needs to be registered.
|
- `server/uptime-kuma-server.js` is where the monitoring backend needs to be registered.
|
||||||
*If you have an idea how we can skip this step, we would love to hear about it ^^*
|
*If you have an idea how we can skip this step, we would love to hear about it ^^*
|
||||||
- `src/pages/EditMonitor.vue` is the shared frontend users interact with.
|
- `src/pages/EditMonitor.vue` is the shared frontend users interact with.
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { defineConfig, devices } from "@playwright/test";
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
const port = 30001;
|
const port = 30001;
|
||||||
const url = `http://localhost:${port}`;
|
export const url = `http://localhost:${port}`;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
// Look for test files in the "tests" directory, relative to this configuration file.
|
// Look for test files in the "tests" directory, relative to this configuration file.
|
||||||
testDir: "../test/e2e",
|
testDir: "../test/e2e/specs",
|
||||||
outputDir: "../private/playwright-test-results",
|
outputDir: "../private/playwright-test-results",
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
locale: "en-US",
|
locale: "en-US",
|
||||||
|
@ -40,9 +40,15 @@ export default defineConfig({
|
||||||
// Configure projects for major browsers.
|
// Configure projects for major browsers.
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: "chromium",
|
name: "run-once setup",
|
||||||
|
testMatch: /setup-process\.once\.js/,
|
||||||
use: { ...devices["Desktop Chrome"] },
|
use: { ...devices["Desktop Chrome"] },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "specs",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
dependencies: [ "run-once setup" ],
|
||||||
|
},
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
name: "firefox",
|
name: "firefox",
|
||||||
|
|
16
db/knex_migrations/2024-04-26-0000-snmp-monitor.js
Normal file
16
db/knex_migrations/2024-04-26-0000-snmp-monitor.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor", function (table) {
|
||||||
|
table.string("snmp_oid").defaultTo(null);
|
||||||
|
table.enum("snmp_version", [ "1", "2c", "3" ]).defaultTo("2c");
|
||||||
|
table.string("json_path_operator").defaultTo(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.alterTable("monitor", function (table) {
|
||||||
|
table.dropColumn("snmp_oid");
|
||||||
|
table.dropColumn("snmp_version");
|
||||||
|
table.dropColumn("json_path_operator");
|
||||||
|
});
|
||||||
|
};
|
13
db/knex_migrations/2024-08-24-000-add-cache-bust.js
Normal file
13
db/knex_migrations/2024-08-24-000-add-cache-bust.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor", function (table) {
|
||||||
|
table.boolean("cache_bust").notNullable().defaultTo(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor", function (table) {
|
||||||
|
table.dropColumn("cache_bust");
|
||||||
|
});
|
||||||
|
};
|
12
db/knex_migrations/2024-08-24-0000-conditions.js
Normal file
12
db/knex_migrations/2024-08-24-0000-conditions.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor", function (table) {
|
||||||
|
table.text("conditions").notNullable().defaultTo("[]");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.alterTable("monitor", function (table) {
|
||||||
|
table.dropColumn("conditions");
|
||||||
|
});
|
||||||
|
};
|
37
package-lock.json
generated
37
package-lock.json
generated
|
@ -24,7 +24,7 @@
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
"compare-versions": "~3.6.0",
|
"compare-versions": "~3.6.0",
|
||||||
"compression": "~1.7.4",
|
"compression": "~1.7.4",
|
||||||
"croner": "~6.0.5",
|
"croner": "~8.1.0",
|
||||||
"dayjs": "~1.11.5",
|
"dayjs": "~1.11.5",
|
||||||
"dev-null": "^0.1.1",
|
"dev-null": "^0.1.1",
|
||||||
"dotenv": "~16.0.3",
|
"dotenv": "~16.0.3",
|
||||||
|
@ -48,12 +48,14 @@
|
||||||
"knex": "^2.4.2",
|
"knex": "^2.4.2",
|
||||||
"limiter": "~2.1.0",
|
"limiter": "~2.1.0",
|
||||||
"liquidjs": "^10.7.0",
|
"liquidjs": "^10.7.0",
|
||||||
|
"marked": "^14.0.0",
|
||||||
"mitt": "~3.0.1",
|
"mitt": "~3.0.1",
|
||||||
"mongodb": "~4.17.1",
|
"mongodb": "~4.17.1",
|
||||||
"mqtt": "~4.3.7",
|
"mqtt": "~4.3.7",
|
||||||
"mssql": "~11.0.0",
|
"mssql": "~11.0.0",
|
||||||
"mysql2": "~3.9.6",
|
"mysql2": "~3.9.6",
|
||||||
"nanoid": "~3.3.4",
|
"nanoid": "~3.3.4",
|
||||||
|
"net-snmp": "^3.11.2",
|
||||||
"node-cloudflared-tunnel": "~1.0.9",
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
"node-radius-client": "~1.0.0",
|
"node-radius-client": "~1.0.0",
|
||||||
"nodemailer": "~6.9.13",
|
"nodemailer": "~6.9.13",
|
||||||
|
@ -112,7 +114,6 @@
|
||||||
"eslint-plugin-vue": "~8.7.1",
|
"eslint-plugin-vue": "~8.7.1",
|
||||||
"favico.js": "~0.3.10",
|
"favico.js": "~0.3.10",
|
||||||
"get-port-please": "^3.1.1",
|
"get-port-please": "^3.1.1",
|
||||||
"marked": "~4.2.5",
|
|
||||||
"node-ssh": "~13.1.0",
|
"node-ssh": "~13.1.0",
|
||||||
"postcss-html": "~1.5.0",
|
"postcss-html": "~1.5.0",
|
||||||
"postcss-rtlcss": "~3.7.2",
|
"postcss-rtlcss": "~3.7.2",
|
||||||
|
@ -5525,6 +5526,11 @@
|
||||||
"safer-buffer": "~2.1.0"
|
"safer-buffer": "~2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asn1-ber": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1-ber/-/asn1-ber-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-CbNem/7hxrjSiOAOOTX4iZxu+0m3jiLqlsERQwwPM1IDR/22M8IPpA1VVndCLw5KtjRYyRODbvAEIfuTogNDng=="
|
||||||
|
},
|
||||||
"node_modules/astral-regex": {
|
"node_modules/astral-regex": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
|
||||||
|
@ -6738,12 +6744,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/croner": {
|
"node_modules/croner": {
|
||||||
"version": "6.0.7",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/croner/-/croner-6.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/croner/-/croner-8.1.0.tgz",
|
||||||
"integrity": "sha512-k3Xx3Rcclfr60Yx4TmvsF3Yscuiql8LSvYLaphTsaq5Hk8La4Z/udmUANMOTKpgGGroI2F6/XOr9cU9OFkYluQ==",
|
"integrity": "sha512-sz990XOUPR8dG/r5BRKMBd15MYDDUu8oeSaxFD5DqvNgHSZw8Psd1s689/IGET7ezxRMiNlCIyGeY1Gvxp/MLg==",
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0"
|
"node": ">=18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cronstrue": {
|
"node_modules/cronstrue": {
|
||||||
|
@ -10640,16 +10645,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "4.2.12",
|
"version": "14.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
|
||||||
"integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==",
|
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mathml-tag-names": {
|
"node_modules/mathml-tag-names": {
|
||||||
|
@ -11285,6 +11289,15 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/net-snmp": {
|
||||||
|
"version": "3.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/net-snmp/-/net-snmp-3.11.2.tgz",
|
||||||
|
"integrity": "sha512-QKy2JQHIBsSK344dUxYRZv7tU0ANk8f8fzKD/Mmq/cCxm/cPbtiT7009QEgxdViW/gGjqGIOiLHxkCc+JhZltg==",
|
||||||
|
"dependencies": {
|
||||||
|
"asn1-ber": "^1.2.1",
|
||||||
|
"smart-buffer": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-cloudflared-tunnel": {
|
"node_modules/node-cloudflared-tunnel": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/node-cloudflared-tunnel/-/node-cloudflared-tunnel-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/node-cloudflared-tunnel/-/node-cloudflared-tunnel-1.0.10.tgz",
|
||||||
|
|
|
@ -27,9 +27,7 @@
|
||||||
"build": "vite build --config ./config/vite.config.js",
|
"build": "vite build --config ./config/vite.config.js",
|
||||||
"test": "npm run test-backend && npm run test-e2e",
|
"test": "npm run test-backend && npm run test-e2e",
|
||||||
"test-with-build": "npm run build && npm test",
|
"test-with-build": "npm run build && npm test",
|
||||||
"test-backend": "node test/backend-test-entry.js",
|
"test-backend": "cross-env TEST_BACKEND=1 node --test test/backend-test",
|
||||||
"test-backend:14": "cross-env TEST_BACKEND=1 NODE_OPTIONS=\"--experimental-abortcontroller --no-warnings\" node--test test/backend-test",
|
|
||||||
"test-backend:18": "cross-env TEST_BACKEND=1 node --test test/backend-test",
|
|
||||||
"test-e2e": "playwright test --config ./config/playwright.config.js",
|
"test-e2e": "playwright test --config ./config/playwright.config.js",
|
||||||
"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",
|
||||||
|
@ -89,7 +87,7 @@
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
"compare-versions": "~3.6.0",
|
"compare-versions": "~3.6.0",
|
||||||
"compression": "~1.7.4",
|
"compression": "~1.7.4",
|
||||||
"croner": "~6.0.5",
|
"croner": "~8.1.0",
|
||||||
"dayjs": "~1.11.5",
|
"dayjs": "~1.11.5",
|
||||||
"dev-null": "^0.1.1",
|
"dev-null": "^0.1.1",
|
||||||
"dotenv": "~16.0.3",
|
"dotenv": "~16.0.3",
|
||||||
|
@ -113,12 +111,14 @@
|
||||||
"knex": "^2.4.2",
|
"knex": "^2.4.2",
|
||||||
"limiter": "~2.1.0",
|
"limiter": "~2.1.0",
|
||||||
"liquidjs": "^10.7.0",
|
"liquidjs": "^10.7.0",
|
||||||
|
"marked": "^14.0.0",
|
||||||
"mitt": "~3.0.1",
|
"mitt": "~3.0.1",
|
||||||
"mongodb": "~4.17.1",
|
"mongodb": "~4.17.1",
|
||||||
"mqtt": "~4.3.7",
|
"mqtt": "~4.3.7",
|
||||||
"mssql": "~11.0.0",
|
"mssql": "~11.0.0",
|
||||||
"mysql2": "~3.9.6",
|
"mysql2": "~3.9.6",
|
||||||
"nanoid": "~3.3.4",
|
"nanoid": "~3.3.4",
|
||||||
|
"net-snmp": "^3.11.2",
|
||||||
"node-cloudflared-tunnel": "~1.0.9",
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
"node-radius-client": "~1.0.0",
|
"node-radius-client": "~1.0.0",
|
||||||
"nodemailer": "~6.9.13",
|
"nodemailer": "~6.9.13",
|
||||||
|
@ -177,7 +177,6 @@
|
||||||
"eslint-plugin-vue": "~8.7.1",
|
"eslint-plugin-vue": "~8.7.1",
|
||||||
"favico.js": "~0.3.10",
|
"favico.js": "~0.3.10",
|
||||||
"get-port-please": "^3.1.1",
|
"get-port-please": "^3.1.1",
|
||||||
"marked": "~4.2.5",
|
|
||||||
"node-ssh": "~13.1.0",
|
"node-ssh": "~13.1.0",
|
||||||
"postcss-html": "~1.5.0",
|
"postcss-html": "~1.5.0",
|
||||||
"postcss-rtlcss": "~3.7.2",
|
"postcss-rtlcss": "~3.7.2",
|
||||||
|
|
|
@ -213,6 +213,32 @@ async function sendRemoteBrowserList(socket) {
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send list of monitor types to client
|
||||||
|
* @param {Socket} socket Socket.io socket instance
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function sendMonitorTypeList(socket) {
|
||||||
|
const result = Object.entries(UptimeKumaServer.monitorTypeList).map(([ key, type ]) => {
|
||||||
|
return [ key, {
|
||||||
|
supportsConditions: type.supportsConditions,
|
||||||
|
conditionVariables: type.conditionVariables.map(v => {
|
||||||
|
return {
|
||||||
|
id: v.id,
|
||||||
|
operators: v.operators.map(o => {
|
||||||
|
return {
|
||||||
|
id: o.id,
|
||||||
|
caption: o.caption,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
|
||||||
|
io.to(socket.userID).emit("monitorTypeList", Object.fromEntries(result));
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendNotificationList,
|
sendNotificationList,
|
||||||
sendImportantHeartbeatList,
|
sendImportantHeartbeatList,
|
||||||
|
@ -222,4 +248,5 @@ module.exports = {
|
||||||
sendInfo,
|
sendInfo,
|
||||||
sendDockerHostList,
|
sendDockerHostList,
|
||||||
sendRemoteBrowserList,
|
sendRemoteBrowserList,
|
||||||
|
sendMonitorTypeList,
|
||||||
};
|
};
|
||||||
|
|
|
@ -239,19 +239,7 @@ class Maintenance extends BeanModel {
|
||||||
this.beanMeta.status = "under-maintenance";
|
this.beanMeta.status = "under-maintenance";
|
||||||
clearTimeout(this.beanMeta.durationTimeout);
|
clearTimeout(this.beanMeta.durationTimeout);
|
||||||
|
|
||||||
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
|
let duration = this.inferDuration(customDuration);
|
||||||
let duration;
|
|
||||||
|
|
||||||
if (customDuration > 0) {
|
|
||||||
duration = customDuration;
|
|
||||||
} else if (this.end_date) {
|
|
||||||
let d = dayjs(this.end_date).diff(dayjs(), "second");
|
|
||||||
if (d < this.duration) {
|
|
||||||
duration = d * 1000;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
duration = this.duration * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||||
|
|
||||||
|
@ -263,9 +251,21 @@ class Maintenance extends BeanModel {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create Cron
|
// Create Cron
|
||||||
|
if (this.strategy === "recurring-interval") {
|
||||||
|
// For recurring-interval, Croner needs to have interval and startAt
|
||||||
|
const startDate = dayjs(this.startDate);
|
||||||
|
const [ hour, minute ] = this.startTime.split(":");
|
||||||
|
const startDateTime = startDate.hour(hour).minute(minute);
|
||||||
|
this.beanMeta.job = new Cron(this.cron, {
|
||||||
|
timezone: await this.getTimezone(),
|
||||||
|
interval: this.interval_day * 24 * 60 * 60,
|
||||||
|
startAt: startDateTime.toISOString(),
|
||||||
|
}, startEvent);
|
||||||
|
} else {
|
||||||
this.beanMeta.job = new Cron(this.cron, {
|
this.beanMeta.job = new Cron(this.cron, {
|
||||||
timezone: await this.getTimezone(),
|
timezone: await this.getTimezone(),
|
||||||
}, startEvent);
|
}, startEvent);
|
||||||
|
}
|
||||||
|
|
||||||
// Continue if the maintenance is still in the window
|
// Continue if the maintenance is still in the window
|
||||||
let runningTimeslot = this.getRunningTimeslot();
|
let runningTimeslot = this.getRunningTimeslot();
|
||||||
|
@ -311,6 +311,24 @@ class Maintenance extends BeanModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the maintenance duration
|
||||||
|
* @param {number} customDuration - The custom duration in milliseconds.
|
||||||
|
* @returns {number} The inferred duration in milliseconds.
|
||||||
|
*/
|
||||||
|
inferDuration(customDuration) {
|
||||||
|
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
|
||||||
|
if (customDuration > 0) {
|
||||||
|
return customDuration;
|
||||||
|
} else if (this.end_date) {
|
||||||
|
let d = dayjs(this.end_date).diff(dayjs(), "second");
|
||||||
|
if (d < this.duration) {
|
||||||
|
return d * 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.duration * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the maintenance
|
* Stop the maintenance
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
|
@ -395,10 +413,8 @@ class Maintenance extends BeanModel {
|
||||||
} else if (!this.strategy.startsWith("recurring-")) {
|
} else if (!this.strategy.startsWith("recurring-")) {
|
||||||
this.cron = "";
|
this.cron = "";
|
||||||
} else if (this.strategy === "recurring-interval") {
|
} else if (this.strategy === "recurring-interval") {
|
||||||
let array = this.start_time.split(":");
|
// For intervals, the pattern is calculated in the run function as the interval-option is set
|
||||||
let hour = parseInt(array[0]);
|
this.cron = "* * * * *";
|
||||||
let minute = parseInt(array[1]);
|
|
||||||
this.cron = minute + " " + hour + " */" + this.interval_day + " * *";
|
|
||||||
this.duration = this.calcDuration();
|
this.duration = this.calcDuration();
|
||||||
log.debug("maintenance", "Cron: " + this.cron);
|
log.debug("maintenance", "Cron: " + this.cron);
|
||||||
log.debug("maintenance", "Duration: " + this.duration);
|
log.debug("maintenance", "Duration: " + this.duration);
|
||||||
|
|
|
@ -2,7 +2,7 @@ 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
|
SQL_DATETIME_FORMAT, evaluateJsonQuery
|
||||||
} = 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
|
||||||
|
@ -17,7 +17,6 @@ const apicache = require("../modules/apicache");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const { DockerHost } = require("../docker");
|
const { DockerHost } = require("../docker");
|
||||||
const Gamedig = require("gamedig");
|
const Gamedig = require("gamedig");
|
||||||
const jsonata = require("jsonata");
|
|
||||||
const jwt = require("jsonwebtoken");
|
const jwt = require("jsonwebtoken");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const { UptimeCalculator } = require("../uptime-calculator");
|
const { UptimeCalculator } = require("../uptime-calculator");
|
||||||
|
@ -160,7 +159,12 @@ class Monitor extends BeanModel {
|
||||||
kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(),
|
kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(),
|
||||||
kafkaProducerMessage: this.kafkaProducerMessage,
|
kafkaProducerMessage: this.kafkaProducerMessage,
|
||||||
screenshot,
|
screenshot,
|
||||||
|
cacheBust: this.getCacheBust(),
|
||||||
remote_browser: this.remote_browser,
|
remote_browser: this.remote_browser,
|
||||||
|
snmpOid: this.snmpOid,
|
||||||
|
jsonPathOperator: this.jsonPathOperator,
|
||||||
|
snmpVersion: this.snmpVersion,
|
||||||
|
conditions: JSON.parse(this.conditions),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeSensitiveData) {
|
if (includeSensitiveData) {
|
||||||
|
@ -293,6 +297,14 @@ class Monitor extends BeanModel {
|
||||||
return Boolean(this.grpcEnableTls);
|
return Boolean(this.grpcEnableTls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean} if cachebusting is enabled
|
||||||
|
*/
|
||||||
|
getCacheBust() {
|
||||||
|
return Boolean(this.cacheBust);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get accepted status codes
|
* Get accepted status codes
|
||||||
* @returns {object} Accepted status codes
|
* @returns {object} Accepted status codes
|
||||||
|
@ -334,7 +346,7 @@ class Monitor extends BeanModel {
|
||||||
let previousBeat = null;
|
let previousBeat = null;
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
|
|
||||||
this.prometheus = new Prometheus(this);
|
this.prometheus = await Prometheus.createAndInitMetrics(this);
|
||||||
|
|
||||||
const beat = async () => {
|
const beat = async () => {
|
||||||
|
|
||||||
|
@ -498,6 +510,14 @@ class Monitor extends BeanModel {
|
||||||
options.data = bodyValue;
|
options.data = bodyValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.cacheBust) {
|
||||||
|
const randomFloatString = Math.random().toString(36);
|
||||||
|
const cacheBust = randomFloatString.substring(2);
|
||||||
|
options.params = {
|
||||||
|
uptime_kuma_cachebuster: cacheBust,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (this.proxy_id) {
|
if (this.proxy_id) {
|
||||||
const proxy = await R.load("proxy", this.proxy_id);
|
const proxy = await R.load("proxy", this.proxy_id);
|
||||||
|
|
||||||
|
@ -598,25 +618,15 @@ class Monitor extends BeanModel {
|
||||||
} else if (this.type === "json-query") {
|
} else if (this.type === "json-query") {
|
||||||
let data = res.data;
|
let data = res.data;
|
||||||
|
|
||||||
// convert data to object
|
const { status, response } = await evaluateJsonQuery(data, this.jsonPath, this.jsonPathOperator, this.expectedValue);
|
||||||
if (typeof data === "string" && res.headers["content-type"] !== "application/json") {
|
|
||||||
try {
|
|
||||||
data = JSON.parse(data);
|
|
||||||
} catch (_) {
|
|
||||||
// Failed to parse as JSON, just process it as a string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let expression = jsonata(this.jsonPath);
|
if (status) {
|
||||||
|
|
||||||
let result = await expression.evaluate(data);
|
|
||||||
|
|
||||||
if (result.toString() === this.expectedValue) {
|
|
||||||
bean.msg += ", expected value is found";
|
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
|
bean.msg = `JSON query passes (comparing ${response} ${this.jsonPathOperator} ${this.expectedValue})`;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
|
throw new Error(`JSON query does not pass (comparing ${response} ${this.jsonPathOperator} ${this.expectedValue})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.type === "port") {
|
} else if (this.type === "port") {
|
||||||
|
@ -988,7 +998,7 @@ class Monitor extends BeanModel {
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] prometheus.update`);
|
log.debug("monitor", `[${this.name}] prometheus.update`);
|
||||||
this.prometheus?.update(bean, tlsInfo);
|
await this.prometheus?.update(bean, tlsInfo);
|
||||||
|
|
||||||
previousBeat = bean;
|
previousBeat = bean;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ const cheerio = require("cheerio");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const jsesc = require("jsesc");
|
const jsesc = require("jsesc");
|
||||||
const googleAnalytics = require("../google-analytics");
|
const googleAnalytics = require("../google-analytics");
|
||||||
|
const { marked } = require("marked");
|
||||||
|
|
||||||
class StatusPage extends BeanModel {
|
class StatusPage extends BeanModel {
|
||||||
|
|
||||||
|
@ -46,7 +47,11 @@ class StatusPage extends BeanModel {
|
||||||
*/
|
*/
|
||||||
static async renderHTML(indexHTML, statusPage) {
|
static async renderHTML(indexHTML, statusPage) {
|
||||||
const $ = cheerio.load(indexHTML);
|
const $ = cheerio.load(indexHTML);
|
||||||
const description155 = statusPage.description?.substring(0, 155) ?? "";
|
|
||||||
|
const description155 = marked(statusPage.description ?? "")
|
||||||
|
.replace(/<[^>]+>/gm, "")
|
||||||
|
.trim()
|
||||||
|
.substring(0, 155);
|
||||||
|
|
||||||
$("title").text(statusPage.title);
|
$("title").text(statusPage.title);
|
||||||
$("meta[name=description]").attr("content", description155);
|
$("meta[name=description]").attr("content", description155);
|
||||||
|
|
71
server/monitor-conditions/evaluator.js
Normal file
71
server/monitor-conditions/evaluator.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("./expression");
|
||||||
|
const { operatorMap } = require("./operators");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ConditionExpression} expression Expression to evaluate
|
||||||
|
* @param {object} context Context to evaluate against; These are values for variables in the expression
|
||||||
|
* @returns {boolean} Whether the expression evaluates true or false
|
||||||
|
* @throws {Error}
|
||||||
|
*/
|
||||||
|
function evaluateExpression(expression, context) {
|
||||||
|
/**
|
||||||
|
* @type {import("./operators").ConditionOperator|null}
|
||||||
|
*/
|
||||||
|
const operator = operatorMap.get(expression.operator) || null;
|
||||||
|
if (operator === null) {
|
||||||
|
throw new Error("Unexpected expression operator ID '" + expression.operator + "'. Expected one of [" + operatorMap.keys().join(",") + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(context, expression.variable)) {
|
||||||
|
throw new Error("Variable missing in context: " + expression.variable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return operator.test(context[expression.variable], expression.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ConditionExpressionGroup} group Group of expressions to evaluate
|
||||||
|
* @param {object} context Context to evaluate against; These are values for variables in the expression
|
||||||
|
* @returns {boolean} Whether the group evaluates true or false
|
||||||
|
* @throws {Error}
|
||||||
|
*/
|
||||||
|
function evaluateExpressionGroup(group, context) {
|
||||||
|
if (!group.children.length) {
|
||||||
|
throw new Error("ConditionExpressionGroup must contain at least one child.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
for (const child of group.children) {
|
||||||
|
let childResult;
|
||||||
|
|
||||||
|
if (child instanceof ConditionExpression) {
|
||||||
|
childResult = evaluateExpression(child, context);
|
||||||
|
} else if (child instanceof ConditionExpressionGroup) {
|
||||||
|
childResult = evaluateExpressionGroup(child, context);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid child type in ConditionExpressionGroup. Expected ConditionExpression or ConditionExpressionGroup");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
result = childResult; // Initialize result with the first child's result
|
||||||
|
} else if (child.andOr === LOGICAL.OR) {
|
||||||
|
result = result || childResult;
|
||||||
|
} else if (child.andOr === LOGICAL.AND) {
|
||||||
|
result = result && childResult;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid logical operator in child of ConditionExpressionGroup. Expected 'and' or 'or'. Got '" + group.andOr + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
throw new Error("ConditionExpressionGroup did not result in a boolean.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
evaluateExpression,
|
||||||
|
evaluateExpressionGroup,
|
||||||
|
};
|
111
server/monitor-conditions/expression.js
Normal file
111
server/monitor-conditions/expression.js
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
const LOGICAL = {
|
||||||
|
AND: "and",
|
||||||
|
OR: "or",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively processes an array of raw condition objects and populates the given parent group with
|
||||||
|
* corresponding ConditionExpression or ConditionExpressionGroup instances.
|
||||||
|
* @param {Array} conditions Array of raw condition objects, where each object represents either a group or an expression.
|
||||||
|
* @param {ConditionExpressionGroup} parentGroup The parent group to which the instantiated ConditionExpression or ConditionExpressionGroup objects will be added.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function processMonitorConditions(conditions, parentGroup) {
|
||||||
|
conditions.forEach(condition => {
|
||||||
|
const andOr = condition.andOr === LOGICAL.OR ? LOGICAL.OR : LOGICAL.AND;
|
||||||
|
|
||||||
|
if (condition.type === "group") {
|
||||||
|
const group = new ConditionExpressionGroup([], andOr);
|
||||||
|
|
||||||
|
// Recursively process the group's children
|
||||||
|
processMonitorConditions(condition.children, group);
|
||||||
|
|
||||||
|
parentGroup.children.push(group);
|
||||||
|
} else if (condition.type === "expression") {
|
||||||
|
const expression = new ConditionExpression(condition.variable, condition.operator, condition.value, andOr);
|
||||||
|
parentGroup.children.push(expression);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConditionExpressionGroup {
|
||||||
|
/**
|
||||||
|
* @type {ConditionExpressionGroup[]|ConditionExpression[]} Groups and/or expressions to test
|
||||||
|
*/
|
||||||
|
children = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {LOGICAL} Connects group result with previous group/expression results
|
||||||
|
*/
|
||||||
|
andOr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ConditionExpressionGroup[]|ConditionExpression[]} children Groups and/or expressions to test
|
||||||
|
* @param {LOGICAL} andOr Connects group result with previous group/expression results
|
||||||
|
*/
|
||||||
|
constructor(children = [], andOr = LOGICAL.AND) {
|
||||||
|
this.children = children;
|
||||||
|
this.andOr = andOr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Monitor} monitor Monitor instance
|
||||||
|
* @returns {ConditionExpressionGroup|null} A ConditionExpressionGroup with the Monitor's conditions
|
||||||
|
*/
|
||||||
|
static fromMonitor(monitor) {
|
||||||
|
const conditions = JSON.parse(monitor.conditions);
|
||||||
|
if (conditions.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = new ConditionExpressionGroup();
|
||||||
|
processMonitorConditions(conditions, root);
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConditionExpression {
|
||||||
|
/**
|
||||||
|
* @type {string} ID of variable
|
||||||
|
*/
|
||||||
|
variable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string} ID of operator
|
||||||
|
*/
|
||||||
|
operator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string} Value to test with the operator
|
||||||
|
*/
|
||||||
|
value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {LOGICAL} Connects expression result with previous group/expression results
|
||||||
|
*/
|
||||||
|
andOr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} variable ID of variable to test against
|
||||||
|
* @param {string} operator ID of operator to test the variable with
|
||||||
|
* @param {string} value Value to test with the operator
|
||||||
|
* @param {LOGICAL} andOr Connects expression result with previous group/expression results
|
||||||
|
*/
|
||||||
|
constructor(variable, operator, value, andOr = LOGICAL.AND) {
|
||||||
|
this.variable = variable;
|
||||||
|
this.operator = operator;
|
||||||
|
this.value = value;
|
||||||
|
this.andOr = andOr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
LOGICAL,
|
||||||
|
ConditionExpressionGroup,
|
||||||
|
ConditionExpression,
|
||||||
|
};
|
318
server/monitor-conditions/operators.js
Normal file
318
server/monitor-conditions/operators.js
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
class ConditionOperator {
|
||||||
|
id = undefined;
|
||||||
|
caption = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {mixed} variable
|
||||||
|
* @type {mixed} value
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
throw new Error("You need to override test()");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const OP_STR_EQUALS = "equals";
|
||||||
|
|
||||||
|
const OP_STR_NOT_EQUALS = "not_equals";
|
||||||
|
|
||||||
|
const OP_CONTAINS = "contains";
|
||||||
|
|
||||||
|
const OP_NOT_CONTAINS = "not_contains";
|
||||||
|
|
||||||
|
const OP_STARTS_WITH = "starts_with";
|
||||||
|
|
||||||
|
const OP_NOT_STARTS_WITH = "not_starts_with";
|
||||||
|
|
||||||
|
const OP_ENDS_WITH = "ends_with";
|
||||||
|
|
||||||
|
const OP_NOT_ENDS_WITH = "not_ends_with";
|
||||||
|
|
||||||
|
const OP_NUM_EQUALS = "num_equals";
|
||||||
|
|
||||||
|
const OP_NUM_NOT_EQUALS = "num_not_equals";
|
||||||
|
|
||||||
|
const OP_LT = "lt";
|
||||||
|
|
||||||
|
const OP_GT = "gt";
|
||||||
|
|
||||||
|
const OP_LTE = "lte";
|
||||||
|
|
||||||
|
const OP_GTE = "gte";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable is equal to a value.
|
||||||
|
*/
|
||||||
|
class StringEqualsOperator extends ConditionOperator {
|
||||||
|
id = OP_STR_EQUALS;
|
||||||
|
caption = "equals";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable === value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable is not equal to a value.
|
||||||
|
*/
|
||||||
|
class StringNotEqualsOperator extends ConditionOperator {
|
||||||
|
id = OP_STR_NOT_EQUALS;
|
||||||
|
caption = "not equals";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable !== value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable contains a value.
|
||||||
|
* Handles both Array and String variable types.
|
||||||
|
*/
|
||||||
|
class ContainsOperator extends ConditionOperator {
|
||||||
|
id = OP_CONTAINS;
|
||||||
|
caption = "contains";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
if (Array.isArray(variable)) {
|
||||||
|
return variable.includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return variable.indexOf(value) !== -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable does not contain a value.
|
||||||
|
* Handles both Array and String variable types.
|
||||||
|
*/
|
||||||
|
class NotContainsOperator extends ConditionOperator {
|
||||||
|
id = OP_NOT_CONTAINS;
|
||||||
|
caption = "not contains";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
if (Array.isArray(variable)) {
|
||||||
|
return !variable.includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return variable.indexOf(value) === -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable starts with a value.
|
||||||
|
*/
|
||||||
|
class StartsWithOperator extends ConditionOperator {
|
||||||
|
id = OP_STARTS_WITH;
|
||||||
|
caption = "starts with";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable.startsWith(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable does not start with a value.
|
||||||
|
*/
|
||||||
|
class NotStartsWithOperator extends ConditionOperator {
|
||||||
|
id = OP_NOT_STARTS_WITH;
|
||||||
|
caption = "not starts with";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return !variable.startsWith(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable ends with a value.
|
||||||
|
*/
|
||||||
|
class EndsWithOperator extends ConditionOperator {
|
||||||
|
id = OP_ENDS_WITH;
|
||||||
|
caption = "ends with";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable.endsWith(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable does not end with a value.
|
||||||
|
*/
|
||||||
|
class NotEndsWithOperator extends ConditionOperator {
|
||||||
|
id = OP_NOT_ENDS_WITH;
|
||||||
|
caption = "not ends with";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return !variable.endsWith(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a numeric variable is equal to a value.
|
||||||
|
*/
|
||||||
|
class NumberEqualsOperator extends ConditionOperator {
|
||||||
|
id = OP_NUM_EQUALS;
|
||||||
|
caption = "equals";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable === Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a numeric variable is not equal to a value.
|
||||||
|
*/
|
||||||
|
class NumberNotEqualsOperator extends ConditionOperator {
|
||||||
|
id = OP_NUM_NOT_EQUALS;
|
||||||
|
caption = "not equals";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable !== Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable is less than a value.
|
||||||
|
*/
|
||||||
|
class LessThanOperator extends ConditionOperator {
|
||||||
|
id = OP_LT;
|
||||||
|
caption = "less than";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable < Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable is greater than a value.
|
||||||
|
*/
|
||||||
|
class GreaterThanOperator extends ConditionOperator {
|
||||||
|
id = OP_GT;
|
||||||
|
caption = "greater than";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable > Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable is less than or equal to a value.
|
||||||
|
*/
|
||||||
|
class LessThanOrEqualToOperator extends ConditionOperator {
|
||||||
|
id = OP_LTE;
|
||||||
|
caption = "less than or equal to";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable <= Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable is greater than or equal to a value.
|
||||||
|
*/
|
||||||
|
class GreaterThanOrEqualToOperator extends ConditionOperator {
|
||||||
|
id = OP_GTE;
|
||||||
|
caption = "greater than or equal to";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable >= Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const operatorMap = new Map([
|
||||||
|
[ OP_STR_EQUALS, new StringEqualsOperator ],
|
||||||
|
[ OP_STR_NOT_EQUALS, new StringNotEqualsOperator ],
|
||||||
|
[ OP_CONTAINS, new ContainsOperator ],
|
||||||
|
[ OP_NOT_CONTAINS, new NotContainsOperator ],
|
||||||
|
[ OP_STARTS_WITH, new StartsWithOperator ],
|
||||||
|
[ OP_NOT_STARTS_WITH, new NotStartsWithOperator ],
|
||||||
|
[ OP_ENDS_WITH, new EndsWithOperator ],
|
||||||
|
[ OP_NOT_ENDS_WITH, new NotEndsWithOperator ],
|
||||||
|
[ OP_NUM_EQUALS, new NumberEqualsOperator ],
|
||||||
|
[ OP_NUM_NOT_EQUALS, new NumberNotEqualsOperator ],
|
||||||
|
[ OP_LT, new LessThanOperator ],
|
||||||
|
[ OP_GT, new GreaterThanOperator ],
|
||||||
|
[ OP_LTE, new LessThanOrEqualToOperator ],
|
||||||
|
[ OP_GTE, new GreaterThanOrEqualToOperator ],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const defaultStringOperators = [
|
||||||
|
operatorMap.get(OP_STR_EQUALS),
|
||||||
|
operatorMap.get(OP_STR_NOT_EQUALS),
|
||||||
|
operatorMap.get(OP_CONTAINS),
|
||||||
|
operatorMap.get(OP_NOT_CONTAINS),
|
||||||
|
operatorMap.get(OP_STARTS_WITH),
|
||||||
|
operatorMap.get(OP_NOT_STARTS_WITH),
|
||||||
|
operatorMap.get(OP_ENDS_WITH),
|
||||||
|
operatorMap.get(OP_NOT_ENDS_WITH)
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultNumberOperators = [
|
||||||
|
operatorMap.get(OP_NUM_EQUALS),
|
||||||
|
operatorMap.get(OP_NUM_NOT_EQUALS),
|
||||||
|
operatorMap.get(OP_LT),
|
||||||
|
operatorMap.get(OP_GT),
|
||||||
|
operatorMap.get(OP_LTE),
|
||||||
|
operatorMap.get(OP_GTE)
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
OP_STR_EQUALS,
|
||||||
|
OP_STR_NOT_EQUALS,
|
||||||
|
OP_CONTAINS,
|
||||||
|
OP_NOT_CONTAINS,
|
||||||
|
OP_STARTS_WITH,
|
||||||
|
OP_NOT_STARTS_WITH,
|
||||||
|
OP_ENDS_WITH,
|
||||||
|
OP_NOT_ENDS_WITH,
|
||||||
|
OP_NUM_EQUALS,
|
||||||
|
OP_NUM_NOT_EQUALS,
|
||||||
|
OP_LT,
|
||||||
|
OP_GT,
|
||||||
|
OP_LTE,
|
||||||
|
OP_GTE,
|
||||||
|
operatorMap,
|
||||||
|
defaultStringOperators,
|
||||||
|
defaultNumberOperators,
|
||||||
|
ConditionOperator,
|
||||||
|
};
|
31
server/monitor-conditions/variables.js
Normal file
31
server/monitor-conditions/variables.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* Represents a variable used in a condition and the set of operators that can be applied to this variable.
|
||||||
|
*
|
||||||
|
* A `ConditionVariable` holds the ID of the variable and a list of operators that define how this variable can be evaluated
|
||||||
|
* in conditions. For example, if the variable is a request body or a specific field in a request, the operators can include
|
||||||
|
* operations such as equality checks, comparisons, or other custom evaluations.
|
||||||
|
*/
|
||||||
|
class ConditionVariable {
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("./operators").ConditionOperator[]}
|
||||||
|
*/
|
||||||
|
operators = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id ID of variable
|
||||||
|
* @param {import("./operators").ConditionOperator[]} operators Operators the condition supports
|
||||||
|
*/
|
||||||
|
constructor(id, operators = []) {
|
||||||
|
this.id = id;
|
||||||
|
this.operators = operators;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ConditionVariable,
|
||||||
|
};
|
|
@ -1,12 +1,22 @@
|
||||||
const { MonitorType } = require("./monitor-type");
|
const { MonitorType } = require("./monitor-type");
|
||||||
const { UP } = require("../../src/util");
|
const { UP, DOWN } = require("../../src/util");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { dnsResolve } = require("../util-server");
|
const { dnsResolve } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
|
const { ConditionVariable } = require("../monitor-conditions/variables");
|
||||||
|
const { defaultStringOperators } = require("../monitor-conditions/operators");
|
||||||
|
const { ConditionExpressionGroup } = require("../monitor-conditions/expression");
|
||||||
|
const { evaluateExpressionGroup } = require("../monitor-conditions/evaluator");
|
||||||
|
|
||||||
class DnsMonitorType extends MonitorType {
|
class DnsMonitorType extends MonitorType {
|
||||||
name = "dns";
|
name = "dns";
|
||||||
|
|
||||||
|
supportsConditions = true;
|
||||||
|
|
||||||
|
conditionVariables = [
|
||||||
|
new ConditionVariable("record", defaultStringOperators ),
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
@ -17,28 +27,48 @@ class DnsMonitorType extends MonitorType {
|
||||||
let dnsRes = await dnsResolve(monitor.hostname, monitor.dns_resolve_server, monitor.port, monitor.dns_resolve_type);
|
let dnsRes = await dnsResolve(monitor.hostname, monitor.dns_resolve_server, monitor.port, monitor.dns_resolve_type);
|
||||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
if (monitor.dns_resolve_type === "A" || monitor.dns_resolve_type === "AAAA" || monitor.dns_resolve_type === "TXT" || monitor.dns_resolve_type === "PTR") {
|
const conditions = ConditionExpressionGroup.fromMonitor(monitor);
|
||||||
dnsMessage += "Records: ";
|
let conditionsResult = true;
|
||||||
dnsMessage += dnsRes.join(" | ");
|
const handleConditions = (data) => conditions ? evaluateExpressionGroup(conditions, data) : true;
|
||||||
} else if (monitor.dns_resolve_type === "CNAME" || monitor.dns_resolve_type === "PTR") {
|
|
||||||
dnsMessage += dnsRes[0];
|
switch (monitor.dns_resolve_type) {
|
||||||
} else if (monitor.dns_resolve_type === "CAA") {
|
case "A":
|
||||||
dnsMessage += dnsRes[0].issue;
|
case "AAAA":
|
||||||
} else if (monitor.dns_resolve_type === "MX") {
|
case "TXT":
|
||||||
dnsRes.forEach(record => {
|
case "PTR":
|
||||||
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
dnsMessage = `Records: ${dnsRes.join(" | ")}`;
|
||||||
});
|
conditionsResult = dnsRes.some(record => handleConditions({ record }));
|
||||||
dnsMessage = dnsMessage.slice(0, -2);
|
break;
|
||||||
} else if (monitor.dns_resolve_type === "NS") {
|
|
||||||
dnsMessage += "Servers: ";
|
case "CNAME":
|
||||||
dnsMessage += dnsRes.join(" | ");
|
dnsMessage = dnsRes[0];
|
||||||
} else if (monitor.dns_resolve_type === "SOA") {
|
conditionsResult = handleConditions({ record: dnsRes[0] });
|
||||||
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
|
break;
|
||||||
} else if (monitor.dns_resolve_type === "SRV") {
|
|
||||||
dnsRes.forEach(record => {
|
case "CAA":
|
||||||
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
dnsMessage = dnsRes[0].issue;
|
||||||
});
|
conditionsResult = handleConditions({ record: dnsRes[0].issue });
|
||||||
dnsMessage = dnsMessage.slice(0, -2);
|
break;
|
||||||
|
|
||||||
|
case "MX":
|
||||||
|
dnsMessage = dnsRes.map(record => `Hostname: ${record.exchange} - Priority: ${record.priority}`).join(" | ");
|
||||||
|
conditionsResult = dnsRes.some(record => handleConditions({ record: record.exchange }));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "NS":
|
||||||
|
dnsMessage = `Servers: ${dnsRes.join(" | ")}`;
|
||||||
|
conditionsResult = dnsRes.some(record => handleConditions({ record }));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "SOA":
|
||||||
|
dnsMessage = `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
|
||||||
|
conditionsResult = handleConditions({ record: dnsRes.nsname });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "SRV":
|
||||||
|
dnsMessage = dnsRes.map(record => `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight}`).join(" | ");
|
||||||
|
conditionsResult = dnsRes.some(record => handleConditions({ record: record.name }));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (monitor.dns_last_result !== dnsMessage && dnsMessage !== undefined) {
|
if (monitor.dns_last_result !== dnsMessage && dnsMessage !== undefined) {
|
||||||
|
@ -46,7 +76,7 @@ class DnsMonitorType extends MonitorType {
|
||||||
}
|
}
|
||||||
|
|
||||||
heartbeat.msg = dnsMessage;
|
heartbeat.msg = dnsMessage;
|
||||||
heartbeat.status = UP;
|
heartbeat.status = conditionsResult ? UP : DOWN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,19 @@
|
||||||
class MonitorType {
|
class MonitorType {
|
||||||
name = undefined;
|
name = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not this type supports monitor conditions. Controls UI visibility in monitor form.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
supportsConditions = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variables supported by this type. e.g. an HTTP type could have a "response_code" variable to test against.
|
||||||
|
* This property controls the choices displayed in the monitor edit form.
|
||||||
|
* @type {import("../monitor-conditions/variables").ConditionVariable[]}
|
||||||
|
*/
|
||||||
|
conditionVariables = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the monitoring check on the given monitor
|
* Run the monitoring check on the given monitor
|
||||||
* @param {Monitor} monitor Monitor to check
|
* @param {Monitor} monitor Monitor to check
|
||||||
|
|
63
server/monitor-types/snmp.js
Normal file
63
server/monitor-types/snmp.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
const { MonitorType } = require("./monitor-type");
|
||||||
|
const { UP, log, evaluateJsonQuery } = require("../../src/util");
|
||||||
|
const snmp = require("net-snmp");
|
||||||
|
|
||||||
|
class SNMPMonitorType extends MonitorType {
|
||||||
|
name = "snmp";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async check(monitor, heartbeat, _server) {
|
||||||
|
let session;
|
||||||
|
try {
|
||||||
|
const sessionOptions = {
|
||||||
|
port: monitor.port || "161",
|
||||||
|
retries: monitor.maxretries,
|
||||||
|
timeout: monitor.timeout * 1000,
|
||||||
|
version: snmp.Version[monitor.snmpVersion],
|
||||||
|
};
|
||||||
|
session = snmp.createSession(monitor.hostname, monitor.radiusPassword, sessionOptions);
|
||||||
|
|
||||||
|
// Handle errors during session creation
|
||||||
|
session.on("error", (error) => {
|
||||||
|
throw new Error(`Error creating SNMP session: ${error.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const varbinds = await new Promise((resolve, reject) => {
|
||||||
|
session.get([ monitor.snmpOid ], (error, varbinds) => {
|
||||||
|
error ? reject(error) : resolve(varbinds);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
log.debug("monitor", `SNMP: Received varbinds (Type: ${snmp.ObjectType[varbinds[0].type]} Value: ${varbinds[0].value})`);
|
||||||
|
|
||||||
|
if (varbinds.length === 0) {
|
||||||
|
throw new Error(`No varbinds returned from SNMP session (OID: ${monitor.snmpOid})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (varbinds[0].type === snmp.ObjectType.NoSuchInstance) {
|
||||||
|
throw new Error(`The SNMP query returned that no instance exists for OID ${monitor.snmpOid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We restrict querying to one OID per monitor, therefore `varbinds[0]` will always contain the value we're interested in.
|
||||||
|
const value = varbinds[0].value;
|
||||||
|
|
||||||
|
const { status, response } = await evaluateJsonQuery(value, monitor.jsonPath, monitor.jsonPathOperator, monitor.expectedValue);
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
heartbeat.status = UP;
|
||||||
|
heartbeat.msg = `JSON query passes (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`;
|
||||||
|
} else {
|
||||||
|
throw new Error(`JSON query does not pass (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (session) {
|
||||||
|
session.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
SNMPMonitorType,
|
||||||
|
};
|
|
@ -1,4 +1,3 @@
|
||||||
const { log } = require("../../src/util");
|
|
||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const {
|
const {
|
||||||
relayInit,
|
relayInit,
|
||||||
|
@ -12,16 +11,7 @@ const {
|
||||||
// polyfills for node versions
|
// polyfills for node versions
|
||||||
const semver = require("semver");
|
const semver = require("semver");
|
||||||
const nodeVersion = process.version;
|
const nodeVersion = process.version;
|
||||||
if (semver.lt(nodeVersion, "16.0.0")) {
|
if (semver.lt(nodeVersion, "20.0.0")) {
|
||||||
log.warn("monitor", "Node <= 16 is unsupported for nostr, sorry :(");
|
|
||||||
} else if (semver.lt(nodeVersion, "18.0.0")) {
|
|
||||||
// polyfills for node 16
|
|
||||||
global.crypto = require("crypto");
|
|
||||||
global.WebSocket = require("isomorphic-ws");
|
|
||||||
if (typeof crypto !== "undefined" && !crypto.subtle && crypto.webcrypto) {
|
|
||||||
crypto.subtle = crypto.webcrypto.subtle;
|
|
||||||
}
|
|
||||||
} else if (semver.lt(nodeVersion, "20.0.0")) {
|
|
||||||
// polyfills for node 18
|
// polyfills for node 18
|
||||||
global.crypto = require("crypto");
|
global.crypto = require("crypto");
|
||||||
global.WebSocket = require("isomorphic-ws");
|
global.WebSocket = require("isomorphic-ws");
|
||||||
|
|
47
server/notification-providers/onesender.js
Normal file
47
server/notification-providers/onesender.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class Onesender extends NotificationProvider {
|
||||||
|
name = "Onesender";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
const okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data = {
|
||||||
|
heartbeat: heartbeatJSON,
|
||||||
|
monitor: monitorJSON,
|
||||||
|
msg,
|
||||||
|
to: notification.onesenderReceiver,
|
||||||
|
type: "text",
|
||||||
|
recipient_type: "individual",
|
||||||
|
text: {
|
||||||
|
body: msg
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (notification.onesenderTypeReceiver === "private") {
|
||||||
|
data.to = notification.onesenderReceiver + "@s.whatsapp.net";
|
||||||
|
} else {
|
||||||
|
data.recipient_type = "group";
|
||||||
|
data.to = notification.onesenderReceiver + "@g.us";
|
||||||
|
}
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bearer " + notification.onesenderToken,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await axios.post(notification.onesenderURL, data, config);
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Onesender;
|
|
@ -1,3 +1,6 @@
|
||||||
|
const { getMonitorRelativeURL } = require("../../src/util");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
|
||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
|
|
||||||
|
@ -23,6 +26,12 @@ class Pushover extends NotificationProvider {
|
||||||
"html": 1,
|
"html": 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL && monitorJSON) {
|
||||||
|
data["url"] = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
||||||
|
data["url_title"] = "Link to Monitor";
|
||||||
|
}
|
||||||
|
|
||||||
if (notification.pushoverdevice) {
|
if (notification.pushoverdevice) {
|
||||||
data.device = notification.pushoverdevice;
|
data.device = notification.pushoverdevice;
|
||||||
}
|
}
|
||||||
|
|
52
server/notification-providers/signl4.js
Normal file
52
server/notification-providers/signl4.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { UP, DOWN } = require("../../src/util");
|
||||||
|
|
||||||
|
class SIGNL4 extends NotificationProvider {
|
||||||
|
name = "SIGNL4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
const okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data = {
|
||||||
|
heartbeat: heartbeatJSON,
|
||||||
|
monitor: monitorJSON,
|
||||||
|
msg,
|
||||||
|
// Source system
|
||||||
|
"X-S4-SourceSystem": "UptimeKuma",
|
||||||
|
monitorUrl: this.extractAdress(monitorJSON),
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
// Test alert
|
||||||
|
data.title = "Uptime Kuma Alert";
|
||||||
|
data.message = msg;
|
||||||
|
} else if (heartbeatJSON.status === UP) {
|
||||||
|
data.title = "Uptime Kuma Monitor ✅ Up";
|
||||||
|
data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID;
|
||||||
|
data["X-S4-Status"] = "resolved";
|
||||||
|
} else if (heartbeatJSON.status === DOWN) {
|
||||||
|
data.title = "Uptime Kuma Monitor 🔴 Down";
|
||||||
|
data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID;
|
||||||
|
data["X-S4-Status"] = "new";
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.post(notification.webhookURL, data, config);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SIGNL4;
|
51
server/notification-providers/wpush.js
Normal file
51
server/notification-providers/wpush.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class WPush extends NotificationProvider {
|
||||||
|
name = "WPush";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
const okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const context = {
|
||||||
|
"title": this.checkStatus(heartbeatJSON, monitorJSON),
|
||||||
|
"content": msg,
|
||||||
|
"apikey": notification.wpushAPIkey,
|
||||||
|
"channel": notification.wpushChannel
|
||||||
|
};
|
||||||
|
const result = await axios.post("https://api.wpush.cn/api/v1/send", context);
|
||||||
|
if (result.data.code !== 0) {
|
||||||
|
throw result.data.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the formatted title for message
|
||||||
|
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||||
|
* @param {?object} monitorJSON Monitor details (For Up/Down only)
|
||||||
|
* @returns {string} Formatted title
|
||||||
|
*/
|
||||||
|
checkStatus(heartbeatJSON, monitorJSON) {
|
||||||
|
let title = "UptimeKuma Message";
|
||||||
|
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||||
|
title = "UptimeKuma Monitor Up " + monitorJSON["name"];
|
||||||
|
}
|
||||||
|
if (heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
||||||
|
title = "UptimeKuma Monitor Down " + monitorJSON["name"];
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WPush;
|
|
@ -42,6 +42,7 @@ const Pushy = require("./notification-providers/pushy");
|
||||||
const RocketChat = require("./notification-providers/rocket-chat");
|
const RocketChat = require("./notification-providers/rocket-chat");
|
||||||
const SerwerSMS = require("./notification-providers/serwersms");
|
const SerwerSMS = require("./notification-providers/serwersms");
|
||||||
const Signal = require("./notification-providers/signal");
|
const Signal = require("./notification-providers/signal");
|
||||||
|
const SIGNL4 = require("./notification-providers/signl4");
|
||||||
const Slack = require("./notification-providers/slack");
|
const Slack = require("./notification-providers/slack");
|
||||||
const SMSPartner = require("./notification-providers/smspartner");
|
const SMSPartner = require("./notification-providers/smspartner");
|
||||||
const SMSEagle = require("./notification-providers/smseagle");
|
const SMSEagle = require("./notification-providers/smseagle");
|
||||||
|
@ -64,6 +65,8 @@ const SevenIO = require("./notification-providers/sevenio");
|
||||||
const Whapi = require("./notification-providers/whapi");
|
const Whapi = require("./notification-providers/whapi");
|
||||||
const GtxMessaging = require("./notification-providers/gtx-messaging");
|
const GtxMessaging = require("./notification-providers/gtx-messaging");
|
||||||
const Cellsynt = require("./notification-providers/cellsynt");
|
const Cellsynt = require("./notification-providers/cellsynt");
|
||||||
|
const Onesender = require("./notification-providers/onesender");
|
||||||
|
const Wpush = require("./notification-providers/wpush");
|
||||||
|
|
||||||
class Notification {
|
class Notification {
|
||||||
|
|
||||||
|
@ -111,6 +114,7 @@ class Notification {
|
||||||
new Ntfy(),
|
new Ntfy(),
|
||||||
new Octopush(),
|
new Octopush(),
|
||||||
new OneBot(),
|
new OneBot(),
|
||||||
|
new Onesender(),
|
||||||
new Opsgenie(),
|
new Opsgenie(),
|
||||||
new PagerDuty(),
|
new PagerDuty(),
|
||||||
new FlashDuty(),
|
new FlashDuty(),
|
||||||
|
@ -124,6 +128,7 @@ class Notification {
|
||||||
new ServerChan(),
|
new ServerChan(),
|
||||||
new SerwerSMS(),
|
new SerwerSMS(),
|
||||||
new Signal(),
|
new Signal(),
|
||||||
|
new SIGNL4(),
|
||||||
new SMSManager(),
|
new SMSManager(),
|
||||||
new SMSPartner(),
|
new SMSPartner(),
|
||||||
new Slack(),
|
new Slack(),
|
||||||
|
@ -145,6 +150,7 @@ class Notification {
|
||||||
new Whapi(),
|
new Whapi(),
|
||||||
new GtxMessaging(),
|
new GtxMessaging(),
|
||||||
new Cellsynt(),
|
new Cellsynt(),
|
||||||
|
new Wpush(),
|
||||||
];
|
];
|
||||||
for (let item of list) {
|
for (let item of list) {
|
||||||
if (! item.name) {
|
if (! item.name) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
const { R } = require("redbean-node");
|
||||||
const PrometheusClient = require("prom-client");
|
const PrometheusClient = require("prom-client");
|
||||||
const { log } = require("../src/util");
|
const { log } = require("../src/util");
|
||||||
|
|
||||||
|
@ -9,36 +10,102 @@ const commonLabels = [
|
||||||
"monitor_port",
|
"monitor_port",
|
||||||
];
|
];
|
||||||
|
|
||||||
const monitorCertDaysRemaining = new PrometheusClient.Gauge({
|
|
||||||
name: "monitor_cert_days_remaining",
|
|
||||||
help: "The number of days remaining until the certificate expires",
|
|
||||||
labelNames: commonLabels
|
|
||||||
});
|
|
||||||
|
|
||||||
const monitorCertIsValid = new PrometheusClient.Gauge({
|
|
||||||
name: "monitor_cert_is_valid",
|
|
||||||
help: "Is the certificate still valid? (1 = Yes, 0= No)",
|
|
||||||
labelNames: commonLabels
|
|
||||||
});
|
|
||||||
const monitorResponseTime = new PrometheusClient.Gauge({
|
|
||||||
name: "monitor_response_time",
|
|
||||||
help: "Monitor Response Time (ms)",
|
|
||||||
labelNames: commonLabels
|
|
||||||
});
|
|
||||||
|
|
||||||
const monitorStatus = new PrometheusClient.Gauge({
|
|
||||||
name: "monitor_status",
|
|
||||||
help: "Monitor Status (1 = UP, 0= DOWN, 2= PENDING, 3= MAINTENANCE)",
|
|
||||||
labelNames: commonLabels
|
|
||||||
});
|
|
||||||
|
|
||||||
class Prometheus {
|
class Prometheus {
|
||||||
monitorLabelValues = {};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} monitor Monitor object to monitor
|
* Metric: monitor_cert_days_remaining
|
||||||
|
* @type {PrometheusClient.Gauge<string> | null}
|
||||||
*/
|
*/
|
||||||
constructor(monitor) {
|
static monitorCertDaysRemaining = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metric: monitor_cert_is_valid
|
||||||
|
* @type {PrometheusClient.Gauge<string> | null}
|
||||||
|
*/
|
||||||
|
static monitorCertIsValid = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metric: monitor_response_time
|
||||||
|
* @type {PrometheusClient.Gauge<string> | null}
|
||||||
|
*/
|
||||||
|
static monitorResponseTime = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metric: monitor_status
|
||||||
|
* @type {PrometheusClient.Gauge<string> | null}
|
||||||
|
*/
|
||||||
|
static monitorStatus = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All registered metric labels.
|
||||||
|
* @type {string[] | null}
|
||||||
|
*/
|
||||||
|
static monitorLabelNames = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitor labels/values combination.
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
|
monitorLabelValues;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize metrics and get all label names the first time called.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
static async initMetrics() {
|
||||||
|
if (!this.monitorLabelNames) {
|
||||||
|
let labelNames = await R.getCol("SELECT name FROM tag");
|
||||||
|
this.monitorLabelNames = [ ...commonLabels, ...labelNames ];
|
||||||
|
}
|
||||||
|
if (!this.monitorCertDaysRemaining) {
|
||||||
|
this.monitorCertDaysRemaining = new PrometheusClient.Gauge({
|
||||||
|
name: "monitor_cert_days_remaining",
|
||||||
|
help: "The number of days remaining until the certificate expires",
|
||||||
|
labelNames: this.monitorLabelNames
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this.monitorCertIsValid) {
|
||||||
|
this.monitorCertIsValid = new PrometheusClient.Gauge({
|
||||||
|
name: "monitor_cert_is_valid",
|
||||||
|
help: "Is the certificate still valid? (1 = Yes, 0 = No)",
|
||||||
|
labelNames: this.monitorLabelNames
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this.monitorResponseTime) {
|
||||||
|
this.monitorResponseTime = new PrometheusClient.Gauge({
|
||||||
|
name: "monitor_response_time",
|
||||||
|
help: "Monitor Response Time (ms)",
|
||||||
|
labelNames: this.monitorLabelNames
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this.monitorStatus) {
|
||||||
|
this.monitorStatus = new PrometheusClient.Gauge({
|
||||||
|
name: "monitor_status",
|
||||||
|
help: "Monitor Status (1 = UP, 0 = DOWN, 2 = PENDING, 3 = MAINTENANCE)",
|
||||||
|
labelNames: this.monitorLabelNames
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper to create a `Prometheus` instance and ensure metrics are initialized.
|
||||||
|
* @param {Monitor} monitor Monitor object to monitor
|
||||||
|
* @returns {Promise<Prometheus>} `Prometheus` instance
|
||||||
|
*/
|
||||||
|
static async createAndInitMetrics(monitor) {
|
||||||
|
await Prometheus.initMetrics();
|
||||||
|
let tags = await monitor.getTags();
|
||||||
|
return new Prometheus(monitor, tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a prometheus metric instance.
|
||||||
|
*
|
||||||
|
* Note: Make sure to call `Prometheus.initMetrics()` once prior creating Prometheus instances.
|
||||||
|
* @param {Monitor} monitor Monitor object to monitor
|
||||||
|
* @param {Promise<LooseObject<any>[]>} tags Tags of the monitor
|
||||||
|
*/
|
||||||
|
constructor(monitor, tags) {
|
||||||
this.monitorLabelValues = {
|
this.monitorLabelValues = {
|
||||||
monitor_name: monitor.name,
|
monitor_name: monitor.name,
|
||||||
monitor_type: monitor.type,
|
monitor_type: monitor.type,
|
||||||
|
@ -46,6 +113,12 @@ class Prometheus {
|
||||||
monitor_hostname: monitor.hostname,
|
monitor_hostname: monitor.hostname,
|
||||||
monitor_port: monitor.port
|
monitor_port: monitor.port
|
||||||
};
|
};
|
||||||
|
Object.values(tags)
|
||||||
|
// only label names that were known at first metric creation.
|
||||||
|
.filter(tag => Prometheus.monitorLabelNames.includes(tag.name))
|
||||||
|
.forEach(tag => {
|
||||||
|
this.monitorLabelValues[tag.name] = tag.value;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,7 +128,6 @@ class Prometheus {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
update(heartbeat, tlsInfo) {
|
update(heartbeat, tlsInfo) {
|
||||||
|
|
||||||
if (typeof tlsInfo !== "undefined") {
|
if (typeof tlsInfo !== "undefined") {
|
||||||
try {
|
try {
|
||||||
let isValid;
|
let isValid;
|
||||||
|
@ -64,7 +136,7 @@ class Prometheus {
|
||||||
} else {
|
} else {
|
||||||
isValid = 0;
|
isValid = 0;
|
||||||
}
|
}
|
||||||
monitorCertIsValid.set(this.monitorLabelValues, isValid);
|
Prometheus.monitorCertIsValid.set(this.monitorLabelValues, isValid);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("prometheus", "Caught error");
|
log.error("prometheus", "Caught error");
|
||||||
log.error("prometheus", e);
|
log.error("prometheus", e);
|
||||||
|
@ -72,7 +144,7 @@ class Prometheus {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (tlsInfo.certInfo != null) {
|
if (tlsInfo.certInfo != null) {
|
||||||
monitorCertDaysRemaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
|
Prometheus.monitorCertDaysRemaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("prometheus", "Caught error");
|
log.error("prometheus", "Caught error");
|
||||||
|
@ -82,7 +154,7 @@ class Prometheus {
|
||||||
|
|
||||||
if (heartbeat) {
|
if (heartbeat) {
|
||||||
try {
|
try {
|
||||||
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
|
Prometheus.monitorStatus.set(this.monitorLabelValues, heartbeat.status);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("prometheus", "Caught error");
|
log.error("prometheus", "Caught error");
|
||||||
log.error("prometheus", e);
|
log.error("prometheus", e);
|
||||||
|
@ -90,10 +162,10 @@ class Prometheus {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (typeof heartbeat.ping === "number") {
|
if (typeof heartbeat.ping === "number") {
|
||||||
monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
|
Prometheus.monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
|
||||||
} else {
|
} else {
|
||||||
// Is it good?
|
// Is it good?
|
||||||
monitorResponseTime.set(this.monitorLabelValues, -1);
|
Prometheus.monitorResponseTime.set(this.monitorLabelValues, -1);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("prometheus", "Caught error");
|
log.error("prometheus", "Caught error");
|
||||||
|
@ -108,10 +180,10 @@ class Prometheus {
|
||||||
*/
|
*/
|
||||||
remove() {
|
remove() {
|
||||||
try {
|
try {
|
||||||
monitorCertDaysRemaining.remove(this.monitorLabelValues);
|
Prometheus.monitorCertDaysRemaining?.remove(this.monitorLabelValues);
|
||||||
monitorCertIsValid.remove(this.monitorLabelValues);
|
Prometheus.monitorCertIsValid?.remove(this.monitorLabelValues);
|
||||||
monitorResponseTime.remove(this.monitorLabelValues);
|
Prometheus.monitorResponseTime?.remove(this.monitorLabelValues);
|
||||||
monitorStatus.remove(this.monitorLabelValues);
|
Prometheus.monitorStatus?.remove(this.monitorLabelValues);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ const nodeVersion = process.versions.node;
|
||||||
|
|
||||||
// Get the required Node.js version from package.json
|
// Get the required Node.js version from package.json
|
||||||
const requiredNodeVersions = require("../package.json").engines.node;
|
const requiredNodeVersions = require("../package.json").engines.node;
|
||||||
const bannedNodeVersions = " < 14 || 20.0.* || 20.1.* || 20.2.* || 20.3.* ";
|
const bannedNodeVersions = " < 18 || 20.0.* || 20.1.* || 20.2.* || 20.3.* ";
|
||||||
console.log(`Your Node.js version: ${nodeVersion}`);
|
console.log(`Your Node.js version: ${nodeVersion}`);
|
||||||
|
|
||||||
const semver = require("semver");
|
const semver = require("semver");
|
||||||
|
@ -132,9 +132,9 @@ const twoFAVerifyOptions = {
|
||||||
const testMode = !!args["test"] || false;
|
const testMode = !!args["test"] || false;
|
||||||
|
|
||||||
// Must be after io instantiation
|
// Must be after io instantiation
|
||||||
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList, sendRemoteBrowserList } = require("./client");
|
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList, sendRemoteBrowserList, sendMonitorTypeList } = require("./client");
|
||||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
const { databaseSocketHandler } = require("./socket-handlers/database-socket-handler");
|
||||||
const { remoteBrowserSocketHandler } = require("./socket-handlers/remote-browser-socket-handler");
|
const { remoteBrowserSocketHandler } = require("./socket-handlers/remote-browser-socket-handler");
|
||||||
const TwoFA = require("./2fa");
|
const TwoFA = require("./2fa");
|
||||||
const StatusPage = require("./model/status_page");
|
const StatusPage = require("./model/status_page");
|
||||||
|
@ -246,6 +246,36 @@ let needSetup = false;
|
||||||
log.debug("test", request.body);
|
log.debug("test", request.body);
|
||||||
response.send("OK");
|
response.send("OK");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
app.get("/_e2e/take-sqlite-snapshot", async (request, response) => {
|
||||||
|
await Database.close();
|
||||||
|
try {
|
||||||
|
fs.cpSync(Database.sqlitePath, `${Database.sqlitePath}.e2e-snapshot`);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error("Unable to copy SQLite DB.");
|
||||||
|
}
|
||||||
|
await Database.connect();
|
||||||
|
|
||||||
|
response.send("Snapshot taken.");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/_e2e/restore-sqlite-snapshot", async (request, response) => {
|
||||||
|
if (!fs.existsSync(`${Database.sqlitePath}.e2e-snapshot`)) {
|
||||||
|
throw new Error("Snapshot doesn't exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Database.close();
|
||||||
|
try {
|
||||||
|
fs.cpSync(`${Database.sqlitePath}.e2e-snapshot`, Database.sqlitePath);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error("Unable to copy snapshot file.");
|
||||||
|
}
|
||||||
|
await Database.connect();
|
||||||
|
|
||||||
|
response.send("Snapshot restored.");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Robots.txt
|
// Robots.txt
|
||||||
|
@ -686,6 +716,8 @@ let needSetup = false;
|
||||||
monitor.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
|
monitor.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
|
||||||
monitor.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
monitor.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
||||||
|
|
||||||
|
monitor.conditions = JSON.stringify(monitor.conditions);
|
||||||
|
|
||||||
bean.import(monitor);
|
bean.import(monitor);
|
||||||
bean.user_id = socket.userID;
|
bean.user_id = socket.userID;
|
||||||
|
|
||||||
|
@ -701,7 +733,7 @@ let needSetup = false;
|
||||||
await startMonitor(socket.userID, bean.id);
|
await startMonitor(socket.userID, bean.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
|
log.info("monitor", `Added Monitor: ${bean.id} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
@ -826,11 +858,17 @@ let needSetup = false;
|
||||||
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
|
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
|
||||||
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
||||||
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
|
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
|
||||||
|
bean.cacheBust = monitor.cacheBust;
|
||||||
bean.kafkaProducerSsl = monitor.kafkaProducerSsl;
|
bean.kafkaProducerSsl = monitor.kafkaProducerSsl;
|
||||||
bean.kafkaProducerAllowAutoTopicCreation =
|
bean.kafkaProducerAllowAutoTopicCreation =
|
||||||
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.snmpVersion = monitor.snmpVersion;
|
||||||
|
bean.snmpOid = monitor.snmpOid;
|
||||||
|
bean.jsonPathOperator = monitor.jsonPathOperator;
|
||||||
|
bean.timeout = monitor.timeout;
|
||||||
|
bean.conditions = JSON.stringify(monitor.conditions);
|
||||||
|
|
||||||
bean.validate();
|
bean.validate();
|
||||||
|
|
||||||
|
@ -1636,6 +1674,7 @@ async function afterLogin(socket, user) {
|
||||||
sendDockerHostList(socket),
|
sendDockerHostList(socket),
|
||||||
sendAPIKeyList(socket),
|
sendAPIKeyList(socket),
|
||||||
sendRemoteBrowserList(socket),
|
sendRemoteBrowserList(socket),
|
||||||
|
sendMonitorTypeList(socket),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await StatusPage.sendStatusPageList(io, socket);
|
await StatusPage.sendStatusPageList(io, socket);
|
||||||
|
|
|
@ -6,7 +6,7 @@ const Database = require("../database");
|
||||||
* @param {Socket} socket Socket.io instance
|
* @param {Socket} socket Socket.io instance
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
module.exports = (socket) => {
|
module.exports.databaseSocketHandler = (socket) => {
|
||||||
|
|
||||||
// Post or edit incident
|
// Post or edit incident
|
||||||
socket.on("getDatabaseSize", async (callback) => {
|
socket.on("getDatabaseSize", async (callback) => {
|
||||||
|
|
|
@ -29,8 +29,13 @@ function getGameList() {
|
||||||
return gameList;
|
return gameList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for general events
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
* @param {UptimeKumaServer} server Uptime Kuma server
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
module.exports.generalSocketHandler = (socket, server) => {
|
module.exports.generalSocketHandler = (socket, server) => {
|
||||||
|
|
||||||
socket.on("initServerTimezone", async (timezone) => {
|
socket.on("initServerTimezone", async (timezone) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
|
@ -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["snmp"] = new SNMPMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
|
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
|
||||||
|
|
||||||
// Allow all CORS origins (polling) in development
|
// Allow all CORS origins (polling) in development
|
||||||
|
@ -517,4 +518,5 @@ 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 { SNMPMonitorType } = require("./monitor-types/snmp");
|
||||||
const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
||||||
|
|
|
@ -576,6 +576,12 @@ optgroup {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prism-editor__container {
|
||||||
|
.important {
|
||||||
|
font-weight: var(--bs-body-font-weight) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h5.settings-subheading::after {
|
h5.settings-subheading::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
|
|
152
src/components/EditMonitorCondition.vue
Normal file
152
src/components/EditMonitorCondition.vue
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
<template>
|
||||||
|
<div class="monitor-condition mb-3" data-testid="condition">
|
||||||
|
<button
|
||||||
|
v-if="!isInGroup || !isFirst || !isLast"
|
||||||
|
class="btn btn-outline-danger remove-button"
|
||||||
|
type="button"
|
||||||
|
:aria-label="$t('conditionDelete')"
|
||||||
|
data-testid="remove-condition"
|
||||||
|
@click="remove"
|
||||||
|
>
|
||||||
|
<font-awesome-icon icon="trash" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<select v-if="!isFirst" v-model="model.andOr" class="form-select and-or-select" data-testid="condition-and-or">
|
||||||
|
<option value="and">{{ $t("and") }}</option>
|
||||||
|
<option value="or">{{ $t("or") }}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select v-model="model.variable" class="form-select" data-testid="condition-variable">
|
||||||
|
<option
|
||||||
|
v-for="variable in conditionVariables"
|
||||||
|
:key="variable.id"
|
||||||
|
:value="variable.id"
|
||||||
|
>
|
||||||
|
{{ $t(variable.id) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select v-model="model.operator" class="form-select" data-testid="condition-operator">
|
||||||
|
<option
|
||||||
|
v-for="operator in getVariableOperators(model.variable)"
|
||||||
|
:key="operator.id"
|
||||||
|
:value="operator.id"
|
||||||
|
>
|
||||||
|
{{ $t(operator.caption) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input
|
||||||
|
v-model="model.value"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:aria-label="$t('conditionValuePlaceholder')"
|
||||||
|
data-testid="condition-value"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "EditMonitorCondition",
|
||||||
|
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* The monitor condition
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this is the first condition
|
||||||
|
*/
|
||||||
|
isFirst: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this is the last condition
|
||||||
|
*/
|
||||||
|
isLast: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this condition is in a group
|
||||||
|
*/
|
||||||
|
isInGroup: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variable choices
|
||||||
|
*/
|
||||||
|
conditionVariables: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: [ "update:modelValue", "remove" ],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
model: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
remove() {
|
||||||
|
this.$emit("remove", this.model);
|
||||||
|
},
|
||||||
|
|
||||||
|
getVariableOperators(variableId) {
|
||||||
|
return this.conditionVariables.find(v => v.id === variableId)?.operators ?? [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.monitor-condition {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-button {
|
||||||
|
justify-self: flex-end;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (min-width: 500px) {
|
||||||
|
.monitor-condition {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-button {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
order: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.and-or-select {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
189
src/components/EditMonitorConditionGroup.vue
Normal file
189
src/components/EditMonitorConditionGroup.vue
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
<template>
|
||||||
|
<div class="condition-group mb-3" data-testid="condition-group">
|
||||||
|
<div class="d-flex">
|
||||||
|
<select v-if="!isFirst" v-model="model.andOr" class="form-select" style="width: auto;" data-testid="condition-group-and-or">
|
||||||
|
<option value="and">{{ $t("and") }}</option>
|
||||||
|
<option value="or">{{ $t("or") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="condition-group-inner mt-2 pa-2">
|
||||||
|
<div class="condition-group-conditions">
|
||||||
|
<template v-for="(child, childIndex) in model.children" :key="childIndex">
|
||||||
|
<EditMonitorConditionGroup
|
||||||
|
v-if="child.type === 'group'"
|
||||||
|
v-model="model.children[childIndex]"
|
||||||
|
:is-first="childIndex === 0"
|
||||||
|
:get-new-group="getNewGroup"
|
||||||
|
:get-new-condition="getNewCondition"
|
||||||
|
:condition-variables="conditionVariables"
|
||||||
|
@remove="removeChild"
|
||||||
|
/>
|
||||||
|
<EditMonitorCondition
|
||||||
|
v-else
|
||||||
|
v-model="model.children[childIndex]"
|
||||||
|
:is-first="childIndex === 0"
|
||||||
|
:is-last="childIndex === model.children.length - 1"
|
||||||
|
:is-in-group="true"
|
||||||
|
:condition-variables="conditionVariables"
|
||||||
|
@remove="removeChild"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="condition-group-actions mt-3">
|
||||||
|
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-condition-button" @click="addCondition">
|
||||||
|
{{ $t("conditionAdd") }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-group-button" @click="addGroup">
|
||||||
|
{{ $t("conditionAddGroup") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-danger"
|
||||||
|
type="button"
|
||||||
|
:aria-label="$t('conditionDeleteGroup')"
|
||||||
|
data-testid="remove-condition-group"
|
||||||
|
@click="remove"
|
||||||
|
>
|
||||||
|
<font-awesome-icon icon="trash" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EditMonitorCondition from "./EditMonitorCondition.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "EditMonitorConditionGroup",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
EditMonitorCondition,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* The condition group
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this is the first condition
|
||||||
|
*/
|
||||||
|
isFirst: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to generate a new group model
|
||||||
|
*/
|
||||||
|
getNewGroup: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to generate a new condition model
|
||||||
|
*/
|
||||||
|
getNewCondition: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variable choices
|
||||||
|
*/
|
||||||
|
conditionVariables: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: [ "update:modelValue", "remove" ],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
model: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
addGroup() {
|
||||||
|
const conditions = [ ...this.model.children ];
|
||||||
|
conditions.push(this.getNewGroup());
|
||||||
|
this.model.children = conditions;
|
||||||
|
},
|
||||||
|
|
||||||
|
addCondition() {
|
||||||
|
const conditions = [ ...this.model.children ];
|
||||||
|
conditions.push(this.getNewCondition());
|
||||||
|
this.model.children = conditions;
|
||||||
|
},
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
this.$emit("remove", this.model);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeChild(child) {
|
||||||
|
const idx = this.model.children.indexOf(child);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.model.children.splice(idx, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.condition-group-inner {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .condition-group-inner {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-group-conditions {
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-group-actions {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
.condition-group-actions > :last-child {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (min-width: 400px) {
|
||||||
|
.condition-group-actions {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
.condition-group-actions > :last-child {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete-group {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
149
src/components/EditMonitorConditions.vue
Normal file
149
src/components/EditMonitorConditions.vue
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
<template>
|
||||||
|
<div class="monitor-conditions">
|
||||||
|
<label class="form-label">{{ $t("Conditions") }}</label>
|
||||||
|
<div class="monitor-conditions-conditions">
|
||||||
|
<template v-for="(condition, conditionIndex) in model" :key="conditionIndex">
|
||||||
|
<EditMonitorConditionGroup
|
||||||
|
v-if="condition.type === 'group'"
|
||||||
|
v-model="model[conditionIndex]"
|
||||||
|
:is-first="conditionIndex === 0"
|
||||||
|
:get-new-group="getNewGroup"
|
||||||
|
:get-new-condition="getNewCondition"
|
||||||
|
:condition-variables="conditionVariables"
|
||||||
|
@remove="removeCondition"
|
||||||
|
/>
|
||||||
|
<EditMonitorCondition
|
||||||
|
v-else
|
||||||
|
v-model="model[conditionIndex]"
|
||||||
|
:is-first="conditionIndex === 0"
|
||||||
|
:is-last="conditionIndex === model.length - 1"
|
||||||
|
:condition-variables="conditionVariables"
|
||||||
|
@remove="removeCondition"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="monitor-conditions-buttons">
|
||||||
|
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-condition-button" @click="addCondition">
|
||||||
|
{{ $t("conditionAdd") }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-group-button" @click="addGroup">
|
||||||
|
{{ $t("conditionAddGroup") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EditMonitorConditionGroup from "./EditMonitorConditionGroup.vue";
|
||||||
|
import EditMonitorCondition from "./EditMonitorCondition.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "EditMonitorConditions",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
EditMonitorConditionGroup,
|
||||||
|
EditMonitorCondition,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* The monitor conditions
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
conditionVariables: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: [ "update:modelValue" ],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
model: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.model.length === 0) {
|
||||||
|
this.addCondition();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getNewGroup() {
|
||||||
|
return {
|
||||||
|
type: "group",
|
||||||
|
children: [ this.getNewCondition() ],
|
||||||
|
andOr: "and",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getNewCondition() {
|
||||||
|
const firstVariable = this.conditionVariables[0]?.id || null;
|
||||||
|
const firstOperator = this.getVariableOperators(firstVariable)[0] || null;
|
||||||
|
return {
|
||||||
|
type: "expression",
|
||||||
|
variable: firstVariable,
|
||||||
|
operator: firstOperator?.id || null,
|
||||||
|
value: "",
|
||||||
|
andOr: "and",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addGroup() {
|
||||||
|
const conditions = [ ...this.model ];
|
||||||
|
conditions.push(this.getNewGroup());
|
||||||
|
this.$emit("update:modelValue", conditions);
|
||||||
|
},
|
||||||
|
|
||||||
|
addCondition() {
|
||||||
|
const conditions = [ ...this.model ];
|
||||||
|
conditions.push(this.getNewCondition());
|
||||||
|
this.$emit("update:modelValue", conditions);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeCondition(condition) {
|
||||||
|
const conditions = [ ...this.model ];
|
||||||
|
const idx = conditions.indexOf(condition);
|
||||||
|
if (idx !== -1) {
|
||||||
|
conditions.splice(idx, 1);
|
||||||
|
this.$emit("update:modelValue", conditions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getVariableOperators(variableId) {
|
||||||
|
return this.conditionVariables.find(v => v.id === variableId)?.operators ?? [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.monitor-conditions,
|
||||||
|
.monitor-conditions-conditions {
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-conditions-buttons {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (min-width: 400px) {
|
||||||
|
.monitor-conditions-buttons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -14,7 +14,7 @@
|
||||||
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
|
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
|
||||||
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
|
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
|
||||||
>
|
>
|
||||||
<div>{{ timeSinceFirstBeat }} ago</div>
|
<div>{{ timeSinceFirstBeat }}</div>
|
||||||
<div v-if="$root.styleElapsedTime === 'with-line'" class="connecting-line"></div>
|
<div v-if="$root.styleElapsedTime === 'with-line'" class="connecting-line"></div>
|
||||||
<div>{{ timeSinceLastBeat }}</div>
|
<div>{{ timeSinceLastBeat }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -184,11 +184,11 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seconds < tolerance) {
|
if (seconds < tolerance) {
|
||||||
return "now";
|
return this.$t("now");
|
||||||
} else if (seconds < 60 * 60) {
|
} else if (seconds < 60 * 60) {
|
||||||
return (seconds / 60).toFixed(0) + "m ago";
|
return this.$t("time ago", [ (seconds / 60).toFixed(0) + "m" ]);
|
||||||
} else {
|
} else {
|
||||||
return (seconds / 60 / 60).toFixed(0) + "h ago";
|
return this.$t("time ago", [ (seconds / 60 / 60).toFixed(0) + "h" ]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -135,6 +135,7 @@ export default {
|
||||||
"ntfy": "Ntfy",
|
"ntfy": "Ntfy",
|
||||||
"octopush": "Octopush",
|
"octopush": "Octopush",
|
||||||
"OneBot": "OneBot",
|
"OneBot": "OneBot",
|
||||||
|
"Onesender": "Onesender",
|
||||||
"Opsgenie": "Opsgenie",
|
"Opsgenie": "Opsgenie",
|
||||||
"PagerDuty": "PagerDuty",
|
"PagerDuty": "PagerDuty",
|
||||||
"PagerTree": "PagerTree",
|
"PagerTree": "PagerTree",
|
||||||
|
@ -144,6 +145,7 @@ export default {
|
||||||
"pushy": "Pushy",
|
"pushy": "Pushy",
|
||||||
"rocket.chat": "Rocket.Chat",
|
"rocket.chat": "Rocket.Chat",
|
||||||
"signal": "Signal",
|
"signal": "Signal",
|
||||||
|
"SIGNL4": "SIGNL4",
|
||||||
"slack": "Slack",
|
"slack": "Slack",
|
||||||
"squadcast": "SquadCast",
|
"squadcast": "SquadCast",
|
||||||
"SMSEagle": "SMSEagle",
|
"SMSEagle": "SMSEagle",
|
||||||
|
@ -178,6 +180,7 @@ export default {
|
||||||
"WeCom": "WeCom (企业微信群机器人)",
|
"WeCom": "WeCom (企业微信群机器人)",
|
||||||
"ServerChan": "ServerChan (Server酱)",
|
"ServerChan": "ServerChan (Server酱)",
|
||||||
"smsc": "SMSC",
|
"smsc": "SMSC",
|
||||||
|
"WPush": "WPush(wpush.cn)",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sort by notification name
|
// Sort by notification name
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<template #item="monitor">
|
<template #item="monitor">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-9 col-md-8 small-padding">
|
<div class="col-6 col-md-4 small-padding">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
|
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
|
||||||
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
|
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
<div :key="$root.userHeartbeatBar" class="col-6 col-md-8">
|
||||||
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
|
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
81
src/components/notifications/Onesender.vue
Normal file
81
src/components/notifications/Onesender.vue
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="host-onesender" class="form-label">{{ $t("Host Onesender") }}</label>
|
||||||
|
<input
|
||||||
|
id="host-onesender"
|
||||||
|
v-model="$parent.notification.onesenderURL"
|
||||||
|
type="url"
|
||||||
|
placeholder="https://xxxxxxxxxxx.com/api/v1/messages"
|
||||||
|
pattern="https?://.+"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="receiver-onesender" class="form-label">{{ $t("Token Onesender") }}</label>
|
||||||
|
<HiddenInput id="receiver-onesender" v-model="$parent.notification.onesenderToken" :required="true" autocomplete="false"></HiddenInput>
|
||||||
|
<i18n-t tag="div" keypath="wayToGetOnesenderUrlandToken" class="form-text">
|
||||||
|
<a href="https://onesender.net/" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="webhook-request-body" class="form-label">{{ $t("Recipient Type") }}</label>
|
||||||
|
<select
|
||||||
|
id="webhook-request-body"
|
||||||
|
v-model="$parent.notification.onesenderTypeReceiver"
|
||||||
|
class="form-select"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="private">{{ $t("Private Number") }}</option>
|
||||||
|
<option value="group">{{ $t("Group ID") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div v-if="$parent.notification.onesenderTypeReceiver == 'private'" class="form-text">{{ $t("privateOnesenderDesc", ['"application/json"']) }}</div>
|
||||||
|
<div v-else class="form-text">{{ $t("groupOnesenderDesc") }}</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<input
|
||||||
|
id="type-receiver-onesender"
|
||||||
|
v-model="$parent.notification.onesenderReceiver"
|
||||||
|
type="text"
|
||||||
|
placeholder="628123456789 or 628123456789-34534"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<input
|
||||||
|
id="type-receiver-onesender"
|
||||||
|
v-model="computedReceiverResult"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
computedReceiverResult() {
|
||||||
|
let receiver = this.$parent.notification.onesenderReceiver;
|
||||||
|
return this.$parent.notification.onesenderTypeReceiver === "private" ? receiver + "@s.whatsapp.net" : receiver + "@g.us";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
textarea {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
16
src/components/notifications/SIGNL4.vue
Normal file
16
src/components/notifications/SIGNL4.vue
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="signl4-webhook-url" class="form-label">{{ $t("SIGNL4 Webhook URL") }}</label>
|
||||||
|
<input
|
||||||
|
id="signl4-webhook-url"
|
||||||
|
v-model="$parent.notification.webhookURL"
|
||||||
|
type="url"
|
||||||
|
pattern="https?://.+"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<i18n-t tag="div" keypath="signl4Docs" class="form-text">
|
||||||
|
<a href="https://docs.signl4.com/integrations/uptime-kuma/uptime-kuma.html" target="_blank">SIGNL4 Docs</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</template>
|
31
src/components/notifications/WPush.vue
Normal file
31
src/components/notifications/WPush.vue
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wpush-apikey" class="form-label">WPush {{ $t("API Key") }}</label>
|
||||||
|
<HiddenInput id="wpush-apikey" v-model="$parent.notification.wpushAPIkey" :required="true" autocomplete="new-password" placeholder="WPushxxxxx"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wpush-channel" class="form-label">发送通道</label>
|
||||||
|
<select id="wpush-channel" v-model="$parent.notification.wpushChannel" class="form-select" required>
|
||||||
|
<option value="wechat">微信</option>
|
||||||
|
<option value="sms">短信</option>
|
||||||
|
<option value="mail">邮件</option>
|
||||||
|
<option value="feishu">飞书</option>
|
||||||
|
<option value="dingtalk">钉钉</option>
|
||||||
|
<option value="wechat_work">企业微信</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<i18n-t tag="p" keypath="More info on:">
|
||||||
|
<a href="https://wpush.cn/" rel="noopener noreferrer" target="_blank">https://wpush.cn/</a>
|
||||||
|
</i18n-t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -29,6 +29,7 @@ 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 OneBot from "./OneBot.vue";
|
import OneBot from "./OneBot.vue";
|
||||||
|
import Onesender from "./Onesender.vue";
|
||||||
import Opsgenie from "./Opsgenie.vue";
|
import Opsgenie from "./Opsgenie.vue";
|
||||||
import PagerDuty from "./PagerDuty.vue";
|
import PagerDuty from "./PagerDuty.vue";
|
||||||
import FlashDuty from "./FlashDuty.vue";
|
import FlashDuty from "./FlashDuty.vue";
|
||||||
|
@ -62,6 +63,8 @@ import Splunk from "./Splunk.vue";
|
||||||
import SevenIO from "./SevenIO.vue";
|
import SevenIO from "./SevenIO.vue";
|
||||||
import Whapi from "./Whapi.vue";
|
import Whapi from "./Whapi.vue";
|
||||||
import Cellsynt from "./Cellsynt.vue";
|
import Cellsynt from "./Cellsynt.vue";
|
||||||
|
import WPush from "./WPush.vue";
|
||||||
|
import SIGNL4 from "./SIGNL4.vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage all notification form.
|
* Manage all notification form.
|
||||||
|
@ -98,6 +101,7 @@ const NotificationFormList = {
|
||||||
"ntfy": Ntfy,
|
"ntfy": Ntfy,
|
||||||
"octopush": Octopush,
|
"octopush": Octopush,
|
||||||
"OneBot": OneBot,
|
"OneBot": OneBot,
|
||||||
|
"Onesender": Onesender,
|
||||||
"Opsgenie": Opsgenie,
|
"Opsgenie": Opsgenie,
|
||||||
"PagerDuty": PagerDuty,
|
"PagerDuty": PagerDuty,
|
||||||
"FlashDuty": FlashDuty,
|
"FlashDuty": FlashDuty,
|
||||||
|
@ -111,6 +115,7 @@ const NotificationFormList = {
|
||||||
"rocket.chat": RocketChat,
|
"rocket.chat": RocketChat,
|
||||||
"serwersms": SerwerSMS,
|
"serwersms": SerwerSMS,
|
||||||
"signal": Signal,
|
"signal": Signal,
|
||||||
|
"SIGNL4": SIGNL4,
|
||||||
"SMSManager": SMSManager,
|
"SMSManager": SMSManager,
|
||||||
"SMSPartner": SMSPartner,
|
"SMSPartner": SMSPartner,
|
||||||
"slack": Slack,
|
"slack": Slack,
|
||||||
|
@ -132,6 +137,7 @@ const NotificationFormList = {
|
||||||
"whapi": Whapi,
|
"whapi": Whapi,
|
||||||
"gtxmessaging": GtxMessaging,
|
"gtxmessaging": GtxMessaging,
|
||||||
"Cellsynt": Cellsynt,
|
"Cellsynt": Cellsynt,
|
||||||
|
"WPush": WPush
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NotificationFormList;
|
export default NotificationFormList;
|
||||||
|
|
|
@ -802,7 +802,6 @@
|
||||||
"twilioApiKey": "API ключ (по избор)",
|
"twilioApiKey": "API ключ (по избор)",
|
||||||
"Expected Value": "Очаквана стойност",
|
"Expected Value": "Очаквана стойност",
|
||||||
"Json Query": "Заявка тип JSON",
|
"Json Query": "Заявка тип JSON",
|
||||||
"jsonQueryDescription": "Прави JSON заявка срещу отговора и проверява за очаквана стойност (Върнатата стойност ще бъде преобразувана в низ за сравнение). Разгледайте {0} за документация относно езика на заявката. Имате възможност да тествате {1}.",
|
|
||||||
"Badge Duration (in hours)": "Времетраене на баджа (в часове)",
|
"Badge Duration (in hours)": "Времетраене на баджа (в часове)",
|
||||||
"Badge Preview": "Преглед на баджа",
|
"Badge Preview": "Преглед на баджа",
|
||||||
"Notify Channel": "Канал за известяване",
|
"Notify Channel": "Канал за известяване",
|
||||||
|
|
|
@ -823,7 +823,6 @@
|
||||||
"Enable Kafka Producer Auto Topic Creation": "Povolit Kafka zprostředkovateli automatické vytváření vláken",
|
"Enable Kafka Producer Auto Topic Creation": "Povolit Kafka zprostředkovateli automatické vytváření vláken",
|
||||||
"Kafka Producer Message": "Zpráva Kafka zprostředkovatele",
|
"Kafka Producer Message": "Zpráva Kafka zprostředkovatele",
|
||||||
"tailscalePingWarning": "Abyste mohli používat Tailscale Ping monitor, je nutné Uptime Kuma nainstalovat mimo Docker, a dále na váš server nainstalovat Tailscale klienta.",
|
"tailscalePingWarning": "Abyste mohli používat Tailscale Ping monitor, je nutné Uptime Kuma nainstalovat mimo Docker, a dále na váš server nainstalovat Tailscale klienta.",
|
||||||
"jsonQueryDescription": "Proveďte JSON dotaz vůči odpovědi a zkontrolujte očekávaný výstup (za účelem porovnání bude návratová hodnota převedena na řetězec). Dokumentaci k dotazovacímu jazyku naleznete na {0}, a využít můžete též {1}.",
|
|
||||||
"Select": "Vybrat",
|
"Select": "Vybrat",
|
||||||
"selectedMonitorCount": "Vybráno: {0}",
|
"selectedMonitorCount": "Vybráno: {0}",
|
||||||
"Check/Uncheck": "Vybrat/Zrušit výběr",
|
"Check/Uncheck": "Vybrat/Zrušit výběr",
|
||||||
|
|
|
@ -812,7 +812,6 @@
|
||||||
"Json Query": "Json-Abfrage",
|
"Json Query": "Json-Abfrage",
|
||||||
"filterActive": "Aktiv",
|
"filterActive": "Aktiv",
|
||||||
"filterActivePaused": "Pausiert",
|
"filterActivePaused": "Pausiert",
|
||||||
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf {0} findest du die Dokumentation zur Abfragesprache. {1} kannst du Abfragen üben.",
|
|
||||||
"Badge Duration (in hours)": "Abzeichen Dauer (in Stunden)",
|
"Badge Duration (in hours)": "Abzeichen Dauer (in Stunden)",
|
||||||
"Badge Preview": "Abzeichen Vorschau",
|
"Badge Preview": "Abzeichen Vorschau",
|
||||||
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
|
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
|
||||||
|
|
|
@ -817,7 +817,6 @@
|
||||||
"filterActivePaused": "Pausiert",
|
"filterActivePaused": "Pausiert",
|
||||||
"Expected Value": "Erwarteter Wert",
|
"Expected Value": "Erwarteter Wert",
|
||||||
"Json Query": "Json-Abfrage",
|
"Json Query": "Json-Abfrage",
|
||||||
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf {0} findest du die Dokumentation zur Abfragesprache. {1} kannst du Abfragen üben.",
|
|
||||||
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
|
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
|
||||||
"Server URL should not contain the nfty topic": "Die Server-URL sollte das nfty-Thema nicht enthalten",
|
"Server URL should not contain the nfty topic": "Die Server-URL sollte das nfty-Thema nicht enthalten",
|
||||||
"pushDeerServerDescription": "Leer lassen um den offiziellen Server zu verwenden",
|
"pushDeerServerDescription": "Leer lassen um den offiziellen Server zu verwenden",
|
||||||
|
|
|
@ -49,17 +49,20 @@
|
||||||
"Uptime": "Uptime",
|
"Uptime": "Uptime",
|
||||||
"Cert Exp.": "Cert Exp.",
|
"Cert Exp.": "Cert Exp.",
|
||||||
"Monitor": "Monitor | Monitors",
|
"Monitor": "Monitor | Monitors",
|
||||||
|
"now": "now",
|
||||||
|
"time ago": "{0} ago",
|
||||||
"day": "day | days",
|
"day": "day | days",
|
||||||
"-day": "-day",
|
"-day": "-day",
|
||||||
"hour": "hour",
|
"hour": "hour",
|
||||||
"-hour": "-hour",
|
"-hour": "-hour",
|
||||||
|
"-year": "-year",
|
||||||
"Response": "Response",
|
"Response": "Response",
|
||||||
"Ping": "Ping",
|
"Ping": "Ping",
|
||||||
"Monitor Type": "Monitor Type",
|
"Monitor Type": "Monitor Type",
|
||||||
"Keyword": "Keyword",
|
"Keyword": "Keyword",
|
||||||
"Invert Keyword": "Invert Keyword",
|
"Invert Keyword": "Invert Keyword",
|
||||||
"Expected Value": "Expected Value",
|
"Expected Value": "Expected Value",
|
||||||
"Json Query": "Json Query",
|
"Json Query Expression": "Json Query Expression",
|
||||||
"Friendly Name": "Friendly Name",
|
"Friendly Name": "Friendly Name",
|
||||||
"URL": "URL",
|
"URL": "URL",
|
||||||
"Hostname": "Hostname",
|
"Hostname": "Hostname",
|
||||||
|
@ -441,6 +444,7 @@
|
||||||
"backupOutdatedWarning": "Deprecated: Since a lot of features were added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.",
|
"backupOutdatedWarning": "Deprecated: Since a lot of features were added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.",
|
||||||
"backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.",
|
"backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.",
|
||||||
"Optional": "Optional",
|
"Optional": "Optional",
|
||||||
|
"and": "and",
|
||||||
"or": "or",
|
"or": "or",
|
||||||
"sameAsServerTimezone": "Same as Server Timezone",
|
"sameAsServerTimezone": "Same as Server Timezone",
|
||||||
"startDateTime": "Start Date/Time",
|
"startDateTime": "Start Date/Time",
|
||||||
|
@ -588,7 +592,7 @@
|
||||||
"notificationDescription": "Notifications must be assigned to a monitor to function.",
|
"notificationDescription": "Notifications must be assigned to a monitor to function.",
|
||||||
"keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
|
"keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
|
||||||
"invertKeywordDescription": "Look for the keyword to be absent rather than present.",
|
"invertKeywordDescription": "Look for the keyword to be absent rather than present.",
|
||||||
"jsonQueryDescription": "Do a json Query against the response and check for expected value (Return value will get converted into string for comparison). Check out {0} for the documentation about the query language. A playground can be found {1}.",
|
"jsonQueryDescription": "Parse and extract specific data from the server's JSON response using JSON query or use \"$\" for the raw response, if not expecting JSON. The result is then compared to the expected value, as strings. See {0} for documentation and use {1} to experiment with queries.",
|
||||||
"backupDescription": "You can backup all monitors and notifications into a JSON file.",
|
"backupDescription": "You can backup all monitors and notifications into a JSON file.",
|
||||||
"backupDescription2": "Note: history and event data is not included.",
|
"backupDescription2": "Note: history and event data is not included.",
|
||||||
"backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.",
|
"backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.",
|
||||||
|
@ -876,6 +880,8 @@
|
||||||
"nostrRecipientsHelp": "npub format, one per line",
|
"nostrRecipientsHelp": "npub format, one per line",
|
||||||
"showCertificateExpiry": "Show Certificate Expiry",
|
"showCertificateExpiry": "Show Certificate Expiry",
|
||||||
"noOrBadCertificate": "No/Bad Certificate",
|
"noOrBadCertificate": "No/Bad Certificate",
|
||||||
|
"cacheBusterParam": "Add the {0} parameter",
|
||||||
|
"cacheBusterParamDescription": "Randomly generated parameter to skip caches.",
|
||||||
"gamedigGuessPort": "Gamedig: Guess Port",
|
"gamedigGuessPort": "Gamedig: Guess Port",
|
||||||
"gamedigGuessPortDescription": "The port used by Valve Server Query Protocol may be different from the client port. Try this if the monitor cannot connect to your server.",
|
"gamedigGuessPortDescription": "The port used by Valve Server Query Protocol may be different from the client port. Try this if the monitor cannot connect to your server.",
|
||||||
"Bitrix24 Webhook URL": "Bitrix24 Webhook URL",
|
"Bitrix24 Webhook URL": "Bitrix24 Webhook URL",
|
||||||
|
@ -943,6 +949,13 @@
|
||||||
"cellsyntSplitLongMessages": "Split long messages into up to 6 parts. 153 x 6 = 918 characters.",
|
"cellsyntSplitLongMessages": "Split long messages into up to 6 parts. 153 x 6 = 918 characters.",
|
||||||
"max 15 digits": "max 15 digits",
|
"max 15 digits": "max 15 digits",
|
||||||
"max 11 alphanumeric characters": "max 11 alphanumeric characters",
|
"max 11 alphanumeric characters": "max 11 alphanumeric characters",
|
||||||
|
"Community String": "Community String",
|
||||||
|
"snmpCommunityStringHelptext": "This string functions as a password to authenticate and control access to SNMP-enabled devices. Match it with your SNMP device's configuration.",
|
||||||
|
"OID (Object Identifier)": "OID (Object Identifier)",
|
||||||
|
"snmpOIDHelptext": "Enter the OID for the sensor or status you want to monitor. Use network management tools like MIB browsers or SNMP software if you're unsure about the OID.",
|
||||||
|
"Condition": "Condition",
|
||||||
|
"SNMP Version": "SNMP Version",
|
||||||
|
"Please enter a valid OID.": "Please enter a valid OID.",
|
||||||
"wayToGetThreemaGateway": "You can register for Threema Gateway {0}.",
|
"wayToGetThreemaGateway": "You can register for Threema Gateway {0}.",
|
||||||
"threemaRecipient": "Recipient",
|
"threemaRecipient": "Recipient",
|
||||||
"threemaRecipientType": "Recipient Type",
|
"threemaRecipientType": "Recipient Type",
|
||||||
|
@ -955,5 +968,51 @@
|
||||||
"threemaSenderIdentityFormat": "8 characters, usually starts with *",
|
"threemaSenderIdentityFormat": "8 characters, usually starts with *",
|
||||||
"threemaApiAuthenticationSecret": "Gateway-ID Secret",
|
"threemaApiAuthenticationSecret": "Gateway-ID Secret",
|
||||||
"threemaBasicModeInfo": "Note: This integration uses Threema Gateway in basic mode (server-based encryption). Further details can be found {0}.",
|
"threemaBasicModeInfo": "Note: This integration uses Threema Gateway in basic mode (server-based encryption). Further details can be found {0}.",
|
||||||
"apiKeysDisabledMsg": "API keys are disabled because authentication is disabled."
|
"apiKeysDisabledMsg": "API keys are disabled because authentication is disabled.",
|
||||||
|
"Host Onesender": "Host Onesender",
|
||||||
|
"Token Onesender": "Token Onesender",
|
||||||
|
"Recipient Type": "Recipient Type",
|
||||||
|
"Private Number": "Private Number",
|
||||||
|
"privateOnesenderDesc": "Make sure the number phone is valid. To send message into private number phone, ex: 628123456789",
|
||||||
|
"groupOnesenderDesc": "Make sure the GroupID is valid. To send message into Group, ex: 628123456789-342345",
|
||||||
|
"Group ID": "Group ID",
|
||||||
|
"wayToGetOnesenderUrlandToken":"You can get the URL and Token by going to the Onesender website. More info {0}",
|
||||||
|
"Add Remote Browser": "Add Remote Browser",
|
||||||
|
"New Group": "New Group",
|
||||||
|
"Group Name": "Group Name",
|
||||||
|
"OAuth2: Client Credentials": "OAuth2: Client Credentials",
|
||||||
|
"Authentication Method": "Authentication Method",
|
||||||
|
"Authorization Header": "Authorization Header",
|
||||||
|
"Form Data Body": "Form Data Body",
|
||||||
|
"OAuth Token URL": "OAuth Token URL",
|
||||||
|
"Client ID": "Client ID",
|
||||||
|
"Client Secret": "Client Secret",
|
||||||
|
"OAuth Scope": "OAuth Scope",
|
||||||
|
"Optional: Space separated list of scopes": "Optional: Space separated list of scopes",
|
||||||
|
"Go back to home page.": "Go back to home page.",
|
||||||
|
"No tags found.": "No tags found.",
|
||||||
|
"Lost connection to the socket server.": "Lost connection to the socket server.",
|
||||||
|
"Cannot connect to the socket server.": "Cannot connect to the socket server.",
|
||||||
|
"SIGNL4": "SIGNL4",
|
||||||
|
"SIGNL4 Webhook URL": "SIGNL4 Webhook URL",
|
||||||
|
"signl4Docs": "You can find more information about how to configure SIGNL4 and how to obtain the SIGNL4 webhook URL in the {0}.",
|
||||||
|
"Conditions": "Conditions",
|
||||||
|
"conditionAdd": "Add Condition",
|
||||||
|
"conditionDelete": "Delete Condition",
|
||||||
|
"conditionAddGroup": "Add Group",
|
||||||
|
"conditionDeleteGroup": "Delete Group",
|
||||||
|
"conditionValuePlaceholder": "Value",
|
||||||
|
"equals": "equals",
|
||||||
|
"not equals": "not equals",
|
||||||
|
"contains": "contains",
|
||||||
|
"not contains": "not contains",
|
||||||
|
"starts with": "starts with",
|
||||||
|
"not starts with": "not starts with",
|
||||||
|
"ends with": "ends with",
|
||||||
|
"not ends with": "not ends with",
|
||||||
|
"less than": "less than",
|
||||||
|
"greater than": "greater than",
|
||||||
|
"less than or equal to": "less than or equal to",
|
||||||
|
"greater than or equal to": "greater than or equal to",
|
||||||
|
"record": "record"
|
||||||
}
|
}
|
||||||
|
|
|
@ -771,7 +771,6 @@
|
||||||
"Json Query": "Consulta Json",
|
"Json Query": "Consulta Json",
|
||||||
"invertKeywordDescription": "Comprobar si la palabra clave está ausente en vez de presente.",
|
"invertKeywordDescription": "Comprobar si la palabra clave está ausente en vez de presente.",
|
||||||
"enableNSCD": "Habilitar NSCD (Demonio de Caché de Servicio de Nombres) para almacenar en caché todas las solicitudes DNS",
|
"enableNSCD": "Habilitar NSCD (Demonio de Caché de Servicio de Nombres) para almacenar en caché todas las solicitudes DNS",
|
||||||
"jsonQueryDescription": "Realiza una consulta JSON contra la respuesta y verifica el valor esperado (el valor de retorno se convertirá a una cadena para la comparación). Consulta {0} para obtener documentación sobre el lenguaje de consulta. Puede encontrar un espacio de prueba {1}.",
|
|
||||||
"Request Timeout": "Tiempo de espera máximo de petición",
|
"Request Timeout": "Tiempo de espera máximo de petición",
|
||||||
"timeoutAfter": "Expirar después de {0} segundos",
|
"timeoutAfter": "Expirar después de {0} segundos",
|
||||||
"chromeExecutableDescription": "Para usuarios de Docker, si Chromium no está instalado, puede que tarde unos minutos en ser instalado y mostrar el resultado de la prueba. Usa 1GB de espacio.",
|
"chromeExecutableDescription": "Para usuarios de Docker, si Chromium no está instalado, puede que tarde unos minutos en ser instalado y mostrar el resultado de la prueba. Usa 1GB de espacio.",
|
||||||
|
|
|
@ -759,7 +759,6 @@
|
||||||
"filterActive": "فعال",
|
"filterActive": "فعال",
|
||||||
"webhookCustomBodyDesc": "یک بدنه HTTP سفارشی برای ریکوئست تعریف کنید. متغیر های قابل استفاده: {msg}, {heartbeat}, {monitor}.",
|
"webhookCustomBodyDesc": "یک بدنه HTTP سفارشی برای ریکوئست تعریف کنید. متغیر های قابل استفاده: {msg}, {heartbeat}, {monitor}.",
|
||||||
"tailscalePingWarning": "برای استفاده از Tailscale Ping monitor، شما باید آپتایم کوما را بدون استفاده از داکر و همچنین Tailscale client را نیز بر روی سرور خود نصب داشته باشید.",
|
"tailscalePingWarning": "برای استفاده از Tailscale Ping monitor، شما باید آپتایم کوما را بدون استفاده از داکر و همچنین Tailscale client را نیز بر روی سرور خود نصب داشته باشید.",
|
||||||
"jsonQueryDescription": "یک کوئری json در برابر پاسخ انجام دهید و مقدار مورد انتظار را (مقدار برگشتی برای مقایسه به رشته تبدیل می شود). برای مستندات درباره زبان کوئری، {0} مشاهده کنید. همچنین محیط تست را میتوانید در {1} پیدا کنید.",
|
|
||||||
"Enter the list of brokers": "لیست بروکر هارا وارد کنید",
|
"Enter the list of brokers": "لیست بروکر هارا وارد کنید",
|
||||||
"Enable Kafka Producer Auto Topic Creation": "فعال سازی ایجاپ موضوع اتوماتیک تهیه کننده",
|
"Enable Kafka Producer Auto Topic Creation": "فعال سازی ایجاپ موضوع اتوماتیک تهیه کننده",
|
||||||
"Secret AccessKey": "کلید محرمانه AccessKey",
|
"Secret AccessKey": "کلید محرمانه AccessKey",
|
||||||
|
|
|
@ -792,7 +792,6 @@
|
||||||
"emailTemplateLimitedToUpDownNotification": "saatavilla vain YLÖS/ALAS sydämensykkeille, muulloin null",
|
"emailTemplateLimitedToUpDownNotification": "saatavilla vain YLÖS/ALAS sydämensykkeille, muulloin null",
|
||||||
"Your User ID": "Käyttäjätunnuksesi",
|
"Your User ID": "Käyttäjätunnuksesi",
|
||||||
"invertKeywordDescription": "Etsi puuttuvaa avainsanaa.",
|
"invertKeywordDescription": "Etsi puuttuvaa avainsanaa.",
|
||||||
"jsonQueryDescription": "Suorita JSON-kysely vastaukselle ja tarkista odotettu arvo (Paluuarvo muutetaan merkkijonoksi vertailua varten). Katso kyselykielen ohjeita osoitteesta {0}. Leikkikenttä löytyy osoitteesta {1}.",
|
|
||||||
"Bark API Version": "Bark API-versio",
|
"Bark API Version": "Bark API-versio",
|
||||||
"Notify Channel": "Ilmoitus kanavalle",
|
"Notify Channel": "Ilmoitus kanavalle",
|
||||||
"aboutNotifyChannel": "Ilmoitus kanavalle antaa työpöytä- tai mobiili-ilmoituksen kaikille kanavan jäsenille; riippumatta ovatko he paikalla vai poissa.",
|
"aboutNotifyChannel": "Ilmoitus kanavalle antaa työpöytä- tai mobiili-ilmoituksen kaikille kanavan jäsenille; riippumatta ovatko he paikalla vai poissa.",
|
||||||
|
|
|
@ -797,7 +797,6 @@
|
||||||
"twilioApiKey": "Clé API (facultatif)",
|
"twilioApiKey": "Clé API (facultatif)",
|
||||||
"Expected Value": "Valeur attendue",
|
"Expected Value": "Valeur attendue",
|
||||||
"Json Query": "Requête Json",
|
"Json Query": "Requête Json",
|
||||||
"jsonQueryDescription": "Faites une requête json contre la réponse et vérifiez la valeur attendue (la valeur de retour sera convertie en chaîne pour comparaison). Consultez {0} pour la documentation sur le langage de requête. Une aire de jeux peut être trouvée {1}.",
|
|
||||||
"Badge Duration (in hours)": "Durée du badge (en heures)",
|
"Badge Duration (in hours)": "Durée du badge (en heures)",
|
||||||
"Badge Preview": "Aperçu du badge",
|
"Badge Preview": "Aperçu du badge",
|
||||||
"aboutNotifyChannel": "Notifier le canal déclenchera une notification de bureau ou mobile pour tous les membres du canal, que leur disponibilité soit active ou absente.",
|
"aboutNotifyChannel": "Notifier le canal déclenchera une notification de bureau ou mobile pour tous les membres du canal, que leur disponibilité soit active ou absente.",
|
||||||
|
|
|
@ -682,7 +682,6 @@
|
||||||
"confirmDisableTwoFAMsg": "An bhfuil tú cinnte gur mhaith leat 2FA a dhíchumasú?",
|
"confirmDisableTwoFAMsg": "An bhfuil tú cinnte gur mhaith leat 2FA a dhíchumasú?",
|
||||||
"affectedStatusPages": "Taispeáin an teachtaireacht cothabhála seo ar leathanaigh stádais roghnaithe",
|
"affectedStatusPages": "Taispeáin an teachtaireacht cothabhála seo ar leathanaigh stádais roghnaithe",
|
||||||
"keywordDescription": "Cuardaigh eochairfhocal i ngnáthfhreagra HTML nó JSON. Tá an cuardach cás-íogair.",
|
"keywordDescription": "Cuardaigh eochairfhocal i ngnáthfhreagra HTML nó JSON. Tá an cuardach cás-íogair.",
|
||||||
"jsonQueryDescription": "Déan Iarratas json in aghaidh an fhreagra agus seiceáil an luach a bhfuiltear ag súil leis (Déanfar an luach fillte a thiontú ina theaghrán le haghaidh comparáide). Seiceáil {0} le haghaidh na gcáipéisí faoin teanga iarratais. Is féidir clós súgartha a aimsiú {1}.",
|
|
||||||
"backupDescription": "Is féidir leat gach monatóir agus fógra a chúltaca isteach i gcomhad JSON.",
|
"backupDescription": "Is féidir leat gach monatóir agus fógra a chúltaca isteach i gcomhad JSON.",
|
||||||
"backupDescription2": "Nóta: níl sonraí staire agus imeachta san áireamh.",
|
"backupDescription2": "Nóta: níl sonraí staire agus imeachta san áireamh.",
|
||||||
"octopushAPIKey": "\"Eochair API\" ó dhintiúir API HTTP sa phainéal rialaithe",
|
"octopushAPIKey": "\"Eochair API\" ó dhintiúir API HTTP sa phainéal rialaithe",
|
||||||
|
|
|
@ -799,7 +799,6 @@
|
||||||
"affectedStatusPages": "Prikazuje poruku o održavanju na odabranim statusnim stranicama",
|
"affectedStatusPages": "Prikazuje poruku o održavanju na odabranim statusnim stranicama",
|
||||||
"atLeastOneMonitor": "Odaberite barem jedan zahvaćeni Monitor",
|
"atLeastOneMonitor": "Odaberite barem jedan zahvaćeni Monitor",
|
||||||
"invertKeywordDescription": "Postavi da ključna riječ mora biti odsutna umjesto prisutna.",
|
"invertKeywordDescription": "Postavi da ključna riječ mora biti odsutna umjesto prisutna.",
|
||||||
"jsonQueryDescription": "Izvršite JSON upit nad primljenim odgovorom i provjerite očekivanu povrtanu vrijednost. Ona će se za usporedbu pretvoriti u niz znakova (string). Pogledajte stranicu {0} za dokumentaciju o jeziku upita. Testno okruženje možete pronaći {1}.",
|
|
||||||
"Strategy": "Strategija",
|
"Strategy": "Strategija",
|
||||||
"Free Mobile User Identifier": "Besplatni mobilni korisnički identifikator",
|
"Free Mobile User Identifier": "Besplatni mobilni korisnički identifikator",
|
||||||
"Free Mobile API Key": "Besplatni mobilni ključ za API",
|
"Free Mobile API Key": "Besplatni mobilni ključ za API",
|
||||||
|
|
|
@ -797,7 +797,6 @@
|
||||||
"emailTemplateHeartbeatJSON": "a szívverést leíró objektum",
|
"emailTemplateHeartbeatJSON": "a szívverést leíró objektum",
|
||||||
"emailTemplateMsg": "az értesítés üzenete",
|
"emailTemplateMsg": "az értesítés üzenete",
|
||||||
"emailTemplateLimitedToUpDownNotification": "csak FEL/LE szívverés esetén érhető el, egyébként null érték",
|
"emailTemplateLimitedToUpDownNotification": "csak FEL/LE szívverés esetén érhető el, egyébként null érték",
|
||||||
"jsonQueryDescription": "Végezzen JSON-lekérdezést a válasz alapján, és ellenőrizze a várt értéket (a visszatérési értéket a rendszer karakterlánccá alakítja az összehasonlításhoz). Nézze meg a {0} webhelyet a lekérdezés paramétereivel kapcsolatos dokumentációért. A test környezet itt található: {1}.",
|
|
||||||
"pushoverMessageTtl": "TTL üzenet (másodperc)",
|
"pushoverMessageTtl": "TTL üzenet (másodperc)",
|
||||||
"Platform": "Platform",
|
"Platform": "Platform",
|
||||||
"aboutNotifyChannel": "A Csatorna értesítése opció, értesítést fog küldeni a csatorna összes tagjának, függetlenül a tagok elérhetőségétől.",
|
"aboutNotifyChannel": "A Csatorna értesítése opció, értesítést fog küldeni a csatorna összes tagjának, függetlenül a tagok elérhetőségétől.",
|
||||||
|
|
|
@ -838,7 +838,6 @@
|
||||||
"emailTemplateHeartbeatJSON": "objek yang menggambarkan heartbeat",
|
"emailTemplateHeartbeatJSON": "objek yang menggambarkan heartbeat",
|
||||||
"emailTemplateMsg": "pesan pemberitahuan",
|
"emailTemplateMsg": "pesan pemberitahuan",
|
||||||
"emailCustomBody": "Kustomisasi Body",
|
"emailCustomBody": "Kustomisasi Body",
|
||||||
"jsonQueryDescription": "Lakukan Query json terhadap respons dan periksa nilai yang diharapkan (Nilai yang dikembalikan akan diubah menjadi string untuk perbandingan). Lihat {0} untuk dokumentasi tentang bahasa kueri. Taman bermain dapat ditemukan {1}.",
|
|
||||||
"Notify Channel": "Beritahu Saluran",
|
"Notify Channel": "Beritahu Saluran",
|
||||||
"Server URL should not contain the nfty topic": "URL server tidak boleh berisi topik nfty",
|
"Server URL should not contain the nfty topic": "URL server tidak boleh berisi topik nfty",
|
||||||
"PushDeer Server": "Server PushDeer",
|
"PushDeer Server": "Server PushDeer",
|
||||||
|
|
|
@ -620,7 +620,6 @@
|
||||||
"enableNSCD": "Abilita NSCD (Name Service Cache Daemon) per abilitare la cache su tutte le richieste DNS",
|
"enableNSCD": "Abilita NSCD (Name Service Cache Daemon) per abilitare la cache su tutte le richieste DNS",
|
||||||
"recurringIntervalMessage": "Esegui una volta al giorno | Esegui una volta ogni {0} giorni",
|
"recurringIntervalMessage": "Esegui una volta al giorno | Esegui una volta ogni {0} giorni",
|
||||||
"affectedMonitorsDescription": "Seleziona i monitoraggi che sono influenzati da questa manutenzione",
|
"affectedMonitorsDescription": "Seleziona i monitoraggi che sono influenzati da questa manutenzione",
|
||||||
"jsonQueryDescription": "Fai una query JSON verso la risposta e controlla se è presente il valore richiesto. (Il valore di ritorno verrà convertito in stringa ai fini della comparazione). Puoi controllare la documentazione su <a href='https://jsonata.org/'>jsonata.org</a> per conoscere come scrivere una query. Un area dimostrativa può essere trovata <a href='https://try.jsonata.org/'>qui</a>.",
|
|
||||||
"For safety, must use secret key": "Per sicurezza, devi usare una chiave segreta",
|
"For safety, must use secret key": "Per sicurezza, devi usare una chiave segreta",
|
||||||
"Proxy server has authentication": "Il server Proxy ha una autenticazione",
|
"Proxy server has authentication": "Il server Proxy ha una autenticazione",
|
||||||
"smseaglePriority": "Priorità messaggio (0-9, default = 0)",
|
"smseaglePriority": "Priorità messaggio (0-9, default = 0)",
|
||||||
|
|
|
@ -804,7 +804,6 @@
|
||||||
"Reconnecting...": "Opnieuw verbinden...",
|
"Reconnecting...": "Opnieuw verbinden...",
|
||||||
"Expected Value": "Verwachte waarde",
|
"Expected Value": "Verwachte waarde",
|
||||||
"Json Query": "Json zoekopdracht",
|
"Json Query": "Json zoekopdracht",
|
||||||
"jsonQueryDescription": "Voer een JSON-query uit op de respons en controleer de verwachte waarde (De retourwaarde wordt omgezet naar een string voor vergelijking). Bekijk {0} voor de documentatie over de querytaal. Een speelplaats is beschikbaar {1}.",
|
|
||||||
"pushViewCode": "Hoe gebruik je Push monitor?(View Code)",
|
"pushViewCode": "Hoe gebruik je Push monitor?(View Code)",
|
||||||
"setupDatabaseChooseDatabase": "Welke database wil je gebruiken?",
|
"setupDatabaseChooseDatabase": "Welke database wil je gebruiken?",
|
||||||
"setupDatabaseEmbeddedMariaDB": "Je hoeft niks in te stellen. Dit docker image heeft een ingebouwde en geconfigureerde MariaDB instantie. Uptime Kuma verbindt met deze database via een unix socket.",
|
"setupDatabaseEmbeddedMariaDB": "Je hoeft niks in te stellen. Dit docker image heeft een ingebouwde en geconfigureerde MariaDB instantie. Uptime Kuma verbindt met deze database via een unix socket.",
|
||||||
|
|
|
@ -793,7 +793,6 @@
|
||||||
"styleElapsedTime": "Czas, który upłynął pod paskiem bicia serca",
|
"styleElapsedTime": "Czas, który upłynął pod paskiem bicia serca",
|
||||||
"tailscalePingWarning": "Aby korzystać z monitora Tailscale Ping, należy zainstalować Uptime Kuma bez Dockera, a także zainstalować klienta Tailscale na serwerze.",
|
"tailscalePingWarning": "Aby korzystać z monitora Tailscale Ping, należy zainstalować Uptime Kuma bez Dockera, a także zainstalować klienta Tailscale na serwerze.",
|
||||||
"invertKeywordDescription": "Słowo kluczowe powinno być raczej nieobecne niż obecne.",
|
"invertKeywordDescription": "Słowo kluczowe powinno być raczej nieobecne niż obecne.",
|
||||||
"jsonQueryDescription": "Wykonaj zapytanie JSON względem odpowiedzi i sprawdź oczekiwaną wartość (wartość zwracana zostanie przekonwertowana na ciąg znaków do porównania). Sprawdź {0}, aby uzyskać dokumentację dotyczącą języka zapytań. Plac zabaw można znaleźć {1}.",
|
|
||||||
"Server URL should not contain the nfty topic": "Adres URL serwera nie powinien zawierać tematu nfty",
|
"Server URL should not contain the nfty topic": "Adres URL serwera nie powinien zawierać tematu nfty",
|
||||||
"Badge Duration (in hours)": "Czas trwania odznaki (w godzinach)",
|
"Badge Duration (in hours)": "Czas trwania odznaki (w godzinach)",
|
||||||
"Enter the list of brokers": "Wprowadź listę brokerów",
|
"Enter the list of brokers": "Wprowadź listę brokerów",
|
||||||
|
|
|
@ -605,7 +605,6 @@
|
||||||
"wayToGetLineChannelToken": "Primeiro acesse o {0}, crie um provedor e um canal (API de Mensagens), então você pode obter o token de acesso do canal e o ID do usuário nos itens de menu mencionados acima.",
|
"wayToGetLineChannelToken": "Primeiro acesse o {0}, crie um provedor e um canal (API de Mensagens), então você pode obter o token de acesso do canal e o ID do usuário nos itens de menu mencionados acima.",
|
||||||
"aboutMattermostChannelName": "Você pode substituir o canal padrão para o qual o Webhook envia postagens, inserindo o nome do canal no campo \"Nome do Canal\". Isso precisa ser habilitado nas configurações do Webhook do Mattermost. Por exemplo: #outro-canal",
|
"aboutMattermostChannelName": "Você pode substituir o canal padrão para o qual o Webhook envia postagens, inserindo o nome do canal no campo \"Nome do Canal\". Isso precisa ser habilitado nas configurações do Webhook do Mattermost. Por exemplo: #outro-canal",
|
||||||
"invertKeywordDescription": "Procure pela palavra-chave estar ausente em vez de presente.",
|
"invertKeywordDescription": "Procure pela palavra-chave estar ausente em vez de presente.",
|
||||||
"jsonQueryDescription": "Faça uma consulta JSON na resposta e verifique o valor esperado (o valor de retorno será convertido em uma string para comparação). Confira {0} para a documentação sobre a linguagem de consulta. Você pode encontrar um playground {1}.",
|
|
||||||
"octopushTypePremium": "Premium (Rápido - recomendado para alertas)",
|
"octopushTypePremium": "Premium (Rápido - recomendado para alertas)",
|
||||||
"octopushTypeLowCost": "Baixo Custo (Lento - às vezes bloqueado pelo operador)",
|
"octopushTypeLowCost": "Baixo Custo (Lento - às vezes bloqueado pelo operador)",
|
||||||
"octopushSMSSender": "Nome do Remetente de SMS: 3-11 caracteres alfanuméricos e espaço (a-zA-Z0-9)",
|
"octopushSMSSender": "Nome do Remetente de SMS: 3-11 caracteres alfanuméricos e espaço (a-zA-Z0-9)",
|
||||||
|
|
|
@ -689,7 +689,6 @@
|
||||||
"emailTemplateLimitedToUpDownNotification": "disponibil numai pentru heartbeat-uri UP/DOWN, altfel nul",
|
"emailTemplateLimitedToUpDownNotification": "disponibil numai pentru heartbeat-uri UP/DOWN, altfel nul",
|
||||||
"emailTemplateStatus": "Stare",
|
"emailTemplateStatus": "Stare",
|
||||||
"invertKeywordDescription": "Căutați după cuvântul cheie să fie absent și nu prezent.",
|
"invertKeywordDescription": "Căutați după cuvântul cheie să fie absent și nu prezent.",
|
||||||
"jsonQueryDescription": "Efectuați o interogare json după răspuns și verificați valoarea așteptată (valoarea returnată va fi convertită în șir pentru comparație). Consultați {0} pentru documentația despre limbajul de interogare. Un playground poate fi găsit {1}.",
|
|
||||||
"goAlertInfo": "GoAlert este o aplicație open source pentru programarea apelurilor, escalări automate și notificări (cum ar fi SMS-uri sau apeluri vocale). Angajați automat persoana potrivită, în modul potrivit și la momentul potrivit! {0}",
|
"goAlertInfo": "GoAlert este o aplicație open source pentru programarea apelurilor, escalări automate și notificări (cum ar fi SMS-uri sau apeluri vocale). Angajați automat persoana potrivită, în modul potrivit și la momentul potrivit! {0}",
|
||||||
"goAlertIntegrationKeyInfo": "Obțineți cheia generică de integrare API pentru serviciu în formatul \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" de obicei valoarea parametrului token al URL-ului copiat.",
|
"goAlertIntegrationKeyInfo": "Obțineți cheia generică de integrare API pentru serviciu în formatul \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" de obicei valoarea parametrului token al URL-ului copiat.",
|
||||||
"SecretAccessKey": "Secret AccessKey",
|
"SecretAccessKey": "Secret AccessKey",
|
||||||
|
|
|
@ -812,7 +812,6 @@
|
||||||
"AccessKey Id": "AccessKey Id",
|
"AccessKey Id": "AccessKey Id",
|
||||||
"Secret AccessKey": "Секретный ключ доступа",
|
"Secret AccessKey": "Секретный ключ доступа",
|
||||||
"Session Token": "Токен сессии",
|
"Session Token": "Токен сессии",
|
||||||
"jsonQueryDescription": "Выполните json-запрос к ответу и проверьте наличие ожидаемого значения (возвращаемое значение будет преобразовано в строку для сравнения). Посмотрите {0} для получения документации по языку запросов. A Потренироваться вы можете {1}.",
|
|
||||||
"Notify Channel": "Канал оповещений",
|
"Notify Channel": "Канал оповещений",
|
||||||
"aboutNotifyChannel": "Уведомление о канале вызовет настольное или мобильное уведомление для всех участников канала, независимо от того, установлена ли их доступность как активная или отсутствующая.",
|
"aboutNotifyChannel": "Уведомление о канале вызовет настольное или мобильное уведомление для всех участников канала, независимо от того, установлена ли их доступность как активная или отсутствующая.",
|
||||||
"Enter the list of brokers": "Введите список брокеров",
|
"Enter the list of brokers": "Введите список брокеров",
|
||||||
|
|
|
@ -795,7 +795,6 @@
|
||||||
"pushoverDesc1": "Nödprioritet (2) har 30 sekunders timeout mellan försök och löper ut efter 1 timme som standard.",
|
"pushoverDesc1": "Nödprioritet (2) har 30 sekunders timeout mellan försök och löper ut efter 1 timme som standard.",
|
||||||
"octopushTypePremium": "Premium (Snabb - rekommenderas för varningar)",
|
"octopushTypePremium": "Premium (Snabb - rekommenderas för varningar)",
|
||||||
"octopushTypeLowCost": "Låg kostnad (långsam - blockeras ibland av operatören)",
|
"octopushTypeLowCost": "Låg kostnad (långsam - blockeras ibland av operatören)",
|
||||||
"jsonQueryDescription": "Gör en json-förfrågan mot svaret och kontrollera det förväntade värdet (returvärde konverteras till en sträng för jämförelse). Se {0} för dokumentation angående frågespråket. En lekplats kan hittas här {1}.",
|
|
||||||
"Check octopush prices": "Kontrollera octopush priser {0}.",
|
"Check octopush prices": "Kontrollera octopush priser {0}.",
|
||||||
"octopushSMSSender": "SMS avsändarnamn: 3-11 alfanumeriska tecken och mellanslag (a-zA-Z0-9)",
|
"octopushSMSSender": "SMS avsändarnamn: 3-11 alfanumeriska tecken och mellanslag (a-zA-Z0-9)",
|
||||||
"LunaSea Device ID": "LunaSea enhetsid",
|
"LunaSea Device ID": "LunaSea enhetsid",
|
||||||
|
|
|
@ -794,7 +794,6 @@
|
||||||
"webhookBodyPresetOption": "Ön ayar - {0}",
|
"webhookBodyPresetOption": "Ön ayar - {0}",
|
||||||
"webhookBodyCustomOption": "Özel Gövde",
|
"webhookBodyCustomOption": "Özel Gövde",
|
||||||
"Request Body": "İstek Gövdesi",
|
"Request Body": "İstek Gövdesi",
|
||||||
"jsonQueryDescription": "Yanıta karşı bir json sorgusu yapın ve beklenen değeri kontrol edin (Dönüş değeri, karşılaştırma için dizgeye dönüştürülür). Sorgu diliyle ilgili belgeler için {0}'a bakın. Bir oyun alanı {1} bulunabilir.",
|
|
||||||
"twilioApiKey": "Api Anahtarı (isteğe bağlı)",
|
"twilioApiKey": "Api Anahtarı (isteğe bağlı)",
|
||||||
"Expected Value": "Beklenen Değer",
|
"Expected Value": "Beklenen Değer",
|
||||||
"Json Query": "Json Sorgusu",
|
"Json Query": "Json Sorgusu",
|
||||||
|
|
|
@ -802,7 +802,6 @@
|
||||||
"Request Body": "Тіло запиту",
|
"Request Body": "Тіло запиту",
|
||||||
"Badge Preview": "Попередній перегляд бейджа",
|
"Badge Preview": "Попередній перегляд бейджа",
|
||||||
"Badge Duration (in hours)": "Тривалість бейджа (у годинах)",
|
"Badge Duration (in hours)": "Тривалість бейджа (у годинах)",
|
||||||
"jsonQueryDescription": "Виконувати JSON-запит до відповіді та перевірити очікуване значення (значення, що повертається, буде перетворено в рядок для порівняння). Зверніться до {0} щоб ознайомитися з документацією про мову запитів. Навчальний майданчик можна знайти {1}.",
|
|
||||||
"twilioApiKey": "Api ключ (необов'язково)",
|
"twilioApiKey": "Api ключ (необов'язково)",
|
||||||
"Expected Value": "Очікуване значення",
|
"Expected Value": "Очікуване значення",
|
||||||
"Json Query": "Json-запит",
|
"Json Query": "Json-запит",
|
||||||
|
|
|
@ -802,7 +802,6 @@
|
||||||
"webhookCustomBodyDesc": "为 webhook 设定一个自定义 HTTP 请求体。可在模板内使用 {msg}、{heartbeat}和{monitor} 变量。",
|
"webhookCustomBodyDesc": "为 webhook 设定一个自定义 HTTP 请求体。可在模板内使用 {msg}、{heartbeat}和{monitor} 变量。",
|
||||||
"webhookBodyPresetOption": "预设 - {0}",
|
"webhookBodyPresetOption": "预设 - {0}",
|
||||||
"Request Body": "请求体",
|
"Request Body": "请求体",
|
||||||
"jsonQueryDescription": "对响应结果执行一次 JSON 查询,其返回值将会被转换为字符串,再与期望值进行比较。可访问 {0} 阅读 JSON 查询语言的文档,或在{1}测试查询语句。",
|
|
||||||
"Json Query": "JSON 查询",
|
"Json Query": "JSON 查询",
|
||||||
"twilioApiKey": "API Key(可选)",
|
"twilioApiKey": "API Key(可选)",
|
||||||
"Expected Value": "预期值",
|
"Expected Value": "预期值",
|
||||||
|
|
|
@ -772,7 +772,6 @@
|
||||||
"Check/Uncheck": "選中/取消選中",
|
"Check/Uncheck": "選中/取消選中",
|
||||||
"tailscalePingWarning": "如需使用 Tailscale Ping 客戶端,您需要以非 docker 方式安裝 Uptime Kuma,並同時安裝 Tailscale 客戶端。",
|
"tailscalePingWarning": "如需使用 Tailscale Ping 客戶端,您需要以非 docker 方式安裝 Uptime Kuma,並同時安裝 Tailscale 客戶端。",
|
||||||
"invertKeywordDescription": "出現關鍵詞將令檢測結果設為失敗,而非成功。",
|
"invertKeywordDescription": "出現關鍵詞將令檢測結果設為失敗,而非成功。",
|
||||||
"jsonQueryDescription": "對回應結果執行一次 JSON 查詢,其返回值將會被轉換為字串,再與期望值進行比較。可造訪{0}閱讀JSON 查詢語言的文件,或在{1}測試查詢語句。",
|
|
||||||
"wayToGetKookGuildID": "在 Kook 設定中打開“開發者模式”,然後右鍵點選頻道可取得其 ID",
|
"wayToGetKookGuildID": "在 Kook 設定中打開“開發者模式”,然後右鍵點選頻道可取得其 ID",
|
||||||
"Notify Channel": "通知該頻道",
|
"Notify Channel": "通知該頻道",
|
||||||
"aboutNotifyChannel": "勾選“通知該頻道”,會令該頻道內所有成員都收到一條桌面端或移動端通知,無論其狀態是在線或離開。",
|
"aboutNotifyChannel": "勾選“通知該頻道”,會令該頻道內所有成員都收到一條桌面端或移動端通知,無論其狀態是在線或離開。",
|
||||||
|
|
|
@ -38,6 +38,7 @@ export default {
|
||||||
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
monitorList: { },
|
monitorList: { },
|
||||||
|
monitorTypeList: {},
|
||||||
maintenanceList: {},
|
maintenanceList: {},
|
||||||
apiKeyList: {},
|
apiKeyList: {},
|
||||||
heartbeatList: { },
|
heartbeatList: { },
|
||||||
|
@ -153,6 +154,10 @@ export default {
|
||||||
this.monitorList = data;
|
this.monitorList = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("monitorTypeList", (data) => {
|
||||||
|
this.monitorTypeList = data;
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("maintenanceList", (data) => {
|
socket.on("maintenanceList", (data) => {
|
||||||
this.maintenanceList = data;
|
this.maintenanceList = data;
|
||||||
});
|
});
|
||||||
|
@ -251,7 +256,7 @@ export default {
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
console.log("disconnect");
|
console.log("disconnect");
|
||||||
this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
|
this.connectionErrorMsg = `${this.$t("Lost connection to the socket server.")} ${this.$t("Reconnecting...")}`;
|
||||||
this.socket.connected = false;
|
this.socket.connected = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}">
|
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}">
|
||||||
<td><router-link :to="`/dashboard/${beat.monitorID}`">{{ $root.monitorList[beat.monitorID]?.name }}</router-link></td>
|
<td class="name-column"><router-link :to="`/dashboard/${beat.monitorID}`">{{ $root.monitorList[beat.monitorID]?.name }}</router-link></td>
|
||||||
<td><Status :status="beat.status" /></td>
|
<td><Status :status="beat.status" /></td>
|
||||||
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
|
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
|
||||||
<td class="border-0">{{ beat.msg }}</td>
|
<td class="border-0">{{ beat.msg }}</td>
|
||||||
|
@ -233,4 +233,16 @@ table {
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1280px) {
|
||||||
|
.name-column {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-aspect-ratio: 4/3) {
|
||||||
|
.name-column {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
<span class="word">{{ $t("checkEverySecond", [ monitor.interval ]) }}</span>
|
<span class="word">{{ $t("checkEverySecond", [ monitor.interval ]) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px;">{{ status.text }}</span>
|
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px;" data-testid="monitor-status">{{ status.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
|
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
|
||||||
<select id="type" v-model="monitor.type" class="form-select">
|
<select id="type" v-model="monitor.type" class="form-select" data-testid="monitor-type-select">
|
||||||
<optgroup :label="$t('General Monitor Type')">
|
<optgroup :label="$t('General Monitor Type')">
|
||||||
<option value="group">
|
<option value="group">
|
||||||
{{ $t("Group") }}
|
{{ $t("Group") }}
|
||||||
|
@ -24,6 +24,9 @@
|
||||||
<option value="ping">
|
<option value="ping">
|
||||||
Ping
|
Ping
|
||||||
</option>
|
</option>
|
||||||
|
<option value="snmp">
|
||||||
|
SNMP
|
||||||
|
</option>
|
||||||
<option value="keyword">
|
<option value="keyword">
|
||||||
HTTP(s) - {{ $t("Keyword") }}
|
HTTP(s) - {{ $t("Keyword") }}
|
||||||
</option>
|
</option>
|
||||||
|
@ -96,7 +99,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>
|
<input id="name" v-model="monitor.name" type="text" class="form-control" required data-testid="friendly-name-input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- URL -->
|
<!-- URL -->
|
||||||
|
@ -168,21 +171,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Json Query -->
|
|
||||||
<div v-if="monitor.type === 'json-query'" class="my-3">
|
|
||||||
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
|
|
||||||
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" required>
|
|
||||||
|
|
||||||
<i18n-t tag="div" class="form-text" keypath="jsonQueryDescription">
|
|
||||||
<a href="https://jsonata.org/">jsonata.org</a>
|
|
||||||
<a href="https://try.jsonata.org/">{{ $t('here') }}</a>
|
|
||||||
</i18n-t>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
|
|
||||||
<input id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Game -->
|
<!-- Game -->
|
||||||
<!-- GameDig only -->
|
<!-- GameDig only -->
|
||||||
<div v-if="monitor.type === 'gamedig'" class="my-3">
|
<div v-if="monitor.type === 'gamedig'" class="my-3">
|
||||||
|
@ -246,19 +234,87 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Hostname -->
|
<!-- Hostname -->
|
||||||
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping only -->
|
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / SNMP 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'" 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 === 'snmp'" class="my-3">
|
||||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||||
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`" required>
|
<input
|
||||||
|
id="hostname"
|
||||||
|
v-model="monitor.hostname"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`"
|
||||||
|
required
|
||||||
|
data-testid="hostname-input"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Port -->
|
<!-- Port -->
|
||||||
<!-- For TCP Port / Steam / MQTT / Radius Type -->
|
<!-- 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'" class="my-3">
|
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || 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>
|
||||||
|
|
||||||
|
<!-- SNMP Monitor Type -->
|
||||||
|
<div v-if="monitor.type === 'snmp'" class="my-3">
|
||||||
|
<label for="snmp_community_string" class="form-label">{{ $t("Community String") }}</label>
|
||||||
|
<!-- TODO: Rename monitor.radiusPassword to monitor.password for general use -->
|
||||||
|
<HiddenInput id="snmp_community_string" v-model="monitor.radiusPassword" autocomplete="false" required="true" placeholder="public"></HiddenInput>
|
||||||
|
|
||||||
|
<div class="form-text">{{ $t('snmpCommunityStringHelptext') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="monitor.type === 'snmp'" class="my-3">
|
||||||
|
<label for="snmp_oid" class="form-label">{{ $t("OID (Object Identifier)") }}</label>
|
||||||
|
<input id="snmp_oid" v-model="monitor.snmpOid" :title="$t('Please enter a valid OID.') + ' ' + $t('Example:', ['1.3.6.1.4.1.9.6.1.101'])" type="text" class="form-control" pattern="^([0-2])((\.0)|(\.[1-9][0-9]*))*$" placeholder="1.3.6.1.4.1.9.6.1.101" required>
|
||||||
|
<div class="form-text">{{ $t('snmpOIDHelptext') }} </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="monitor.type === 'snmp'" class="my-3">
|
||||||
|
<label for="snmp_version" class="form-label">{{ $t("SNMP Version") }}</label>
|
||||||
|
<select id="snmp_version" v-model="monitor.snmpVersion" class="form-select">
|
||||||
|
<option value="1">
|
||||||
|
SNMPv1
|
||||||
|
</option>
|
||||||
|
<option value="2c">
|
||||||
|
SNMPv2c
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Json Query -->
|
||||||
|
<!-- For Json Query / SNMP -->
|
||||||
|
<div v-if="monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3">
|
||||||
|
<div class="my-2">
|
||||||
|
<label for="jsonPath" class="form-label mb-0">{{ $t("Json Query Expression") }}</label>
|
||||||
|
<i18n-t tag="div" class="form-text mb-2" keypath="jsonQueryDescription">
|
||||||
|
<a href="https://jsonata.org/">jsonata.org</a>
|
||||||
|
<a href="https://try.jsonata.org/">{{ $t('playground') }}</a>
|
||||||
|
</i18n-t>
|
||||||
|
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" placeholder="$" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<div class="me-2">
|
||||||
|
<label for="json_path_operator" class="form-label">{{ $t("Condition") }}</label>
|
||||||
|
<select id="json_path_operator" v-model="monitor.jsonPathOperator" class="form-select me-3" required>
|
||||||
|
<option value=">">></option>
|
||||||
|
<option value=">=">>=</option>
|
||||||
|
<option value="<"><</option>
|
||||||
|
<option value="<="><=</option>
|
||||||
|
<option value="!=">!=</option>
|
||||||
|
<option value="==">==</option>
|
||||||
|
<option value="contains">contains</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
|
||||||
|
<input v-if="monitor.jsonPathOperator !== 'contains' && monitor.jsonPathOperator !== '==' && monitor.jsonPathOperator !== '!='" id="expectedValue" v-model="monitor.expectedValue" type="number" class="form-control" required step=".01">
|
||||||
|
<input v-else id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- DNS Resolver Server -->
|
<!-- DNS Resolver Server -->
|
||||||
<!-- For DNS Type -->
|
<!-- For DNS Type -->
|
||||||
<template v-if="monitor.type === 'dns'">
|
<template v-if="monitor.type === 'dns'">
|
||||||
|
@ -295,6 +351,7 @@
|
||||||
:preselect-first="false"
|
:preselect-first="false"
|
||||||
:max-height="500"
|
:max-height="500"
|
||||||
:taggable="false"
|
:taggable="false"
|
||||||
|
data-testid="resolve-type-select"
|
||||||
></VueMultiselect>
|
></VueMultiselect>
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
|
@ -461,6 +518,14 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Conditions -->
|
||||||
|
<EditMonitorConditions
|
||||||
|
v-if="supportsConditions && conditionVariables.length > 0"
|
||||||
|
v-model="monitor.conditions"
|
||||||
|
:condition-variables="conditionVariables"
|
||||||
|
class="my-3"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Interval -->
|
<!-- Interval -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
|
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
|
||||||
|
@ -483,8 +548,8 @@
|
||||||
<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 only -->
|
<!-- Timeout: HTTP / Keyword / SNMP only -->
|
||||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query'" class="my-3">
|
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || 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">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label>
|
||||||
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
|
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
|
||||||
</div>
|
</div>
|
||||||
|
@ -516,6 +581,18 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check">
|
||||||
|
<input id="cache-bust" v-model="monitor.cacheBust" class="form-check-input" type="checkbox" value="">
|
||||||
|
<label class="form-check-label" for="cache-bust">
|
||||||
|
<i18n-t tag="label" keypath="cacheBusterParam" class="form-check-label" for="cache-bust">
|
||||||
|
<code>uptime_kuma_cachebuster</code>
|
||||||
|
</i18n-t>
|
||||||
|
</label>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("cacheBusterParamDescription") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="my-3 form-check">
|
<div class="my-3 form-check">
|
||||||
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
|
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
|
||||||
<label class="form-check-label" for="upside-down">
|
<label class="form-check-label" for="upside-down">
|
||||||
|
@ -903,7 +980,15 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fixed-bottom-bar p-3">
|
<div class="fixed-bottom-bar p-3">
|
||||||
<button id="monitor-submit-btn" class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
|
<button
|
||||||
|
id="monitor-submit-btn"
|
||||||
|
class="btn btn-primary"
|
||||||
|
type="submit"
|
||||||
|
:disabled="processing"
|
||||||
|
data-testid="save-button"
|
||||||
|
>
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -912,7 +997,7 @@
|
||||||
<DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
|
<DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
|
||||||
<ProxyDialog ref="proxyDialog" @added="addedProxy" />
|
<ProxyDialog ref="proxyDialog" @added="addedProxy" />
|
||||||
<CreateGroupDialog ref="createGroupDialog" @added="addedDraftGroup" />
|
<CreateGroupDialog ref="createGroupDialog" @added="addedDraftGroup" />
|
||||||
<RemoteBrowserDialog ref="remoteBrowserDialog" @added="addedRemoteBrowser" />
|
<RemoteBrowserDialog ref="remoteBrowserDialog" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
@ -931,6 +1016,7 @@ 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";
|
||||||
|
|
||||||
const toast = useToast;
|
const toast = useToast;
|
||||||
|
|
||||||
|
@ -946,7 +1032,6 @@ const monitorDefaults = {
|
||||||
retryInterval: 60,
|
retryInterval: 60,
|
||||||
resendInterval: 0,
|
resendInterval: 0,
|
||||||
maxretries: 0,
|
maxretries: 0,
|
||||||
timeout: 48,
|
|
||||||
notificationIDList: {},
|
notificationIDList: {},
|
||||||
ignoreTls: false,
|
ignoreTls: false,
|
||||||
upsideDown: false,
|
upsideDown: false,
|
||||||
|
@ -971,10 +1056,12 @@ const monitorDefaults = {
|
||||||
kafkaProducerSaslOptions: {
|
kafkaProducerSaslOptions: {
|
||||||
mechanism: "None",
|
mechanism: "None",
|
||||||
},
|
},
|
||||||
|
cacheBust: false,
|
||||||
kafkaProducerSsl: false,
|
kafkaProducerSsl: false,
|
||||||
kafkaProducerAllowAutoTopicCreation: false,
|
kafkaProducerAllowAutoTopicCreation: false,
|
||||||
gamedigGivenPortOnly: true,
|
gamedigGivenPortOnly: true,
|
||||||
remote_browser: null
|
remote_browser: null,
|
||||||
|
conditions: []
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -989,6 +1076,7 @@ export default {
|
||||||
RemoteBrowserDialog,
|
RemoteBrowserDialog,
|
||||||
TagsManager,
|
TagsManager,
|
||||||
VueMultiselect,
|
VueMultiselect,
|
||||||
|
EditMonitorConditions,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -1243,7 +1331,15 @@ message HealthCheckResponse {
|
||||||
value: null,
|
value: null,
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
supportsConditions() {
|
||||||
|
return this.$root.monitorTypeList[this.monitor.type]?.supportsConditions || false;
|
||||||
|
},
|
||||||
|
|
||||||
|
conditionVariables() {
|
||||||
|
return this.$root.monitorTypeList[this.monitor.type]?.conditionVariables || [];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"$root.proxyList"() {
|
"$root.proxyList"() {
|
||||||
|
@ -1276,7 +1372,7 @@ message HealthCheckResponse {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"monitor.type"() {
|
"monitor.type"(newType, oldType) {
|
||||||
if (this.monitor.type === "push") {
|
if (this.monitor.type === "push") {
|
||||||
if (! this.monitor.pushToken) {
|
if (! this.monitor.pushToken) {
|
||||||
// ideally this would require checking if the generated token is already used
|
// ideally this would require checking if the generated token is already used
|
||||||
|
@ -1291,11 +1387,35 @@ message HealthCheckResponse {
|
||||||
this.monitor.port = "53";
|
this.monitor.port = "53";
|
||||||
} else if (this.monitor.type === "radius") {
|
} else if (this.monitor.type === "radius") {
|
||||||
this.monitor.port = "1812";
|
this.monitor.port = "1812";
|
||||||
|
} else if (this.monitor.type === "snmp") {
|
||||||
|
this.monitor.port = "161";
|
||||||
} else {
|
} else {
|
||||||
this.monitor.port = undefined;
|
this.monitor.port = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.monitor.type === "snmp") {
|
||||||
|
// snmp is not expected to be executed via the internet => we can choose a lower default timeout
|
||||||
|
this.monitor.timeout = 5;
|
||||||
|
} else {
|
||||||
|
this.monitor.timeout = 48;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default SNMP version
|
||||||
|
if (!this.monitor.snmpVersion) {
|
||||||
|
this.monitor.snmpVersion = "2c";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default jsonPath
|
||||||
|
if (!this.monitor.jsonPath) {
|
||||||
|
this.monitor.jsonPath = "$";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default condition for for jsonPathOperator
|
||||||
|
if (!this.monitor.jsonPathOperator) {
|
||||||
|
this.monitor.jsonPathOperator = "==";
|
||||||
|
}
|
||||||
|
|
||||||
// Get the game list from server
|
// Get the game list from server
|
||||||
if (this.monitor.type === "gamedig") {
|
if (this.monitor.type === "gamedig") {
|
||||||
this.$root.getSocket().emit("getGameList", (res) => {
|
this.$root.getSocket().emit("getGameList", (res) => {
|
||||||
|
@ -1324,6 +1444,10 @@ message HealthCheckResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset conditions since condition variables likely change:
|
||||||
|
if (oldType && newType !== oldType) {
|
||||||
|
this.monitor.conditions = [];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
currentGameObject(newGameObject, previousGameObject) {
|
currentGameObject(newGameObject, previousGameObject) {
|
||||||
|
|
59
src/util.js
59
src/util.js
|
@ -14,8 +14,11 @@ var __importDefault = (this && this.__importDefault) || function (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.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.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.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.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_1 = __importDefault(require("dayjs"));
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const jsonata = require("jsonata");
|
||||||
exports.isDev = process.env.NODE_ENV === "development";
|
exports.isDev = process.env.NODE_ENV === "development";
|
||||||
exports.isNode = typeof process !== "undefined" && ((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node);
|
exports.isNode = typeof process !== "undefined" && ((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node);
|
||||||
exports.appName = "Uptime Kuma";
|
exports.appName = "Uptime Kuma";
|
||||||
|
@ -399,3 +402,59 @@ function intHash(str, length = 10) {
|
||||||
return (hash % length + length) % length;
|
return (hash % length + length) % length;
|
||||||
}
|
}
|
||||||
exports.intHash = intHash;
|
exports.intHash = intHash;
|
||||||
|
async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue) {
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = JSON.parse(data);
|
||||||
|
}
|
||||||
|
catch (_a) {
|
||||||
|
response = (typeof data === "object" || typeof data === "number") && !Buffer.isBuffer(data) ? data : data.toString();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
response = (jsonPath) ? await jsonata(jsonPath).evaluate(response) : response;
|
||||||
|
if (response === null || response === undefined) {
|
||||||
|
throw new Error("Empty or undefined response. Check query syntax and response structure");
|
||||||
|
}
|
||||||
|
if (typeof response === "object" || response instanceof Date || typeof response === "function") {
|
||||||
|
throw new Error(`The post-JSON query evaluated response from the server is of type ${typeof response}, which cannot be directly compared to the expected value`);
|
||||||
|
}
|
||||||
|
let jsonQueryExpression;
|
||||||
|
switch (jsonPathOperator) {
|
||||||
|
case ">":
|
||||||
|
case ">=":
|
||||||
|
case "<":
|
||||||
|
case "<=":
|
||||||
|
jsonQueryExpression = `$number($.value) ${jsonPathOperator} $number($.expected)`;
|
||||||
|
break;
|
||||||
|
case "!=":
|
||||||
|
jsonQueryExpression = "$.value != $.expected";
|
||||||
|
break;
|
||||||
|
case "==":
|
||||||
|
jsonQueryExpression = "$.value = $.expected";
|
||||||
|
break;
|
||||||
|
case "contains":
|
||||||
|
jsonQueryExpression = "$contains($.value, $.expected)";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid condition ${jsonPathOperator}`);
|
||||||
|
}
|
||||||
|
const expression = jsonata(jsonQueryExpression);
|
||||||
|
const status = await expression.evaluate({
|
||||||
|
value: response.toString(),
|
||||||
|
expected: expectedValue.toString()
|
||||||
|
});
|
||||||
|
if (status === undefined) {
|
||||||
|
throw new Error("Query evaluation returned undefined. Check query syntax and the structure of the response data");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
response
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
response = JSON.stringify(response);
|
||||||
|
response = (response && response.length > 50) ? `${response.substring(0, 100)}… (truncated)` : response;
|
||||||
|
throw new Error(`Error evaluating JSON query: ${err.message}. Response from server was: ${response}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.evaluateJsonQuery = evaluateJsonQuery;
|
||||||
|
|
75
src/util.ts
75
src/util.ts
|
@ -17,6 +17,8 @@ import * as timezone from "dayjs/plugin/timezone";
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
import * as utc from "dayjs/plugin/utc";
|
import * as utc from "dayjs/plugin/utc";
|
||||||
|
|
||||||
|
import * as jsonata from "jsonata";
|
||||||
|
|
||||||
export const isDev = process.env.NODE_ENV === "development";
|
export const isDev = process.env.NODE_ENV === "development";
|
||||||
export const isNode = typeof process !== "undefined" && process?.versions?.node;
|
export const isNode = typeof process !== "undefined" && process?.versions?.node;
|
||||||
export const appName = "Uptime Kuma";
|
export const appName = "Uptime Kuma";
|
||||||
|
@ -643,3 +645,76 @@ export function intHash(str : string, length = 10) : number {
|
||||||
return (hash % length + length) % length; // Ensure the result is non-negative
|
return (hash % length + length) % length; // Ensure the result is non-negative
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate a JSON query expression against the provided data.
|
||||||
|
* @param data The data to evaluate the JSON query against.
|
||||||
|
* @param jsonPath The JSON path or custom JSON query expression.
|
||||||
|
* @param jsonPathOperator The operator to use for comparison.
|
||||||
|
* @param expectedValue The expected value to compare against.
|
||||||
|
* @returns An object containing the status and the evaluation result.
|
||||||
|
* @throws Error if the evaluation returns undefined.
|
||||||
|
*/
|
||||||
|
export async function evaluateJsonQuery(data: any, jsonPath: string, jsonPathOperator: string, expectedValue: any): Promise<{ status: boolean; response: any }> {
|
||||||
|
// Attempt to parse data as JSON; if unsuccessful, handle based on data type.
|
||||||
|
let response: any;
|
||||||
|
try {
|
||||||
|
response = JSON.parse(data);
|
||||||
|
} catch {
|
||||||
|
response = (typeof data === "object" || typeof data === "number") && !Buffer.isBuffer(data) ? data : data.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If a JSON path is provided, pre-evaluate the data using it.
|
||||||
|
response = (jsonPath) ? await jsonata(jsonPath).evaluate(response) : response;
|
||||||
|
|
||||||
|
if (response === null || response === undefined) {
|
||||||
|
throw new Error("Empty or undefined response. Check query syntax and response structure");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof response === "object" || response instanceof Date || typeof response === "function") {
|
||||||
|
throw new Error(`The post-JSON query evaluated response from the server is of type ${typeof response}, which cannot be directly compared to the expected value`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the comparison logic using the chosen operator
|
||||||
|
let jsonQueryExpression;
|
||||||
|
switch (jsonPathOperator) {
|
||||||
|
case ">":
|
||||||
|
case ">=":
|
||||||
|
case "<":
|
||||||
|
case "<=":
|
||||||
|
jsonQueryExpression = `$number($.value) ${jsonPathOperator} $number($.expected)`;
|
||||||
|
break;
|
||||||
|
case "!=":
|
||||||
|
jsonQueryExpression = "$.value != $.expected";
|
||||||
|
break;
|
||||||
|
case "==":
|
||||||
|
jsonQueryExpression = "$.value = $.expected";
|
||||||
|
break;
|
||||||
|
case "contains":
|
||||||
|
jsonQueryExpression = "$contains($.value, $.expected)";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid condition ${jsonPathOperator}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate the JSON Query Expression
|
||||||
|
const expression = jsonata(jsonQueryExpression);
|
||||||
|
const status = await expression.evaluate({
|
||||||
|
value: response.toString(),
|
||||||
|
expected: expectedValue.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status === undefined) {
|
||||||
|
throw new Error("Query evaluation returned undefined. Check query syntax and the structure of the response data");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status, // The evaluation of the json query
|
||||||
|
response // The response from the server or result from initial json-query evaluation
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
response = JSON.stringify(response); // Ensure the response is treated as a string for the console
|
||||||
|
response = (response && response.length > 50) ? `${response.substring(0, 100)}… (truncated)` : response;// Truncate long responses to the console
|
||||||
|
throw new Error(`Error evaluating JSON query: ${err.message}. Response from server was: ${response}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
// Check Node.js version
|
|
||||||
const semver = require("semver");
|
|
||||||
const childProcess = require("child_process");
|
|
||||||
|
|
||||||
const nodeVersion = process.versions.node;
|
|
||||||
console.log("Node.js version: " + nodeVersion);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Node.js version >= 18
|
|
||||||
if (semver.satisfies(nodeVersion, ">= 18")) {
|
|
||||||
console.log("Use the native test runner: `node --test`");
|
|
||||||
childProcess.execSync("npm run test-backend:18", { stdio: "inherit" });
|
|
||||||
} else {
|
|
||||||
// 14 - 16 here
|
|
||||||
console.log("Use `test` package: `node--test`")
|
|
||||||
childProcess.execSync("npm run test-backend:14", { stdio: "inherit" });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -7,15 +7,7 @@ Create a test file in this directory with the name `*.js`.
|
||||||
## Template
|
## Template
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const semver = require("semver");
|
const test = require("node:test");
|
||||||
let test;
|
|
||||||
const nodeVersion = process.versions.node;
|
|
||||||
if (semver.satisfies(nodeVersion, ">= 18")) {
|
|
||||||
test = require("node:test");
|
|
||||||
} else {
|
|
||||||
test = require("test");
|
|
||||||
}
|
|
||||||
|
|
||||||
const assert = require("node:assert");
|
const assert = require("node:assert");
|
||||||
|
|
||||||
test("Test name", async (t) => {
|
test("Test name", async (t) => {
|
||||||
|
@ -25,14 +17,6 @@ test("Test name", async (t) => {
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
Node.js >=18
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run test-backend:18
|
npm run test-backend
|
||||||
```
|
|
||||||
|
|
||||||
Node.js < 18
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run test-backend:14
|
|
||||||
```
|
```
|
||||||
|
|
46
test/backend-test/monitor-conditions/test-evaluator.js
Normal file
46
test/backend-test/monitor-conditions/test-evaluator.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("../../../server/monitor-conditions/expression.js");
|
||||||
|
const { evaluateExpressionGroup, evaluateExpression } = require("../../../server/monitor-conditions/evaluator.js");
|
||||||
|
|
||||||
|
test("Test evaluateExpression", async (t) => {
|
||||||
|
const expr = new ConditionExpression("record", "contains", "mx1.example.com");
|
||||||
|
assert.strictEqual(true, evaluateExpression(expr, { record: "mx1.example.com" }));
|
||||||
|
assert.strictEqual(false, evaluateExpression(expr, { record: "mx2.example.com" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test evaluateExpressionGroup with logical AND", async (t) => {
|
||||||
|
const group = new ConditionExpressionGroup([
|
||||||
|
new ConditionExpression("record", "contains", "mx1."),
|
||||||
|
new ConditionExpression("record", "contains", "example.com", LOGICAL.AND),
|
||||||
|
]);
|
||||||
|
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" }));
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." }));
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test evaluateExpressionGroup with logical OR", async (t) => {
|
||||||
|
const group = new ConditionExpressionGroup([
|
||||||
|
new ConditionExpression("record", "contains", "example.com"),
|
||||||
|
new ConditionExpression("record", "contains", "example.org", LOGICAL.OR),
|
||||||
|
]);
|
||||||
|
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.com" }));
|
||||||
|
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.org" }));
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.net" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test evaluateExpressionGroup with nested group", async (t) => {
|
||||||
|
const group = new ConditionExpressionGroup([
|
||||||
|
new ConditionExpression("record", "contains", "mx1."),
|
||||||
|
new ConditionExpressionGroup([
|
||||||
|
new ConditionExpression("record", "contains", "example.com"),
|
||||||
|
new ConditionExpression("record", "contains", "example.org", LOGICAL.OR),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." }));
|
||||||
|
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" }));
|
||||||
|
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.org" }));
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" }));
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.org" }));
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1.example.net" }));
|
||||||
|
});
|
55
test/backend-test/monitor-conditions/test-expressions.js
Normal file
55
test/backend-test/monitor-conditions/test-expressions.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { ConditionExpressionGroup, ConditionExpression } = require("../../../server/monitor-conditions/expression.js");
|
||||||
|
|
||||||
|
test("Test ConditionExpressionGroup.fromMonitor", async (t) => {
|
||||||
|
const monitor = {
|
||||||
|
conditions: JSON.stringify([
|
||||||
|
{
|
||||||
|
"type": "expression",
|
||||||
|
"andOr": "and",
|
||||||
|
"operator": "contains",
|
||||||
|
"value": "foo",
|
||||||
|
"variable": "record"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "group",
|
||||||
|
"andOr": "and",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "expression",
|
||||||
|
"andOr": "and",
|
||||||
|
"operator": "contains",
|
||||||
|
"value": "bar",
|
||||||
|
"variable": "record"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "group",
|
||||||
|
"andOr": "and",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "expression",
|
||||||
|
"andOr": "and",
|
||||||
|
"operator": "contains",
|
||||||
|
"value": "car",
|
||||||
|
"variable": "record"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
const root = ConditionExpressionGroup.fromMonitor(monitor);
|
||||||
|
assert.strictEqual(true, root.children.length === 2);
|
||||||
|
assert.strictEqual(true, root.children[0] instanceof ConditionExpression);
|
||||||
|
assert.strictEqual(true, root.children[0].value === "foo");
|
||||||
|
assert.strictEqual(true, root.children[1] instanceof ConditionExpressionGroup);
|
||||||
|
assert.strictEqual(true, root.children[1].children.length === 2);
|
||||||
|
assert.strictEqual(true, root.children[1].children[0] instanceof ConditionExpression);
|
||||||
|
assert.strictEqual(true, root.children[1].children[0].value === "bar");
|
||||||
|
assert.strictEqual(true, root.children[1].children[1] instanceof ConditionExpressionGroup);
|
||||||
|
assert.strictEqual(true, root.children[1].children[1].children.length === 1);
|
||||||
|
assert.strictEqual(true, root.children[1].children[1].children[0] instanceof ConditionExpression);
|
||||||
|
assert.strictEqual(true, root.children[1].children[1].children[0].value === "car");
|
||||||
|
});
|
108
test/backend-test/monitor-conditions/test-operators.js
Normal file
108
test/backend-test/monitor-conditions/test-operators.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { operatorMap, OP_CONTAINS, OP_NOT_CONTAINS, OP_LT, OP_GT, OP_LTE, OP_GTE, OP_STR_EQUALS, OP_STR_NOT_EQUALS, OP_NUM_EQUALS, OP_NUM_NOT_EQUALS, OP_STARTS_WITH, OP_ENDS_WITH, OP_NOT_STARTS_WITH, OP_NOT_ENDS_WITH } = require("../../../server/monitor-conditions/operators.js");
|
||||||
|
|
||||||
|
test("Test StringEqualsOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_STR_EQUALS);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.org"));
|
||||||
|
assert.strictEqual(false, op.test("1", 1)); // strict equality
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test StringNotEqualsOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_STR_NOT_EQUALS);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com"));
|
||||||
|
assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test ContainsOperator with scalar", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_CONTAINS);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.org", "example.org"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.org", "example.com"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test ContainsOperator with array", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_CONTAINS);
|
||||||
|
assert.strictEqual(true, op.test([ "example.org" ], "example.org"));
|
||||||
|
assert.strictEqual(false, op.test([ "example.org" ], "example.com"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test NotContainsOperator with scalar", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_NOT_CONTAINS);
|
||||||
|
assert.strictEqual(true, op.test("example.org", ".com"));
|
||||||
|
assert.strictEqual(false, op.test("example.org", ".org"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test NotContainsOperator with array", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_NOT_CONTAINS);
|
||||||
|
assert.strictEqual(true, op.test([ "example.org" ], "example.com"));
|
||||||
|
assert.strictEqual(false, op.test([ "example.org" ], "example.org"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test StartsWithOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_STARTS_WITH);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.com", "mx1"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.com", "mx2"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test NotStartsWithOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_NOT_STARTS_WITH);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.com", "mx2"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.com", "mx1"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test EndsWithOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_ENDS_WITH);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.com", "example.com"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.com", "example.net"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test NotEndsWithOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_NOT_ENDS_WITH);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.com", "example.net"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.com", "example.com"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test NumberEqualsOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_NUM_EQUALS);
|
||||||
|
assert.strictEqual(true, op.test(1, 1));
|
||||||
|
assert.strictEqual(true, op.test(1, "1"));
|
||||||
|
assert.strictEqual(false, op.test(1, "2"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test NumberNotEqualsOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_NUM_NOT_EQUALS);
|
||||||
|
assert.strictEqual(true, op.test(1, "2"));
|
||||||
|
assert.strictEqual(false, op.test(1, "1"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test LessThanOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_LT);
|
||||||
|
assert.strictEqual(true, op.test(1, 2));
|
||||||
|
assert.strictEqual(true, op.test(1, "2"));
|
||||||
|
assert.strictEqual(false, op.test(1, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test GreaterThanOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_GT);
|
||||||
|
assert.strictEqual(true, op.test(2, 1));
|
||||||
|
assert.strictEqual(true, op.test(2, "1"));
|
||||||
|
assert.strictEqual(false, op.test(1, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test LessThanOrEqualToOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_LTE);
|
||||||
|
assert.strictEqual(true, op.test(1, 1));
|
||||||
|
assert.strictEqual(true, op.test(1, 2));
|
||||||
|
assert.strictEqual(true, op.test(1, "2"));
|
||||||
|
assert.strictEqual(false, op.test(1, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test GreaterThanOrEqualToOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_GTE);
|
||||||
|
assert.strictEqual(true, op.test(1, 1));
|
||||||
|
assert.strictEqual(true, op.test(2, 1));
|
||||||
|
assert.strictEqual(true, op.test(2, "2"));
|
||||||
|
assert.strictEqual(false, op.test(2, 3));
|
||||||
|
});
|
|
@ -1,13 +1,4 @@
|
||||||
const semver = require("semver");
|
const test = require("node:test");
|
||||||
let test;
|
|
||||||
const nodeVersion = process.versions.node;
|
|
||||||
// Node.js version >= 18
|
|
||||||
if (semver.satisfies(nodeVersion, ">= 18")) {
|
|
||||||
test = require("node:test");
|
|
||||||
} else {
|
|
||||||
test = require("test");
|
|
||||||
}
|
|
||||||
|
|
||||||
const assert = require("node:assert");
|
const assert = require("node:assert");
|
||||||
const { UptimeCalculator } = require("../../server/uptime-calculator");
|
const { UptimeCalculator } = require("../../server/uptime-calculator");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
import { test } from "@playwright/test";
|
|
||||||
import { login, screenshot } from "./util-test";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Setup
|
|
||||||
*/
|
|
||||||
|
|
||||||
test("setup sqlite", async ({ page }, testInfo) => {
|
|
||||||
await page.goto("./");
|
|
||||||
await page.getByText("SQLite").click();
|
|
||||||
await page.getByRole("button", { name: "Next" }).click();
|
|
||||||
await screenshot(testInfo, page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("setup admin", async ({ page }, testInfo) => {
|
|
||||||
await page.goto("./");
|
|
||||||
await page.getByPlaceholder("Username").click();
|
|
||||||
await page.getByPlaceholder("Username").fill("admin");
|
|
||||||
await page.getByPlaceholder("Username").press("Tab");
|
|
||||||
await page.getByPlaceholder("Password", { exact: true }).fill("admin123");
|
|
||||||
await page.getByPlaceholder("Password", { exact: true }).press("Tab");
|
|
||||||
await page.getByPlaceholder("Repeat Password").fill("admin123");
|
|
||||||
await page.getByRole("button", { name: "Create" }).click();
|
|
||||||
await screenshot(testInfo, page);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* All other tests should be run after setup
|
|
||||||
*/
|
|
||||||
|
|
||||||
test("login", async ({ page }, testInfo) => {
|
|
||||||
await page.goto("./dashboard");
|
|
||||||
await login(page);
|
|
||||||
await screenshot(testInfo, page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("logout", async ({ page }, testInfo) => {
|
|
||||||
await page.goto("./dashboard");
|
|
||||||
await login(page);
|
|
||||||
await page.getByText("A", { exact: true }).click();
|
|
||||||
await page.getByRole("button", { name: "Log out" }).click();
|
|
||||||
await screenshot(testInfo, page);
|
|
||||||
});
|
|
38
test/e2e/specs/example.spec.js
Normal file
38
test/e2e/specs/example.spec.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
|
||||||
|
|
||||||
|
test.describe("Example Spec", () => {
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await restoreSqliteSnapshot(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dashboard", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./dashboard");
|
||||||
|
await login(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("change display timezone", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./settings/general");
|
||||||
|
await login(page);
|
||||||
|
await page.getByLabel("Display Timezone").selectOption("Pacific/Fiji");
|
||||||
|
await page.getByRole("button", { name: "Save" }).click();
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
await page.goto("./dashboard");
|
||||||
|
await page.goto("./settings/general");
|
||||||
|
await expect(page.getByLabel("Display Timezone")).toHaveValue("Pacific/Fiji");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("database is reset after previous test", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./settings/general");
|
||||||
|
await login(page);
|
||||||
|
|
||||||
|
const timezoneEl = page.getByLabel("Display Timezone");
|
||||||
|
await expect(timezoneEl).toBeVisible();
|
||||||
|
await expect(timezoneEl).toHaveValue("auto");
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
109
test/e2e/specs/monitor-form.spec.js
Normal file
109
test/e2e/specs/monitor-form.spec.js
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
|
||||||
|
|
||||||
|
test.describe("Monitor Form", () => {
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await restoreSqliteSnapshot(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("condition ui", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./add");
|
||||||
|
await login(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
const monitorTypeSelect = page.getByTestId("monitor-type-select");
|
||||||
|
await expect(monitorTypeSelect).toBeVisible();
|
||||||
|
|
||||||
|
await monitorTypeSelect.selectOption("dns");
|
||||||
|
const selectedValue = await monitorTypeSelect.evaluate(select => select.value);
|
||||||
|
expect(selectedValue).toBe("dns");
|
||||||
|
|
||||||
|
// Add Conditions & verify:
|
||||||
|
await page.getByTestId("add-condition-button").click();
|
||||||
|
expect(await page.getByTestId("condition").count()).toEqual(2); // 1 added by default + 1 explicitly added
|
||||||
|
|
||||||
|
// Add a Condition Group & verify:
|
||||||
|
await page.getByTestId("add-group-button").click();
|
||||||
|
expect(await page.getByTestId("condition-group").count()).toEqual(1);
|
||||||
|
expect(await page.getByTestId("condition").count()).toEqual(3); // 2 solo conditions + 1 condition in group
|
||||||
|
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
// Remove a condition & verify:
|
||||||
|
await page.getByTestId("remove-condition").first().click();
|
||||||
|
expect(await page.getByTestId("condition").count()).toEqual(2); // 1 solo condition + 1 condition in group
|
||||||
|
|
||||||
|
// Remove a condition group & verify:
|
||||||
|
await page.getByTestId("remove-condition-group").first().click();
|
||||||
|
expect(await page.getByTestId("condition-group").count()).toEqual(0);
|
||||||
|
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("successful condition", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./add");
|
||||||
|
await login(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
const monitorTypeSelect = page.getByTestId("monitor-type-select");
|
||||||
|
await expect(monitorTypeSelect).toBeVisible();
|
||||||
|
|
||||||
|
await monitorTypeSelect.selectOption("dns");
|
||||||
|
const selectedValue = await monitorTypeSelect.evaluate(select => select.value);
|
||||||
|
expect(selectedValue).toBe("dns");
|
||||||
|
|
||||||
|
const friendlyName = "Example DNS NS";
|
||||||
|
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||||
|
await page.getByTestId("hostname-input").fill("example.com");
|
||||||
|
|
||||||
|
// Vue-Multiselect component
|
||||||
|
const resolveTypeSelect = page.getByTestId("resolve-type-select");
|
||||||
|
await resolveTypeSelect.click();
|
||||||
|
await resolveTypeSelect.getByRole("option", { name: "NS" }).click();
|
||||||
|
|
||||||
|
await page.getByTestId("add-condition-button").click();
|
||||||
|
expect(await page.getByTestId("condition").count()).toEqual(2); // 1 added by default + 1 explicitly added
|
||||||
|
await page.getByTestId("condition-value").nth(0).fill("a.iana-servers.net");
|
||||||
|
await page.getByTestId("condition-and-or").nth(0).selectOption("or");
|
||||||
|
await page.getByTestId("condition-value").nth(1).fill("b.iana-servers.net");
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
await page.getByTestId("save-button").click();
|
||||||
|
await page.waitForURL("/dashboard/*"); // wait for the monitor to be created
|
||||||
|
await expect(page.getByTestId("monitor-status")).toHaveText("up", { ignoreCase: true });
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("failing condition", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./add");
|
||||||
|
await login(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
const monitorTypeSelect = page.getByTestId("monitor-type-select");
|
||||||
|
await expect(monitorTypeSelect).toBeVisible();
|
||||||
|
|
||||||
|
await monitorTypeSelect.selectOption("dns");
|
||||||
|
const selectedValue = await monitorTypeSelect.evaluate(select => select.value);
|
||||||
|
expect(selectedValue).toBe("dns");
|
||||||
|
|
||||||
|
const friendlyName = "Example DNS NS";
|
||||||
|
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||||
|
await page.getByTestId("hostname-input").fill("example.com");
|
||||||
|
|
||||||
|
// Vue-Multiselect component
|
||||||
|
const resolveTypeSelect = page.getByTestId("resolve-type-select");
|
||||||
|
await resolveTypeSelect.click();
|
||||||
|
await resolveTypeSelect.getByRole("option", { name: "NS" }).click();
|
||||||
|
|
||||||
|
expect(await page.getByTestId("condition").count()).toEqual(1); // 1 added by default
|
||||||
|
await page.getByTestId("condition-value").nth(0).fill("definitely-not.net");
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
await page.getByTestId("save-button").click();
|
||||||
|
await page.waitForURL("/dashboard/*"); // wait for the monitor to be created
|
||||||
|
await expect(page.getByTestId("monitor-status")).toHaveText("down", { ignoreCase: true });
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
56
test/e2e/specs/setup-process.once.js
Normal file
56
test/e2e/specs/setup-process.once.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { test } from "@playwright/test";
|
||||||
|
import { getSqliteDatabaseExists, login, screenshot, takeSqliteSnapshot } from "../util-test";
|
||||||
|
|
||||||
|
test.describe("Uptime Kuma Setup", () => {
|
||||||
|
|
||||||
|
test.skip(() => getSqliteDatabaseExists(), "Must only run once per session");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Setup
|
||||||
|
*/
|
||||||
|
|
||||||
|
test("setup sqlite", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./");
|
||||||
|
await page.getByText("SQLite").click();
|
||||||
|
await page.getByRole("button", { name: "Next" }).click();
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
await page.waitForURL("/setup"); // ensures the server is ready to continue to the next test
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("setup admin", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./");
|
||||||
|
await page.getByPlaceholder("Username").click();
|
||||||
|
await page.getByPlaceholder("Username").fill("admin");
|
||||||
|
await page.getByPlaceholder("Username").press("Tab");
|
||||||
|
await page.getByPlaceholder("Password", { exact: true }).fill("admin123");
|
||||||
|
await page.getByPlaceholder("Password", { exact: true }).press("Tab");
|
||||||
|
await page.getByPlaceholder("Repeat Password").fill("admin123");
|
||||||
|
await page.getByRole("button", { name: "Create" }).click();
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* All other tests should be run after setup
|
||||||
|
*/
|
||||||
|
|
||||||
|
test("login", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./dashboard");
|
||||||
|
await login(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logout", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./dashboard");
|
||||||
|
await login(page);
|
||||||
|
await page.getByText("A", { exact: true }).click();
|
||||||
|
await page.getByRole("button", { name: "Log out" }).click();
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("take sqlite snapshot", async ({ page }, testInfo) => {
|
||||||
|
await takeSqliteSnapshot(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -1,3 +1,9 @@
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const serverUrl = require("../../config/playwright.config.js").url;
|
||||||
|
|
||||||
|
const dbPath = "./../../data/playwright-test/kuma.db";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {TestInfo} testInfo Test info
|
* @param {TestInfo} testInfo Test info
|
||||||
* @param {Page} page Page
|
* @param {Page} page Page
|
||||||
|
@ -25,3 +31,32 @@ export async function login(page) {
|
||||||
await page.getByRole("button", { name: "Log in" }).click();
|
await page.getByRole("button", { name: "Log in" }).click();
|
||||||
await page.isVisible("text=Add New Monitor");
|
await page.isVisible("text=Add New Monitor");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the SQLite database has been created. This indicates setup has completed.
|
||||||
|
* @returns {boolean} True if exists
|
||||||
|
*/
|
||||||
|
export function getSqliteDatabaseExists() {
|
||||||
|
return fs.existsSync(path.resolve(__dirname, dbPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a request to the server to take a snapshot of the SQLite database.
|
||||||
|
* @param {Page|null} page Page
|
||||||
|
* @returns {Promise<Response>} Promise of response from snapshot request.
|
||||||
|
*/
|
||||||
|
export async function takeSqliteSnapshot(page = null) {
|
||||||
|
if (page) {
|
||||||
|
return page.goto("./_e2e/take-sqlite-snapshot");
|
||||||
|
} else {
|
||||||
|
return fetch(`${serverUrl}/_e2e/take-sqlite-snapshot`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a request to the server to restore the snapshot of the SQLite database.
|
||||||
|
* @returns {Promise<Response>} Promise of response from restoration request.
|
||||||
|
*/
|
||||||
|
export async function restoreSqliteSnapshot() {
|
||||||
|
return fetch(`${serverUrl}/_e2e/restore-sqlite-snapshot`);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue