Compare commits

..

8 Commits

Author SHA1 Message Date
Frank Elsinga
c247ecdb10 tested reverting a change to the testing harness 2024-10-04 03:57:25 +02:00
Frank Elsinga
28c5191195 fixed the skip condition correctly targetting the platforms I want 2024-10-04 03:57:25 +02:00
Frank Elsinga
a6a662e751 made sure that skipping of the testcase uses the provided mechanisms 2024-10-04 03:57:25 +02:00
Frank Elsinga
e97fd40b54 excluded arm from testcontainer runs 2024-10-04 03:57:25 +02:00
UptimeKumaBot
e80a6ef728 Translations Update from Weblate (#5072)
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Co-authored-by: Marco <marco@nanoweb.ch>
Co-authored-by: Ilkka Myller <ilkka.myller@nodefield.com>
Co-authored-by: Michal <black23@gmail.com>
Co-authored-by: Harry Suryapambagya <harsxv@gmail.com>
Co-authored-by: Lee Min Waan <rakis9076@gmail.com>
Co-authored-by: AnnAngela <naganjue@vip.qq.com>
Co-authored-by: kindercat <156439718+kindercat@users.noreply.github.com>
Co-authored-by: stanol <stanol777@gmail.com>
Co-authored-by: Hyeonho Kang <hyeonhokang10@o365.ice.go.kr>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: MrEddX <mreddx@chatrix.one>
Co-authored-by: Sajjad Jazini <s.jazini@vatanmail.ir>
Co-authored-by: Ossie Stinga <ossie.stinga@gmail.com>
Co-authored-by: Elia Ronchetti <e.ronchetti@campus.unimib.it>
Co-authored-by: B3CKDOOR <B3CKDOOR@users.noreply.weblate.kuma.pet>
Co-authored-by: Ivan M <ivan.meishutovich@gmail.com>
Co-authored-by: AlanFarley <alanfarley2010@live.com>
Co-authored-by: Mohammad Hesam Imani <hesamimany@gmail.com>
Co-authored-by: Shinwoo PARK <devpysweb@gmail.com>
Co-authored-by: Lucius Gaitán <lucius@luciusgaitan.com>
Co-authored-by: Ömer Faruk GÖL <omerfarukgol@hotmail.com>
Co-authored-by: Satveek Gupta <guptasatveek@gmail.com>
Co-authored-by: Aung ko <aungko.use@gmail.com>
2024-10-04 03:57:25 +02:00
Frank Elsinga
e262056512 Better description for shrink database button (#4814)
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2024-10-04 03:57:25 +02:00
Frank Elsinga
4bbd9430f3 made sure that CI is happy 2024-10-04 02:48:42 +02:00
Frank Elsinga
18d80b254b implemented a testcase for mqtt monitors 2024-10-04 02:30:06 +02:00
76 changed files with 1504 additions and 1055 deletions

View File

@@ -15,14 +15,14 @@ on:
jobs:
auto-test:
needs: [ check-linters ]
needs: [ check-linters, e2e-test ]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
node: [ 18, 20 ]
node: [ 18, 20.5 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
@@ -42,7 +42,7 @@ jobs:
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
armv7-simple-test:
needs: [ ]
needs: [ check-linters ]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
if: ${{ github.repository == 'louislam/uptime-kuma' }}
@@ -77,7 +77,7 @@ jobs:
- run: npm run lint:prod
e2e-test:
needs: [ ]
needs: [ check-linters ]
runs-on: ARM64
steps:
- run: git config --global core.autocrlf false # Mainly for Windows

View File

@@ -1,6 +1,6 @@
# Project Info
First of all, I want to thank everyone who has submitted issues or shared pull requests for Uptime Kuma.
First of all, I want to thank everyone who have wrote issues or shared pull requests for Uptime Kuma.
I never thought the GitHub community would be so nice!
Because of this, I also never thought that other people would actually read and edit my code.
Parts of the code are not very well-structured or commented, sorry about that.
@@ -9,7 +9,7 @@ The project was created with `vite.js` and is written in `vue3`.
Our backend lives in the `server`-directory and mostly communicates via websockets.
Both frontend and backend share the same `package.json`.
For production, the frontend is built into the `dist`-directory and the server (`express.js`) exposes the `dist` directory as the root of the endpoint.
For production, the frontend is build into `dist`-directory and the server (`express.js`) exposes the `dist` directory as the root of the endpoint.
For development, we run vite in development mode on another port.
## Directories
@@ -28,7 +28,7 @@ For development, we run vite in development mode on another port.
## Can I create a pull request for Uptime Kuma?
Yes or no, it depends on what you will try to do.
Both yours and our maintainers' time is precious, and we don't want to waste either.
Both your and our maintainers time is precious, and we don't want to waste both time.
If you have any questions about any process/.. is not clear, you are likely not alone => please ask them ^^
@@ -49,11 +49,11 @@ Different guidelines exist for different types of pull requests (PRs):
<p>
If you come across a bug and think you can solve, we appreciate your work.
Please make sure that you follow these rules:
Please make sure that you follow by these rules:
- keep the PR as small as possible, fix only one thing at a time => keeping it reviewable
- test that your code does what you claim it does.
- test that your code does what you came it does.
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>translations / internationalisation (i18n)</b></summary>
@@ -68,7 +68,7 @@ Different guidelines exist for different types of pull requests (PRs):
- language keys need to be **added to `en.json`** to be visible in weblate. If this has not happened, a PR is appreciated.
- **Adding a new language** requires a new file see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md)
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>new notification providers</b></summary>
@@ -102,7 +102,7 @@ Different guidelines exist for different types of pull requests (PRs):
Therefore, making sure that they work is also really important.
Because testing notification providers is quite time intensive, we mostly offload this onto the person contributing a notification provider.
To make sure you have tested the notification provider, please include screenshots of the following events in the pull-request description:
To make shure you have tested the notification provider, please include screenshots of the following events in the pull-request description:
- `UP`/`DOWN`
- Certificate Expiry via https://expired.badssl.com/
- Testing (the test button on the notification provider setup page)
@@ -117,7 +117,7 @@ Different guidelines exist for different types of pull requests (PRs):
| Testing | paste-image-here | paste-image-here |
```
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>new monitoring types</b></summary>
@@ -138,14 +138,14 @@ Different guidelines exist for different types of pull requests (PRs):
-
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>new features/ major changes / breaking bugfixes</b></summary>
<p>
be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**.
This is especially important for a large pull request or when you don't know if it will be merged or not.
This is especially important for a large pull request or you don't know if it will be merged or not.
<sub>Because of the large impact of this work, only senior maintainers may merge PRs in this area.</sub>
</p>
@@ -201,7 +201,7 @@ The rationale behind this is that we can align the direction and scope of the fe
## Project Styles
I personally do not like something that requires a lot of configuration before you can finally start the app.
I personally do not like something that requires so many configurations before you can finally start the app.
The goal is to make the Uptime Kuma installation as easy as installing a mobile app.
- Easy to install for non-Docker users
@@ -260,7 +260,7 @@ Port `3000` and port `3001` will be used.
npm run dev
```
But sometimes you may want to restart the server without restarting the frontend. In that case, you can run these commands in two terminals:
But sometimes, you would like to restart the server, but not the frontend, you can run these commands in two terminals:
```bash
npm run start-frontend-dev
@@ -409,7 +409,7 @@ https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
### What is a maintainer and what are their roles?
This project has multiple maintainers who specialise in different areas.
This project has multiple maintainers which specialise in different areas.
Currently, there are 3 maintainers:
| Person | Role | Main Area |

View File

@@ -16,7 +16,9 @@ export default defineConfig({
},
define: {
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
"process.env": {},
"DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER),
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
},
plugins: [
vue(),

View File

@@ -4,6 +4,7 @@ const tar = require("tar");
const packageJSON = require("../package.json");
const fs = require("fs");
const rmSync = require("./fs-rmSync.js");
const version = packageJSON.version;
const filename = "dist.tar.gz";
@@ -28,9 +29,8 @@ function download(url) {
if (fs.existsSync("./dist")) {
if (fs.existsSync("./dist-backup")) {
fs.rmSync("./dist-backup", {
recursive: true,
force: true,
rmSync("./dist-backup", {
recursive: true
});
}
@@ -43,9 +43,8 @@ function download(url) {
tarStream.on("close", () => {
if (fs.existsSync("./dist-backup")) {
fs.rmSync("./dist-backup", {
recursive: true,
force: true,
rmSync("./dist-backup", {
recursive: true
});
}
console.log("Done");

23
extra/fs-rmSync.js Normal file
View File

@@ -0,0 +1,23 @@
const fs = require("fs");
/**
* Detect if `fs.rmSync` is available
* to avoid the runtime deprecation warning triggered for using `fs.rmdirSync` with `{ recursive: true }` in Node.js v16,
* or the `recursive` property removing completely in the future Node.js version.
* See the link below.
* @todo Once we drop the support for Node.js v14 (or at least versions before v14.14.0), we can safely replace this function with `fs.rmSync`, since `fs.rmSync` was add in Node.js v14.14.0 and currently we supports all the Node.js v14 versions that include the versions before the v14.14.0, and this function have almost the same signature with `fs.rmSync`.
* @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true- the deprecation information of `fs.rmdirSync`
* @link https://nodejs.org/docs/latest-v16.x/api/fs.html#fsrmsyncpath-options the document of `fs.rmSync`
* @param {fs.PathLike} path Valid types for path values in "fs".
* @param {fs.RmDirOptions} options options for `fs.rmdirSync`, if `fs.rmSync` is available and property `recursive` is true, it will automatically have property `force` with value `true`.
* @returns {void}
*/
const rmSync = (path, options) => {
if (typeof fs.rmSync === "function") {
if (options.recursive) {
options.force = true;
}
return fs.rmSync(path, options);
}
return fs.rmdirSync(path, options);
};
module.exports = rmSync;

View File

@@ -2,6 +2,7 @@
import fs from "fs";
import util from "util";
import rmSync from "../fs-rmSync.js";
/**
* Copy across the required language files
@@ -15,10 +16,7 @@ import util from "util";
*/
function copyFiles(langCode, baseLang) {
if (fs.existsSync("./languages")) {
fs.rmSync("./languages", {
recursive: true,
force: true,
});
rmSync("./languages", { recursive: true });
}
fs.mkdirSync("./languages");
@@ -95,9 +93,6 @@ console.log("Updating: " + langCode);
copyFiles(langCode, baseLangCode);
await updateLanguage(langCode, baseLangCode);
fs.rmSync("./languages", {
recursive: true,
force: true,
});
rmSync("./languages", { recursive: true });
console.log("Done. Fixing formatting by ESLint...");

988
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -68,7 +68,8 @@
"sort-contributors": "node extra/sort-contributors.js",
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X"
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
"start-server-node14-win": "private\\node14\\node.exe server/server.js"
},
"dependencies": {
"@grpc/grpc-js": "~1.8.22",
@@ -154,6 +155,7 @@
"@fortawesome/vue-fontawesome": "~3.0.0-5",
"@playwright/test": "~1.39.0",
"@popperjs/core": "~2.10.2",
"@testcontainers/hivemq": "^10.13.1",
"@types/bootstrap": "~5.1.9",
"@types/node": "^20.8.6",
"@typescript-eslint/eslint-plugin": "^6.7.5",
@@ -189,6 +191,7 @@
"stylelint-config-standard": "~25.0.0",
"terser": "~5.15.0",
"test": "~3.3.0",
"testcontainers": "^10.13.1",
"typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
"vite": "~5.2.8",

View File

@@ -1,7 +1,6 @@
const basicAuth = require("express-basic-auth");
const passwordHash = require("./password-hash");
const { R } = require("redbean-node");
const { setting } = require("./util-server");
const { log } = require("../src/util");
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
const { Settings } = require("./settings");
@@ -139,7 +138,7 @@ exports.basicAuth = async function (req, res, next) {
challenge: true,
});
const disabledAuth = await setting("disableAuth");
const disabledAuth = await Settings.get("disableAuth");
if (!disabledAuth) {
middleware(req, res, next);

View File

@@ -1,7 +1,7 @@
const { setSetting, setting } = require("./util-server");
const axios = require("axios");
const compareVersions = require("compare-versions");
const { log } = require("../src/util");
const { Settings } = require("./settings");
exports.version = require("../package.json").version;
exports.latestVersion = null;
@@ -14,7 +14,7 @@ let interval;
exports.startInterval = () => {
let check = async () => {
if (await setting("checkUpdate") === false) {
if (await Settings.get("checkUpdate") === false) {
return;
}
@@ -28,7 +28,7 @@ exports.startInterval = () => {
res.data.slow = "1000.0.0";
}
let checkBeta = await setting("checkBeta");
let checkBeta = await Settings.get("checkBeta");
if (checkBeta && res.data.beta) {
if (compareVersions.compare(res.data.beta, res.data.slow, ">")) {
@@ -57,7 +57,7 @@ exports.startInterval = () => {
* @returns {Promise<void>}
*/
exports.enableCheckUpdate = async (value) => {
await setSetting("checkUpdate", value);
await Settings.set("checkUpdate", value);
clearInterval(interval);

View File

@@ -6,8 +6,8 @@ const { R } = require("redbean-node");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const server = UptimeKumaServer.getInstance();
const io = server.io;
const { setting } = require("./util-server");
const checkVersion = require("./check-version");
const { Settings } = require("./settings");
const Database = require("./database");
/**
@@ -158,8 +158,8 @@ async function sendInfo(socket, hideVersion = false) {
version,
latestVersion,
isContainer,
primaryBaseURL: await Settings.get("primaryBaseURL"),
dbType,
primaryBaseURL: await setting("primaryBaseURL"),
serverTimezone: await server.getTimezone(),
serverTimezoneOffset: server.getTimezoneOffset(),
});

View File

@@ -1,11 +1,11 @@
const fs = require("fs");
const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server");
const { log, sleep } = require("../src/util");
const knex = require("knex");
const path = require("path");
const { EmbeddedMariaDB } = require("./embedded-mariadb");
const mysql = require("mysql2/promise");
const { Settings } = require("./settings");
/**
* Database & App Data Folder
@@ -420,7 +420,7 @@ class Database {
* @deprecated
*/
static async patchSqlite() {
let version = parseInt(await setting("database_version"));
let version = parseInt(await Settings.get("database_version"));
if (! version) {
version = 0;
@@ -445,7 +445,7 @@ class Database {
log.info("db", `Patching ${sqlFile}`);
await Database.importSQLFile(sqlFile);
log.info("db", `Patched ${sqlFile}`);
await setSetting("database_version", i);
await Settings.set("database_version", i);
}
} catch (ex) {
await Database.close();
@@ -471,7 +471,7 @@ class Database {
*/
static async patchSqlite2() {
log.debug("db", "Database Patch 2.0 Process");
let databasePatchedFiles = await setting("databasePatchedFiles");
let databasePatchedFiles = await Settings.get("databasePatchedFiles");
if (! databasePatchedFiles) {
databasePatchedFiles = {};
@@ -499,7 +499,7 @@ class Database {
process.exit(1);
}
await setSetting("databasePatchedFiles", databasePatchedFiles);
await Settings.set("databasePatchedFiles", databasePatchedFiles);
}
/**
@@ -512,27 +512,27 @@ class Database {
// Fix 1.13.0 empty slug bug
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
let title = await setting("title");
let title = await Settings.get("title");
if (title) {
console.log("Migrating Status Page");
log.info("database", "Migrating Status Page");
let statusPageCheck = await R.findOne("status_page", " slug = 'default' ");
if (statusPageCheck !== null) {
console.log("Migrating Status Page - Skip, default slug record is already existing");
log.info("database", "Migrating Status Page - Skip, default slug record is already existing");
return;
}
let statusPage = R.dispense("status_page");
statusPage.slug = "default";
statusPage.title = title;
statusPage.description = await setting("description");
statusPage.icon = await setting("icon");
statusPage.theme = await setting("statusPageTheme");
statusPage.published = !!await setting("statusPagePublished");
statusPage.search_engine_index = !!await setting("searchEngineIndex");
statusPage.show_tags = !!await setting("statusPageTags");
statusPage.description = await Settings.get("description");
statusPage.icon = await Settings.get("icon");
statusPage.theme = await Settings.get("statusPageTheme");
statusPage.published = !!await Settings.get("statusPagePublished");
statusPage.search_engine_index = !!await Settings.get("searchEngineIndex");
statusPage.show_tags = !!await Settings.get("statusPageTags");
statusPage.password = null;
if (!statusPage.title) {
@@ -560,13 +560,13 @@ class Database {
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
// Migrate Entry Page if it is status page
let entryPage = await setting("entryPage");
let entryPage = await Settings.get("entryPage");
if (entryPage === "statusPage") {
await setSetting("entryPage", "statusPage-default", "general");
await Settings.set("entryPage", "statusPage-default", "general");
}
console.log("Migrating Status Page - Done");
log.info("database", "Migrating Status Page - Done");
}
}

View File

@@ -1,7 +1,7 @@
const { R } = require("redbean-node");
const { log } = require("../../src/util");
const { setSetting, setting } = require("../util-server");
const Database = require("../database");
const { Settings } = require("../settings");
const DEFAULT_KEEP_PERIOD = 180;
@@ -11,11 +11,11 @@ const DEFAULT_KEEP_PERIOD = 180;
*/
const clearOldData = async () => {
let period = await setting("keepDataPeriodDays");
let period = await Settings.get("keepDataPeriodDays");
// Set Default Period
if (period == null) {
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
await Settings.set("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
period = DEFAULT_KEEP_PERIOD;
}
@@ -25,7 +25,7 @@ const clearOldData = async () => {
parsedPeriod = parseInt(period);
} catch (_) {
log.warn("clearOldData", "Failed to parse setting, resetting to default..");
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
await Settings.set("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
parsedPeriod = DEFAULT_KEEP_PERIOD;
}

View File

@@ -4,7 +4,7 @@ const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
SQL_DATETIME_FORMAT, evaluateJsonQuery
} = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, mssqlQuery, postgresQuery, mysqlQuery, httpNtlm, radius, grpcQuery,
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
} = require("../util-server");
const { R } = require("redbean-node");
@@ -24,6 +24,7 @@ const { CookieJar } = require("tough-cookie");
const { HttpsCookieAgent } = require("http-cookie-agent/http");
const https = require("https");
const http = require("http");
const { Settings } = require("../settings");
const rootCertificates = rootCertificatesFingerprints();
@@ -71,12 +72,23 @@ class Monitor extends BeanModel {
/**
* Return an object that ready to parse to JSON
* @param {object} preloadData to prevent n+1 problems, we query the data in a batch outside of this function
* @param {boolean} includeSensitiveData Include sensitive data in
* JSON
* @returns {object} Object ready to parse
* @returns {Promise<object>} Object ready to parse
*/
toJSON(preloadData = {}, includeSensitiveData = true) {
async toJSON(includeSensitiveData = true) {
let notificationIDList = {};
let list = await R.find("monitor_notification", " monitor_id = ? ", [
this.id,
]);
for (let bean of list) {
notificationIDList[bean.notification_id] = true;
}
const tags = await this.getTags();
let screenshot = null;
@@ -84,7 +96,7 @@ class Monitor extends BeanModel {
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
}
const path = preloadData.paths.get(this.id) || [];
const path = await this.getPath();
const pathName = path.join(" / ");
let data = {
@@ -94,15 +106,15 @@ class Monitor extends BeanModel {
path,
pathName,
parent: this.parent,
childrenIDs: preloadData.childrenIDs.get(this.id) || [],
childrenIDs: await Monitor.getAllChildrenIDs(this.id),
url: this.url,
method: this.method,
hostname: this.hostname,
port: this.port,
maxretries: this.maxretries,
weight: this.weight,
active: preloadData.activeStatus.get(this.id),
forceInactive: preloadData.forceInactive.get(this.id),
active: await this.isActive(),
forceInactive: !await Monitor.isParentActive(this.id),
type: this.type,
timeout: this.timeout,
interval: this.interval,
@@ -122,9 +134,9 @@ class Monitor extends BeanModel {
docker_container: this.docker_container,
docker_host: this.docker_host,
proxyId: this.proxy_id,
notificationIDList: preloadData.notifications.get(this.id) || {},
tags: preloadData.tags.get(this.id) || [],
maintenance: preloadData.maintenanceStatus.get(this.id),
notificationIDList,
tags: tags,
maintenance: await Monitor.isUnderMaintenance(this.id),
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage,
mqttCheckType: this.mqttCheckType,
@@ -190,6 +202,16 @@ class Monitor extends BeanModel {
return data;
}
/**
* Checks if the monitor is active based on itself and its parents
* @returns {Promise<boolean>} Is the monitor active?
*/
async isActive() {
const parentActive = await Monitor.isParentActive(this.id);
return (this.active === 1) && parentActive;
}
/**
* Get all tags applied to this monitor
* @returns {Promise<LooseObject<any>[]>} List of tags on the
@@ -325,7 +347,7 @@ class Monitor extends BeanModel {
let previousBeat = null;
let retries = 0;
this.prometheus = new Prometheus(this);
this.prometheus = await Prometheus.createAndInitMetrics(this);
const beat = async () => {
@@ -651,7 +673,7 @@ class Monitor extends BeanModel {
} else if (this.type === "steam") {
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
const steamAPIKey = await setting("steamAPIKey");
const steamAPIKey = await Settings.get("steamAPIKey");
const filter = `addr\\${this.hostname}:${this.port}`;
if (!steamAPIKey) {
@@ -977,7 +999,7 @@ class Monitor extends BeanModel {
await R.store(bean);
log.debug("monitor", `[${this.name}] prometheus.update`);
this.prometheus?.update(bean, tlsInfo);
await this.prometheus?.update(bean, tlsInfo);
previousBeat = bean;
@@ -1175,18 +1197,6 @@ class Monitor extends BeanModel {
return checkCertificateResult;
}
/**
* Checks if the monitor is active based on itself and its parents
* @param {number} monitorID ID of monitor to send
* @param {boolean} active is active
* @returns {Promise<boolean>} Is the monitor active?
*/
static async isActive(monitorID, active) {
const parentActive = await Monitor.isParentActive(monitorID);
return (active === 1) && parentActive;
}
/**
* Send statistics to clients
* @param {Server} io Socket server instance
@@ -1323,10 +1333,7 @@ class Monitor extends BeanModel {
for (let notification of notificationList) {
try {
const heartbeatJSON = bean.toJSON();
const monitorData = [{ id: monitor.id,
active: monitor.active
}];
const preloadData = await Monitor.preparePreloadData(monitorData);
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
if (!heartbeatJSON["msg"]) {
heartbeatJSON["msg"] = "N/A";
@@ -1337,7 +1344,7 @@ class Monitor extends BeanModel {
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
await Notification.send(JSON.parse(notification.config), msg, monitor.toJSON(preloadData, false), heartbeatJSON);
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
} catch (e) {
log.error("monitor", "Cannot send notification to " + notification.name);
log.error("monitor", e);
@@ -1373,11 +1380,12 @@ class Monitor extends BeanModel {
return;
}
let notifyDays = await setting("tlsExpiryNotifyDays");
let notifyDays = await Settings.get("tlsExpiryNotifyDays");
if (notifyDays == null || !Array.isArray(notifyDays)) {
// Reset Default
await setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
await Settings.set("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
notifyDays = [ 7, 14, 21 ];
await Settings.set("tlsExpiryNotifyDays", notifyDays, "general");
}
if (Array.isArray(notifyDays)) {
@@ -1499,108 +1507,6 @@ class Monitor extends BeanModel {
}
}
/**
* Gets monitor notification of multiple monitor
* @param {Array} monitorIDs IDs of monitor to get
* @returns {Promise<LooseObject<any>>} object
*/
static async getMonitorNotification(monitorIDs) {
return await R.getAll(`
SELECT monitor_notification.monitor_id, monitor_notification.notification_id
FROM monitor_notification
WHERE monitor_notification.monitor_id IN (${monitorIDs.map((_) => "?").join(",")})
`, monitorIDs);
}
/**
* Gets monitor tags of multiple monitor
* @param {Array} monitorIDs IDs of monitor to get
* @returns {Promise<LooseObject<any>>} object
*/
static async getMonitorTag(monitorIDs) {
return await R.getAll(`
SELECT monitor_tag.monitor_id, monitor_tag.tag_id, tag.name, tag.color
FROM monitor_tag
JOIN tag ON monitor_tag.tag_id = tag.id
WHERE monitor_tag.monitor_id IN (${monitorIDs.map((_) => "?").join(",")})
`, monitorIDs);
}
/**
* prepare preloaded data for efficient access
* @param {Array} monitorData IDs & active field of monitor to get
* @returns {Promise<LooseObject<any>>} object
*/
static async preparePreloadData(monitorData) {
const notificationsMap = new Map();
const tagsMap = new Map();
const maintenanceStatusMap = new Map();
const childrenIDsMap = new Map();
const activeStatusMap = new Map();
const forceInactiveMap = new Map();
const pathsMap = new Map();
if (monitorData.length > 0) {
const monitorIDs = monitorData.map(monitor => monitor.id);
const notifications = await Monitor.getMonitorNotification(monitorIDs);
const tags = await Monitor.getMonitorTag(monitorIDs);
const maintenanceStatuses = await Promise.all(monitorData.map(monitor => Monitor.isUnderMaintenance(monitor.id)));
const childrenIDs = await Promise.all(monitorData.map(monitor => Monitor.getAllChildrenIDs(monitor.id)));
const activeStatuses = await Promise.all(monitorData.map(monitor => Monitor.isActive(monitor.id, monitor.active)));
const forceInactiveStatuses = await Promise.all(monitorData.map(monitor => Monitor.isParentActive(monitor.id)));
const paths = await Promise.all(monitorData.map(monitor => Monitor.getAllPath(monitor.id, monitor.name)));
notifications.forEach(row => {
if (!notificationsMap.has(row.monitor_id)) {
notificationsMap.set(row.monitor_id, {});
}
notificationsMap.get(row.monitor_id)[row.notification_id] = true;
});
tags.forEach(row => {
if (!tagsMap.has(row.monitor_id)) {
tagsMap.set(row.monitor_id, []);
}
tagsMap.get(row.monitor_id).push({
tag_id: row.tag_id,
name: row.name,
color: row.color
});
});
monitorData.forEach((monitor, index) => {
maintenanceStatusMap.set(monitor.id, maintenanceStatuses[index]);
});
monitorData.forEach((monitor, index) => {
childrenIDsMap.set(monitor.id, childrenIDs[index]);
});
monitorData.forEach((monitor, index) => {
activeStatusMap.set(monitor.id, activeStatuses[index]);
});
monitorData.forEach((monitor, index) => {
forceInactiveMap.set(monitor.id, !forceInactiveStatuses[index]);
});
monitorData.forEach((monitor, index) => {
pathsMap.set(monitor.id, paths[index]);
});
}
return {
notifications: notificationsMap,
tags: tagsMap,
maintenanceStatus: maintenanceStatusMap,
childrenIDs: childrenIDsMap,
activeStatus: activeStatusMap,
forceInactive: forceInactiveMap,
paths: pathsMap,
};
}
/**
* Gets Parent of the monitor
* @param {number} monitorID ID of monitor to get
@@ -1633,18 +1539,16 @@ class Monitor extends BeanModel {
/**
* Gets the full path
* @param {number} monitorID ID of the monitor to get
* @param {string} name of the monitor to get
* @returns {Promise<string[]>} Full path (includes groups and the name) of the monitor
*/
static async getAllPath(monitorID, name) {
const path = [ name ];
async getPath() {
const path = [ this.name ];
if (this.parent === null) {
return path;
}
let parent = await Monitor.getParent(monitorID);
let parent = await Monitor.getParent(this.id);
while (parent !== null) {
path.unshift(parent.name);
parent = await Monitor.getParent(parent.id);

View File

@@ -1,5 +1,6 @@
let url = require("url");
let MemoryCache = require("./memory-cache");
const { log } = require("../../../src/util");
let t = {
ms: 1,
@@ -90,24 +91,6 @@ function ApiCache() {
instances.push(this);
this.id = instances.length;
/**
* Logs a message to the console if the `DEBUG` environment variable is set.
* @param {string} a The first argument to log.
* @param {string} b The second argument to log.
* @param {string} c The third argument to log.
* @param {string} d The fourth argument to log, and so on... (optional)
*
* Generated by Trelent
*/
function debug(a, b, c, d) {
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) {
return arg !== undefined;
});
let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1;
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
}
/**
* Returns true if the given request and response should be logged.
* @param {Object} request The HTTP request object.
@@ -146,7 +129,7 @@ function ApiCache() {
let groupName = req.apicacheGroup;
if (groupName) {
debug("group detected \"" + groupName + "\"");
log.debug("apicache", `group detected "${groupName}"`);
let group = (index.groups[groupName] = index.groups[groupName] || []);
group.unshift(key);
}
@@ -212,7 +195,7 @@ function ApiCache() {
redis.hset(key, "duration", duration);
redis.expire(key, duration / 1000, expireCallback || function () {});
} catch (err) {
debug("[apicache] error in redis.hset()");
log.debug("apicache", `error in redis.hset(): ${err}`);
}
} else {
memCache.add(key, value, duration, expireCallback);
@@ -320,10 +303,10 @@ function ApiCache() {
// display log entry
let elapsed = new Date() - req.apicacheTimer;
debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed));
debug("_apicache.headers: ", res._apicache.headers);
debug("res.getHeaders(): ", getSafeHeaders(res));
debug("cacheObject: ", cacheObject);
log.debug("apicache", `adding cache entry for "${key}" @ ${strDuration} ${logDuration(elapsed)}`);
log.debug("apicache", `_apicache.headers: ${JSON.stringify(res._apicache.headers)}`);
log.debug("apicache", `res.getHeaders(): ${JSON.stringify(getSafeHeaders(res))}`);
log.debug("apicache", `cacheObject: ${JSON.stringify(cacheObject)}`);
}
}
@@ -402,10 +385,10 @@ function ApiCache() {
let redis = globalOptions.redisClient;
if (group) {
debug("clearing group \"" + target + "\"");
log.debug("apicache", `clearing group "${target}"`);
group.forEach(function (key) {
debug("clearing cached entry for \"" + key + "\"");
log.debug("apicache", `clearing cached entry for "${key}"`);
clearTimeout(timers[key]);
delete timers[key];
if (!globalOptions.redisClient) {
@@ -414,7 +397,7 @@ function ApiCache() {
try {
redis.del(key);
} catch (err) {
console.log("[apicache] error in redis.del(\"" + key + "\")");
log.info("apicache", "error in redis.del(\"" + key + "\")");
}
}
index.all = index.all.filter(doesntMatch(key));
@@ -422,7 +405,7 @@ function ApiCache() {
delete index.groups[target];
} else if (target) {
debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\"");
log.debug("apicache", `clearing ${isAutomatic ? "expired" : "cached"} entry for "${target}"`);
clearTimeout(timers[target]);
delete timers[target];
// clear actual cached entry
@@ -432,7 +415,7 @@ function ApiCache() {
try {
redis.del(target);
} catch (err) {
console.log("[apicache] error in redis.del(\"" + target + "\")");
log.error("apicache", "error in redis.del(\"" + target + "\")");
}
}
@@ -449,7 +432,7 @@ function ApiCache() {
}
});
} else {
debug("clearing entire index");
log.debug("apicache", "clearing entire index");
if (!redis) {
memCache.clear();
@@ -461,7 +444,7 @@ function ApiCache() {
try {
redis.del(key);
} catch (err) {
console.log("[apicache] error in redis.del(\"" + key + "\")");
log.error("apicache", `error in redis.del("${key}"): ${err}`);
}
});
}
@@ -528,7 +511,7 @@ function ApiCache() {
/**
* Get index of a group
* @param {string} group
* @param {string} group
* @returns {number}
*/
this.getIndex = function (group) {
@@ -543,9 +526,9 @@ function ApiCache() {
* Express middleware
* @param {(string|number)} strDuration Duration to cache responses
* for.
* @param {function(Object, Object):boolean} middlewareToggle
* @param {function(Object, Object):boolean} middlewareToggle
* @param {Object} localOptions Options for APICache
* @returns
* @returns
*/
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
let duration = instance.getDuration(strDuration);
@@ -752,7 +735,7 @@ function ApiCache() {
*/
let cache = function (req, res, next) {
function bypass() {
debug("bypass detected, skipping cache.");
log.debug("apicache", "bypass detected, skipping cache.");
return next();
}
@@ -805,7 +788,7 @@ function ApiCache() {
// send if cache hit from memory-cache
if (cached) {
let elapsed = new Date() - req.apicacheTimer;
debug("sending cached (memory-cache) version of", key, logDuration(elapsed));
log.debug("apicache", `sending cached (memory-cache) version of ${key} ${logDuration(elapsed)}`);
perf.hit(key);
return sendCachedResponse(req, res, cached, middlewareToggle, next, duration);
@@ -817,7 +800,7 @@ function ApiCache() {
redis.hgetall(key, function (err, obj) {
if (!err && obj && obj.response) {
let elapsed = new Date() - req.apicacheTimer;
debug("sending cached (redis) version of", key, logDuration(elapsed));
log.debug("apicache", "sending cached (redis) version of "+ key+" "+ logDuration(elapsed));
perf.hit(key);
return sendCachedResponse(
@@ -859,7 +842,7 @@ function ApiCache() {
/**
* Process options
* @param {Object} options
* @param {Object} options
* @returns {Object}
*/
this.options = function (options) {
@@ -873,7 +856,7 @@ function ApiCache() {
}
if (globalOptions.trackPerformance) {
debug("WARNING: using trackPerformance flag can cause high memory usage!");
log.debug("apicache", "WARNING: using trackPerformance flag can cause high memory usage!");
}
return this;

View File

@@ -63,7 +63,7 @@ if (process.platform === "win32") {
* @returns {Promise<boolean>} The executable is allowed?
*/
async function isAllowedChromeExecutable(executablePath) {
console.log(config.args);
log.info("Chromium", config.args);
if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
return true;
}
@@ -102,7 +102,8 @@ async function getBrowser() {
*/
async function getRemoteBrowser(remoteBrowserID, userId) {
let remoteBrowser = await RemoteBrowser.get(remoteBrowserID, userId);
log.debug("MONITOR", `Using remote browser: ${remoteBrowser.name} (${remoteBrowser.id})`);
log.debug("Chromium", `Using remote browser: ${remoteBrowser.name} (${remoteBrowser.id})`);
browser = chromium.connect(remoteBrowser.url);
browser = await chromium.connect(remoteBrowser.url);
return browser;
}

View File

@@ -1,35 +0,0 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Elks extends NotificationProvider {
name = "Elks";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://api.46elks.com/a1/sms";
try {
let data = new URLSearchParams();
data.append("from", notification.elksFromNumber);
data.append("to", notification.elksToNumber );
data.append("message", msg);
const config = {
headers: {
"Authorization": "Basic " + Buffer.from(`${notification.elksUsername}:${notification.elksAuthToken}`).toString("base64")
}
};
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Elks;

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setting } = require("../util-server");
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
const { Settings } = require("../settings");
class AlertNow extends NotificationProvider {
name = "AlertNow";
@@ -29,7 +29,7 @@ class AlertNow extends NotificationProvider {
textMsg += ` - ${msg}`;
const baseURL = await setting("primaryBaseURL");
const baseURL = await Settings.get("primaryBaseURL");
if (baseURL && monitorJSON) {
textMsg += ` >> ${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
}

View File

@@ -87,6 +87,7 @@ class DingDing extends NotificationProvider {
* @returns {string} Status
*/
statusToString(status) {
// TODO: Move to notification-provider.js to avoid repetition in classes
switch (status) {
case DOWN:
return "DOWN";

View File

@@ -48,7 +48,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAddress(monitorJSON),
value: this.extractAdress(monitorJSON),
},
{
name: `Time (${heartbeatJSON["timezone"]})`,
@@ -85,7 +85,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAddress(monitorJSON),
value: this.extractAdress(monitorJSON),
},
{
name: `Time (${heartbeatJSON["timezone"]})`,

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
const { Settings } = require("../settings");
const successMessage = "Sent Successfully.";
class FlashDuty extends NotificationProvider {
@@ -84,7 +84,7 @@ class FlashDuty extends NotificationProvider {
}
};
const baseURL = await setting("primaryBaseURL");
const baseURL = await Settings.get("primaryBaseURL");
if (baseURL && monitorInfo) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setting } = require("../util-server");
const { getMonitorRelativeURL, UP } = require("../../src/util");
const { Settings } = require("../settings");
class GoogleChat extends NotificationProvider {
name = "GoogleChat";
@@ -45,7 +45,7 @@ class GoogleChat extends NotificationProvider {
}
// add button for monitor link if available
const baseURL = await setting("primaryBaseURL");
const baseURL = await Settings.get("primaryBaseURL");
if (baseURL) {
const urlPath = monitorJSON ? getMonitorRelativeURL(monitorJSON.id) : "/";
sectionWidgets.push({

View File

@@ -24,7 +24,7 @@ class NotificationProvider {
* @param {?object} monitorJSON Monitor details (For Up/Down only)
* @returns {string} The extracted address based on the monitor type.
*/
extractAddress(monitorJSON) {
extractAdress(monitorJSON) {
if (!monitorJSON) {
return "";
}

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
const { Settings } = require("../settings");
let successMessage = "Sent Successfully.";
class PagerDuty extends NotificationProvider {
@@ -95,7 +95,7 @@ class PagerDuty extends NotificationProvider {
}
};
const baseURL = await setting("primaryBaseURL");
const baseURL = await Settings.get("primaryBaseURL");
if (baseURL && monitorInfo) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
const { Settings } = require("../settings");
let successMessage = "Sent Successfully.";
class PagerTree extends NotificationProvider {
@@ -74,7 +74,7 @@ class PagerTree extends NotificationProvider {
}
};
const baseURL = await setting("primaryBaseURL");
const baseURL = await Settings.get("primaryBaseURL");
if (baseURL && monitorJSON) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorJSON.id);

View File

@@ -1,8 +1,8 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const Slack = require("./slack");
const { setting } = require("../util-server");
const { getMonitorRelativeURL, DOWN } = require("../../src/util");
const { Settings } = require("../settings");
class RocketChat extends NotificationProvider {
name = "rocket.chat";
@@ -49,7 +49,7 @@ class RocketChat extends NotificationProvider {
await Slack.deprecateURL(notification.rocketbutton);
}
const baseURL = await setting("primaryBaseURL");
const baseURL = await Settings.get("primaryBaseURL");
if (baseURL) {
data.attachments[0].title_link = baseURL + getMonitorRelativeURL(monitorJSON.id);

View File

@@ -32,7 +32,7 @@ class SevenIO extends NotificationProvider {
return okMsg;
}
let address = this.extractAddress(monitorJSON);
let address = this.extractAdress(monitorJSON);
if (address !== "") {
address = `(${address}) `;
}

View File

@@ -18,7 +18,7 @@ class SIGNL4 extends NotificationProvider {
msg,
// Source system
"X-S4-SourceSystem": "UptimeKuma",
monitorUrl: this.extractAddress(monitorJSON),
monitorUrl: this.extractAdress(monitorJSON),
};
const config = {

View File

@@ -1,7 +1,8 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setSettings, setting } = require("../util-server");
const { getMonitorRelativeURL, UP } = require("../../src/util");
const { Settings } = require("../settings");
const { log } = require("../../src/util");
class Slack extends NotificationProvider {
name = "slack";
@@ -14,15 +15,13 @@ class Slack extends NotificationProvider {
* @returns {Promise<void>}
*/
static async deprecateURL(url) {
let currentPrimaryBaseURL = await setting("primaryBaseURL");
let currentPrimaryBaseURL = await Settings.get("primaryBaseURL");
if (!currentPrimaryBaseURL) {
console.log("Move the url to be the primary base URL");
await setSettings("general", {
primaryBaseURL: url,
});
log.error("notification", "Move the url to be the primary base URL");
await Settings.set("primaryBaseURL", url, "general");
} else {
console.log("Already there, no need to move the primary base URL");
log.debug("notification", "Already there, no need to move the primary base URL");
}
}
@@ -48,7 +47,7 @@ class Slack extends NotificationProvider {
}
const address = this.extractAddress(monitorJSON);
const address = this.extractAdress(monitorJSON);
if (address) {
actions.push({
"type": "button",
@@ -136,26 +135,21 @@ class Slack extends NotificationProvider {
return okMsg;
}
const baseURL = await setting("primaryBaseURL");
const baseURL = await Settings.get("primaryBaseURL");
const title = "Uptime Kuma Alert";
let data = {
"text": `${title}\n${msg}`,
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
"attachments": [],
};
if (notification.slackrichmessage) {
data.attachments.push(
"attachments": [
{
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
"blocks": Slack.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
}
);
} else {
data.text = `${title}\n${msg}`;
}
]
};
if (notification.slackbutton) {
await Slack.deprecateURL(notification.slackbutton);

View File

@@ -93,7 +93,7 @@ class SMTP extends NotificationProvider {
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
monitorHostnameOrURL = this.extractAddress(monitorJSON);
monitorHostnameOrURL = this.extractAdress(monitorJSON);
}
let serviceStatus = "⚠️ Test";

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
const { Settings } = require("../settings");
let successMessage = "Sent Successfully.";
class Splunk extends NotificationProvider {
@@ -95,7 +95,7 @@ class Splunk extends NotificationProvider {
}
};
const baseURL = await setting("primaryBaseURL");
const baseURL = await Settings.get("primaryBaseURL");
if (baseURL && monitorInfo) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);

View File

@@ -34,7 +34,7 @@ class Squadcast extends NotificationProvider {
data.status = "resolve";
}
data.tags["AlertAddress"] = this.extractAddress(monitorJSON);
data.tags["AlertAddress"] = this.extractAdress(monitorJSON);
monitorJSON["tags"].forEach(tag => {
data.tags[tag["name"]] = {

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setting } = require("../util-server");
const { getMonitorRelativeURL } = require("../../src/util");
const { Settings } = require("../settings");
class Stackfield extends NotificationProvider {
name = "stackfield";
@@ -23,7 +23,7 @@ class Stackfield extends NotificationProvider {
textMsg += `\n${msg}`;
const baseURL = await setting("primaryBaseURL");
const baseURL = await Settings.get("primaryBaseURL");
if (baseURL) {
textMsg += `\n${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
}

View File

@@ -225,7 +225,7 @@ class Teams extends NotificationProvider {
const payload = this._notificationPayloadFactory({
heartbeatJSON: heartbeatJSON,
monitorName: monitorJSON.name,
monitorUrl: this.extractAddress(monitorJSON),
monitorUrl: this.extractAdress(monitorJSON),
dashboardUrl: dashboardUrl,
});

View File

@@ -10,22 +10,11 @@ class TechulusPush extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
let data = {
"title": notification?.pushTitle?.length ? notification.pushTitle : "Uptime-Kuma",
"body": msg,
"timeSensitive": notification.pushTimeSensitive ?? true,
};
if (notification.pushChannel) {
data.channel = notification.pushChannel;
}
if (notification.pushSound) {
data.sound = notification.pushSound;
}
try {
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, data);
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, {
"title": "Uptime-Kuma",
"body": msg,
});
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);

View File

@@ -32,17 +32,20 @@ class WeCom extends NotificationProvider {
* @returns {object} Message
*/
composeMessage(heartbeatJSON, msg) {
let title = "UptimeKuma Message";
let title;
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
title = "UptimeKuma Monitor Up";
}
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
title = "UptimeKuma Monitor Down";
}
if (msg != null) {
title = "UptimeKuma Message";
}
return {
msgtype: "text",
text: {
content: title + "\n" + msg
content: title + msg
}
};
}

View File

@@ -85,7 +85,7 @@ class ZohoCliq extends NotificationProvider {
const payload = this._notificationPayloadFactory({
monitorMessage: heartbeatJSON.msg,
monitorName: monitorJSON.name,
monitorUrl: this.extractAddress(monitorJSON),
monitorUrl: this.extractAdress(monitorJSON),
status: heartbeatJSON.status
});

View File

@@ -11,7 +11,6 @@ const CallMeBot = require("./notification-providers/call-me-bot");
const SMSC = require("./notification-providers/smsc");
const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
const Elks = require("./notification-providers/46elks");
const Feishu = require("./notification-providers/feishu");
const FreeMobile = require("./notification-providers/freemobile");
const GoogleChat = require("./notification-providers/google-chat");
@@ -96,7 +95,6 @@ class Notification {
new SMSC(),
new DingDing(),
new Discord(),
new Elks(),
new Feishu(),
new FreeMobile(),
new GoogleChat(),

View File

@@ -1,3 +1,4 @@
const { R } = require("redbean-node");
const PrometheusClient = require("prom-client");
const { log } = require("../src/util");
@@ -9,36 +10,102 @@ const commonLabels = [
"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 {
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 = {
monitor_name: monitor.name,
monitor_type: monitor.type,
@@ -46,6 +113,12 @@ class Prometheus {
monitor_hostname: monitor.hostname,
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}
*/
update(heartbeat, tlsInfo) {
if (typeof tlsInfo !== "undefined") {
try {
let isValid;
@@ -64,7 +136,7 @@ class Prometheus {
} else {
isValid = 0;
}
monitorCertIsValid.set(this.monitorLabelValues, isValid);
Prometheus.monitorCertIsValid.set(this.monitorLabelValues, isValid);
} catch (e) {
log.error("prometheus", "Caught error");
log.error("prometheus", e);
@@ -72,7 +144,7 @@ class Prometheus {
try {
if (tlsInfo.certInfo != null) {
monitorCertDaysRemaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
Prometheus.monitorCertDaysRemaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
}
} catch (e) {
log.error("prometheus", "Caught error");
@@ -82,7 +154,7 @@ class Prometheus {
if (heartbeat) {
try {
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
Prometheus.monitorStatus.set(this.monitorLabelValues, heartbeat.status);
} catch (e) {
log.error("prometheus", "Caught error");
log.error("prometheus", e);
@@ -90,10 +162,10 @@ class Prometheus {
try {
if (typeof heartbeat.ping === "number") {
monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
Prometheus.monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
} else {
// Is it good?
monitorResponseTime.set(this.monitorLabelValues, -1);
Prometheus.monitorResponseTime.set(this.monitorLabelValues, -1);
}
} catch (e) {
log.error("prometheus", "Caught error");
@@ -108,10 +180,10 @@ class Prometheus {
*/
remove() {
try {
monitorCertDaysRemaining.remove(this.monitorLabelValues);
monitorCertIsValid.remove(this.monitorLabelValues);
monitorResponseTime.remove(this.monitorLabelValues);
monitorStatus.remove(this.monitorLabelValues);
Prometheus.monitorCertDaysRemaining?.remove(this.monitorLabelValues);
Prometheus.monitorCertIsValid?.remove(this.monitorLabelValues);
Prometheus.monitorResponseTime?.remove(this.monitorLabelValues);
Prometheus.monitorStatus?.remove(this.monitorLabelValues);
} catch (e) {
console.error(e);
}

View File

@@ -2,7 +2,7 @@ const { R } = require("redbean-node");
const HttpProxyAgent = require("http-proxy-agent");
const HttpsProxyAgent = require("https-proxy-agent");
const SocksProxyAgent = require("socks-proxy-agent");
const { debug } = require("../src/util");
const { log } = require("../src/util");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const { CookieJar } = require("tough-cookie");
const { createCookieAgent } = require("http-cookie-agent/http");
@@ -110,9 +110,9 @@ class Proxy {
proxyOptions.auth = `${proxy.username}:${proxy.password}`;
}
debug(`Proxy Options: ${JSON.stringify(proxyOptions)}`);
debug(`HTTP Agent Options: ${JSON.stringify(httpAgentOptions)}`);
debug(`HTTPS Agent Options: ${JSON.stringify(httpsAgentOptions)}`);
log.debug("update-proxy", `Proxy Options: ${JSON.stringify(proxyOptions)}`);
log.debug("update-proxy", `HTTP Agent Options: ${JSON.stringify(httpAgentOptions)}`);
log.debug("update-proxy", `HTTPS Agent Options: ${JSON.stringify(httpsAgentOptions)}`);
switch (proxy.protocol) {
case "http":

View File

@@ -1,6 +1,5 @@
let express = require("express");
const {
setting,
allowDevAllOrigin,
allowAllOrigin,
percentageToColor,
@@ -18,6 +17,7 @@ const { makeBadge } = require("badge-maker");
const { Prometheus } = require("../prometheus");
const Database = require("../database");
const { UptimeCalculator } = require("../uptime-calculator");
const { Settings } = require("../settings");
let router = express.Router();
@@ -30,7 +30,7 @@ router.get("/api/entry-page", async (request, response) => {
let result = { };
let hostname = request.hostname;
if ((await setting("trustProxy")) && request.headers["x-forwarded-host"]) {
if ((await Settings.get("trustProxy")) && request.headers["x-forwarded-host"]) {
hostname = request.headers["x-forwarded-host"];
}

View File

@@ -90,8 +90,7 @@ const Monitor = require("./model/monitor");
const User = require("./model/user");
log.debug("server", "Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, doubleCheckPassword, shake256, SHAKE256_LENGTH, allowDevAllOrigin,
} = require("./util-server");
const { initJWTSecret, checkLogin, doubleCheckPassword, shake256, SHAKE256_LENGTH, allowDevAllOrigin } = require("./util-server");
log.debug("server", "Importing Notification");
const { Notification } = require("./notification");
@@ -201,7 +200,7 @@ let needSetup = false;
// Entry Page
app.get("/", async (request, response) => {
let hostname = request.hostname;
if (await setting("trustProxy")) {
if (await Settings.get("trustProxy")) {
const proxy = request.headers["x-forwarded-host"];
if (proxy) {
hostname = proxy;
@@ -281,7 +280,7 @@ let needSetup = false;
// Robots.txt
app.get("/robots.txt", async (_request, response) => {
let txt = "User-agent: *\nDisallow:";
if (!await setting("searchEngineIndex")) {
if (!await Settings.get("searchEngineIndex")) {
txt += " /";
}
response.setHeader("Content-Type", "text/plain");
@@ -727,7 +726,7 @@ let needSetup = false;
await updateMonitorNotification(bean.id, notificationIDList);
await server.sendUpdateMonitorIntoList(socket, bean.id);
await server.sendMonitorList(socket);
if (monitor.active !== false) {
await startMonitor(socket.userID, bean.id);
@@ -880,11 +879,11 @@ let needSetup = false;
await updateMonitorNotification(bean.id, monitor.notificationIDList);
if (await Monitor.isActive(bean.id, bean.active)) {
if (await bean.isActive()) {
await restartMonitor(socket.userID, bean.id);
}
await server.sendUpdateMonitorIntoList(socket, bean.id);
await server.sendMonitorList(socket);
callback({
ok: true,
@@ -924,17 +923,14 @@ let needSetup = false;
log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`);
let monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [
let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [
monitorID,
socket.userID,
]);
const monitorData = [{ id: monitor.id,
active: monitor.active
}];
const preloadData = await Monitor.preparePreloadData(monitorData);
callback({
ok: true,
monitor: monitor.toJSON(preloadData),
monitor: await bean.toJSON(),
});
} catch (e) {
@@ -985,7 +981,7 @@ let needSetup = false;
try {
checkLogin(socket);
await startMonitor(socket.userID, monitorID);
await server.sendUpdateMonitorIntoList(socket, monitorID);
await server.sendMonitorList(socket);
callback({
ok: true,
@@ -1005,7 +1001,7 @@ let needSetup = false;
try {
checkLogin(socket);
await pauseMonitor(socket.userID, monitorID);
await server.sendUpdateMonitorIntoList(socket, monitorID);
await server.sendMonitorList(socket);
callback({
ok: true,
@@ -1051,7 +1047,8 @@ let needSetup = false;
msg: "successDeleted",
msgi18n: true,
});
await server.sendDeleteMonitorFromList(socket, monitorID);
await server.sendMonitorList(socket);
} catch (e) {
callback({
@@ -1327,7 +1324,7 @@ let needSetup = false;
socket.on("getSettings", async (callback) => {
try {
checkLogin(socket);
const data = await getSettings("general");
const data = await Settings.getSettings("general");
if (!data.serverTimezone) {
data.serverTimezone = await server.getTimezone();
@@ -1355,7 +1352,7 @@ let needSetup = false;
// Disabled Auth + Want to Enable Auth => No Check
// Enabled Auth + Want to Disable Auth => Check!!
// Enabled Auth + Want to Enable Auth => No Check
const currentDisabledAuth = await setting("disableAuth");
const currentDisabledAuth = await Settings.get("disableAuth");
if (!currentDisabledAuth && data.disableAuth) {
await doubleCheckPassword(socket, currentPassword);
}
@@ -1369,7 +1366,7 @@ let needSetup = false;
const previousChromeExecutable = await Settings.get("chromeExecutable");
const previousNSCDStatus = await Settings.get("nscd");
await setSettings("general", data);
await Settings.setSettings("general", data);
server.entryPage = data.entryPage;
// Also need to apply timezone globally
@@ -1465,7 +1462,7 @@ let needSetup = false;
});
} catch (e) {
console.error(e);
log.error("server", e);
callback({
ok: false,
@@ -1578,7 +1575,7 @@ let needSetup = false;
// ***************************
log.debug("auth", "check auto login");
if (await setting("disableAuth")) {
if (await Settings.get("disableAuth")) {
log.info("auth", "Disabled Auth: auto login to admin");
await afterLogin(socket, await R.findOne("user"));
socket.emit("autoLogin");
@@ -1681,13 +1678,13 @@ async function afterLogin(socket, user) {
await StatusPage.sendStatusPageList(io, socket);
const monitorPromises = [];
for (let monitorID in monitorList) {
monitorPromises.push(sendHeartbeatList(socket, monitorID));
monitorPromises.push(Monitor.sendStats(io, monitorID, user.id));
await sendHeartbeatList(socket, monitorID);
}
await Promise.all(monitorPromises);
for (let monitorID in monitorList) {
await Monitor.sendStats(io, monitorID, user.id);
}
// Set server timezone from client browser if not set
// It should be run once only

View File

@@ -60,7 +60,7 @@ module.exports.apiKeySocketHandler = (socket) => {
ok: true,
});
} catch (e) {
console.error(e);
log.error("apikeys", e);
callback({
ok: false,
msg: e.message,

View File

@@ -1,7 +1,8 @@
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
const { checkLogin, doubleCheckPassword } = require("../util-server");
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { log } = require("../../src/util");
const { Settings } = require("../settings");
const io = UptimeKumaServer.getInstance().io;
const prefix = "cloudflared_";
@@ -40,7 +41,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
socket.join("cloudflared");
io.to(socket.userID).emit(prefix + "installed", cloudflared.checkInstalled());
io.to(socket.userID).emit(prefix + "running", cloudflared.running);
io.to(socket.userID).emit(prefix + "token", await setting("cloudflaredTunnelToken"));
io.to(socket.userID).emit(prefix + "token", await Settings.get("cloudflaredTunnelToken"));
} catch (error) { }
});
@@ -55,7 +56,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
try {
checkLogin(socket);
if (token && typeof token === "string") {
await setSetting("cloudflaredTunnelToken", token);
await Settings.set("cloudflaredTunnelToken", token);
cloudflared.token = token;
} else {
cloudflared.token = null;
@@ -67,7 +68,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
socket.on(prefix + "stop", async (currentPassword, callback) => {
try {
checkLogin(socket);
const disabledAuth = await setting("disableAuth");
const disabledAuth = await Settings.get("disableAuth");
if (!disabledAuth) {
await doubleCheckPassword(socket, currentPassword);
}
@@ -83,7 +84,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
socket.on(prefix + "removeToken", async () => {
try {
checkLogin(socket);
await setSetting("cloudflaredTunnelToken", "");
await Settings.set("cloudflaredTunnelToken", "");
} catch (error) { }
});
@@ -96,15 +97,15 @@ module.exports.cloudflaredSocketHandler = (socket) => {
*/
module.exports.autoStart = async (token) => {
if (!token) {
token = await setting("cloudflaredTunnelToken");
token = await Settings.get("cloudflaredTunnelToken");
} else {
// Override the current token via args or env var
await setSetting("cloudflaredTunnelToken", token);
console.log("Use cloudflared token from args or env var");
await Settings.set("cloudflaredTunnelToken", token);
log.info("cloudflare", "Use cloudflared token from args or env var");
}
if (token) {
console.log("Start cloudflared");
log.info("cloudflare", "Start cloudflared");
cloudflared.token = token;
cloudflared.start();
}

View File

@@ -67,7 +67,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
});
} catch (e) {
console.error(e);
log.error("maintenance", e);
callback({
ok: false,
msg: e.message,
@@ -177,7 +177,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
ok: true,
});
} catch (e) {
console.error(e);
log.error("maintenance", e);
callback({
ok: false,
msg: e.message,
@@ -201,7 +201,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
});
} catch (e) {
console.error(e);
log.error("maintenance", e);
callback({
ok: false,
msg: e.message,
@@ -225,7 +225,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
});
} catch (e) {
console.error(e);
log.error("maintenance", e);
callback({
ok: false,
msg: e.message,

View File

@@ -1,5 +1,5 @@
const { R } = require("redbean-node");
const { checkLogin, setSetting } = require("../util-server");
const { checkLogin } = require("../util-server");
const dayjs = require("dayjs");
const { log } = require("../../src/util");
const ImageDataURI = require("../image-data-uri");
@@ -7,6 +7,7 @@ const Database = require("../database");
const apicache = require("../modules/apicache");
const StatusPage = require("../model/status_page");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { Settings } = require("../settings");
/**
* Socket handlers for status page
@@ -233,7 +234,7 @@ module.exports.statusPageSocketHandler = (socket) => {
// Also change entry page to new slug if it is the default one, and slug is changed.
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
server.entryPage = "statusPage-" + statusPage.slug;
await setSetting("entryPage", server.entryPage, "general");
await Settings.set("entryPage", server.entryPage, "general");
}
apicache.clear();
@@ -291,7 +292,7 @@ module.exports.statusPageSocketHandler = (socket) => {
});
} catch (error) {
console.error(error);
log.error("socket", error);
callback({
ok: false,
msg: error.message,
@@ -313,7 +314,7 @@ module.exports.statusPageSocketHandler = (socket) => {
// Reset entry page if it is the default one.
if (server.entryPage === "statusPage-" + slug) {
server.entryPage = "dashboard";
await setSetting("entryPage", server.entryPage, "general");
await Settings.set("entryPage", server.entryPage, "general");
}
// No need to delete records from `status_page_cname`, because it has cascade foreign key.

View File

@@ -205,56 +205,24 @@ class UptimeKumaServer {
return list;
}
/**
* Update Monitor into list
* @param {Socket} socket Socket to send list on
* @param {number} monitorID update or deleted monitor id
* @returns {Promise<void>}
*/
async sendUpdateMonitorIntoList(socket, monitorID) {
let list = await this.getMonitorJSONList(socket.userID, monitorID);
this.io.to(socket.userID).emit("updateMonitorIntoList", list);
}
/**
* Delete Monitor from list
* @param {Socket} socket Socket to send list on
* @param {number} monitorID update or deleted monitor id
* @returns {Promise<void>}
*/
async sendDeleteMonitorFromList(socket, monitorID) {
this.io.to(socket.userID).emit("deleteMonitorFromList", monitorID);
}
/**
* Get a list of monitors for the given user.
* @param {string} userID - The ID of the user to get monitors for.
* @param {number} monitorID - The ID of monitor for.
* @returns {Promise<object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
*
* Generated by Trelent
*/
async getMonitorJSONList(userID, monitorID = null) {
async getMonitorJSONList(userID) {
let result = {};
let query = " user_id = ? ";
let queryParams = [ userID ];
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
userID,
]);
if (monitorID) {
query += "AND id = ? ";
queryParams.push(monitorID);
for (let monitor of monitorList) {
result[monitor.id] = await monitor.toJSON();
}
let monitorList = await R.find("monitor", query + "ORDER BY weight DESC, name", queryParams);
const monitorData = monitorList.map(monitor => ({
id: monitor.id,
active: monitor.active,
name: monitor.name,
}));
const preloadData = await Monitor.preparePreloadData(monitorData);
const result = {};
monitorList.forEach(monitor => result[monitor.id] = monitor.toJSON(preloadData));
return result;
}
@@ -552,4 +520,3 @@ const { DnsMonitorType } = require("./monitor-types/dns");
const { MqttMonitorType } = require("./monitor-types/mqtt");
const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb");
const Monitor = require("./model/monitor");

View File

@@ -12,7 +12,6 @@ const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
const mysql = require("mysql2");
const { NtlmClient } = require("./modules/axios-ntlm/lib/ntlmClient.js");
const { Settings } = require("./settings");
const grpc = require("@grpc/grpc-js");
const protojs = require("protobufjs");
const radiusClient = require("node-radius-client");
@@ -521,46 +520,6 @@ exports.redisPingAsync = function (dsn, rejectUnauthorized) {
});
};
/**
* Retrieve value of setting based on key
* @param {string} key Key of setting to retrieve
* @returns {Promise<any>} Value
* @deprecated Use await Settings.get(key)
*/
exports.setting = async function (key) {
return await Settings.get(key);
};
/**
* Sets the specified setting to specified value
* @param {string} key Key of setting to set
* @param {any} value Value to set to
* @param {?string} type Type of setting
* @returns {Promise<void>}
*/
exports.setSetting = async function (key, value, type = null) {
await Settings.set(key, value, type);
};
/**
* Get settings based on type
* @param {string} type The type of setting
* @returns {Promise<Bean>} Settings of requested type
*/
exports.getSettings = async function (type) {
return await Settings.getSettings(type);
};
/**
* Set settings based on type
* @param {string} type Type of settings to set
* @param {object} data Values of settings
* @returns {Promise<void>}
*/
exports.setSettings = async function (type, data) {
await Settings.setSettings(type, data);
};
// ssl-checker by @dyaa
//https://github.com/dyaa/ssl-checker/blob/master/src/index.ts

View File

@@ -118,7 +118,6 @@ export default {
"clicksendsms": "ClickSend SMS",
"CallMeBot": "CallMeBot (WhatsApp, Telegram Call, Facebook Messanger)",
"discord": "Discord",
"Elks": "46elks",
"GoogleChat": "Google Chat (Google Workspace)",
"gorush": "Gorush",
"gotify": "Gotify",

View File

@@ -33,7 +33,7 @@
<template #item="monitor">
<div class="item" data-testid="monitor">
<div class="row">
<div class="col-9 col-md-8 small-padding">
<div class="col-6 col-md-4 small-padding">
<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="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
@@ -71,7 +71,7 @@
</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" />
</div>
</div>

View File

@@ -1,48 +0,0 @@
<template>
<div class="mb-3">
<label for="ElksUsername" class="form-label">{{ $t("Username") }}</label>
<input id="ElksUsername" v-model="$parent.notification.elksUsername" type="text" class="form-control" required>
<label for="ElksPassword" class="form-label">{{ $t("Password") }}</label>
</div>
<div class="form-text">
<HiddenInput id="ElksPassword" v-model="$parent.notification.elksAuthToken" :required="true" autocomplete="new-password"></HiddenInput>
<i18n-t tag="p" keypath="Can be found on:">
<a href="https://46elks.com/account" target="_blank">https://46elks.com/account</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="Elks-from-number" class="form-label">{{ $t("From") }}</label>
<input id="Elks-from-number" v-model="$parent.notification.elksFromNumber" type="text" class="form-control" required>
<div class="form-text">
{{ $t("Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.") }}
<i18n-t tag="p" keypath="More info on:">
<a href="https://46elks.se/kb/text-sender-id" target="_blank">https://46elks.se/kb/text-sender-id</a>
</i18n-t>
</div>
</div>
<div class="mb-3">
<label for="Elks-to-number" class="form-label">{{ $t("To Number") }}</label>
<input id="Elks-to-number" v-model="$parent.notification.elksToNumber" type="text" class="form-control" required>
<div class="form-text">
{{ $t("The phone number of the recipient in E.164 format.") }}
<i18n-t tag="p" keypath="More info on:">
<a href="https://46elks.se/kb/e164" target="_blank">https://46elks.se/kb/e164</a>
</i18n-t>
</div>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://46elks.com/docs/send-sms" target="_blank">https://46elks.com/docs/send-sms</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View File

@@ -9,12 +9,6 @@
<label for="slack-channel" class="form-label">{{ $t("Channel Name") }}</label>
<input id="slack-channel-name" v-model="$parent.notification.slackchannel" type="text" class="form-control">
<label class="form-label">{{ $t("Message format") }}</label>
<div class="form-check form-switch">
<input id="slack-text-message" v-model="$parent.notification.slackrichmessage" type="checkbox" class="form-check-input">
<label for="slack-text-message" class="form-label">{{ $t("Send rich messages") }}</label>
</div>
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">

View File

@@ -4,53 +4,6 @@
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="push-api-title" class="form-label">{{ $t("Title") }}</label>
<input id="push-api-title" v-model="$parent.notification.pushTitle" type="text" class="form-control">
</div>
<div class="mb-3">
<label for="push-api-channel" class="form-label">{{ $t("Notification Channel") }}</label>
<input id="push-api-channel" v-model="$parent.notification.pushChannel" type="text" class="form-control" patttern="[A-Za-z0-9-]+">
<div class="form-text">
{{ $t("Alphanumerical string and hyphens only") }}
</div>
</div>
<div class="mb-3">
<label for="push-api-sound" class="form-label">{{ $t("Sound") }}</label>
<select id="push-api-sound" v-model="$parent.notification.pushSound" class="form-select">
<option value="default">{{ $t("Default") }}</option>
<option value="arcade">{{ $t("Arcade") }}</option>
<option value="correct">{{ $t("Correct") }}</option>
<option value="fail">{{ $t("Fail") }}</option>
<option value="harp">{{ $t("Harp") }}</option>
<option value="reveal">{{ $t("Reveal") }}</option>
<option value="bubble">{{ $t("Bubble") }}</option>
<option value="doorbell">{{ $t("Doorbell") }}</option>
<option value="flute">{{ $t("Flute") }}</option>
<option value="money">{{ $t("Money") }}</option>
<option value="scifi">{{ $t("Scifi") }}</option>
<option value="clear">{{ $t("Clear") }}</option>
<option value="elevator">{{ $t("Elevator") }}</option>
<option value="guitar">{{ $t("Guitar") }}</option>
<option value="pop">{{ $t("Pop") }}</option>
</select>
<div class="form-text">
{{ $t("Custom sound to override default notification sound") }}
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input v-model="$parent.notification.pushTimeSensitive" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t("Time Sensitive (iOS Only)") }}</label>
</div>
<div class="form-text">
{{ $t("Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.") }}
</div>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://docs.push.techulus.com" target="_blank">https://docs.push.techulus.com</a>
</i18n-t>
@@ -63,19 +16,5 @@ export default {
components: {
HiddenInput,
},
mounted() {
if (typeof this.$parent.notification.pushTitle === "undefined") {
this.$parent.notification.pushTitle = "Uptime-Kuma";
}
if (typeof this.$parent.notification.pushChannel === "undefined") {
this.$parent.notification.pushChannel = "uptime-kuma";
}
if (typeof this.$parent.notification.pushSound === "undefined") {
this.$parent.notification.pushSound = "default";
}
if (typeof this.$parent.notification.pushTimeSensitive === "undefined") {
this.$parent.notification.pushTimeSensitive = true;
}
},
};
</script>

View File

@@ -9,7 +9,6 @@ import CallMeBot from "./CallMeBot.vue";
import SMSC from "./SMSC.vue";
import DingDing from "./DingDing.vue";
import Discord from "./Discord.vue";
import Elks from "./46elks.vue";
import Feishu from "./Feishu.vue";
import FreeMobile from "./FreeMobile.vue";
import GoogleChat from "./GoogleChat.vue";
@@ -83,7 +82,6 @@ const NotificationFormList = {
"smsc": SMSC,
"DingDing": DingDing,
"discord": Discord,
"Elks": Elks,
"Feishu": Feishu,
"FreeMobile": FreeMobile,
"GoogleChat": GoogleChat,

View File

@@ -1 +0,0 @@
{}

View File

@@ -1011,45 +1011,5 @@
"OAuth2: Client Credentials": "OAuth2: přihlašovací údaje klienta",
"Authentication Method": "Metoda ověřování",
"Authorization Header": "Hlavička autorizace",
"Form Data Body": "Tělo formuláře s daty",
"threemaRecipientTypePhoneFormat": "E.164, bez počátečního +",
"jsonQueryDescription": "Pro zpracování a získání konkrétních dat z JSON odpovědi serveru použijte JSON dotaz - případně \"$\" pro zdrojovou (raw) odpověď, pokud neočekáváte JSON výstup. Výsledek bude následně porovnán jako řetězec vůči očekávaní hodnotě. Dokumentaci naleznete na {0} a pro testování dotazů můžete využít {1}.",
"shrinkDatabaseDescriptionSqlite": "Podmínka spuštění příkazu {vacuum} nad SQLite databází. Příkaz {auto_vacuum} je již zapnutý, ale nedochází k defragmentaci databáze ani k přebalení jednotlivých stránek databáze tak, jak to dělá příkaz {vacuum}.",
"Community String": "Řetězec komunity",
"Host Onesender": "Onesender hostitel",
"Token Onesender": "Onesender token",
"snmpOIDHelptext": "Zadejte OID senzoru nebo stavu, který chcete monitorovat. Pokud si nejste jisti identifikátorem OID, použijte nástroje pro správu sítě, jako jsou prohlížeče MIB nebo SNMP software.",
"snmpCommunityStringHelptext": "Tento řetězec slouží jako heslo pro ověřování a řízení přístupu k zařízením podporujícím protokol SNMP. Shodujte se s konfigurací zařízení SNMP.",
"record": "záznam",
"Go back to home page.": "Vrátit se domovskou stránku.",
"No tags found.": "Nenalezeny žádné štítky.",
"Lost connection to the socket server.": "Ztraceno socketové spojení se serverem.",
"Cannot connect to the socket server.": "Nelze navázat socketové spojení se serverem.",
"SIGNL4": "SIGNL4",
"SIGNL4 Webhook URL": "URL adresa webhooku SIGNL4",
"signl4Docs": "Další informace související s konfigurací SIGNL4 a postup jak získat URL webhooku SIGNL4 naleznete na {0}.",
"Conditions": "Podmínky",
"conditionAdd": "Přidat podmínku",
"conditionDelete": "Vymazat podmínku",
"conditionAddGroup": "Přidat skupinu",
"conditionDeleteGroup": "Smazat skupinu",
"conditionValuePlaceholder": "Hodnota",
"equals": "rovná se",
"not equals": "nerovná se",
"contains": "obsahuje",
"not contains": "neobsahuje",
"starts with": "začíná na",
"not starts with": "nezačíná na",
"ends with": "končí na",
"not ends with": "nekončí na",
"less than": "menší než",
"greater than": "větší než",
"less than or equal to": "menší nebo rovno",
"greater than or equal to": "větší nebo rovno",
"groupOnesenderDesc": "Ujistěte se, že jste zadali platné GroupID. Pro odeslání zprávy do skupiny zadejte například 628123456789-342345",
"OAuth Token URL": "URL OAuth tokenu",
"Client ID": "ID klienta",
"Client Secret": "Tajemství klienta",
"OAuth Scope": "OAuth rozsah",
"Optional: Space separated list of scopes": "Volitelné: seznam rozsahů oddělte mezerami"
"Form Data Body": "Tělo formuláře s daty"
}

View File

@@ -1,7 +1,7 @@
{
"languageName": "Deutsch (Schweiz)",
"Settings": "Einstellungen",
"Dashboard": "Überblick",
"Dashboard": "Dashboard",
"New Update": "Update verfügbar",
"Language": "Sprache",
"Appearance": "Erscheinungsbild",
@@ -1047,6 +1047,5 @@
"greater than": "mehr als",
"less than or equal to": "kleiner als oder gleich",
"greater than or equal to": "grösser als oder gleich",
"record": "Eintrag",
"shrinkDatabaseDescriptionSqlite": "Datenbank {vacuum} für SQLite auslösen. {auto_vacuum} ist bereits aktiviert, aber dies defragmentiert die Datenbank nicht und packt auch nicht einzelne Datenbankseiten neu, wie es der Befehl {vacuum} tut."
"record": "Eintrag"
}

View File

@@ -1,7 +1,7 @@
{
"languageName": "Deutsch",
"Settings": "Einstellungen",
"Dashboard": "Überblick",
"Dashboard": "Dashboard",
"New Update": "Aktualisierung verfügbar",
"Language": "Sprache",
"Appearance": "Erscheinungsbild",
@@ -1050,6 +1050,5 @@
"less than": "weniger als",
"less than or equal to": "kleiner als oder gleich",
"greater than or equal to": "größer als oder gleich",
"record": "Eintrag",
"shrinkDatabaseDescriptionSqlite": "Datenbank {vacuum} für SQLite auslösen. {auto_vacuum} ist bereits aktiviert, aber dies defragmentiert die Datenbank nicht und packt auch nicht einzelne Datenbankseiten neu, wie es der Befehl {vacuum} tut."
"record": "Eintrag"
}

View File

@@ -82,7 +82,6 @@
"resendEveryXTimes": "Resend every {0} times",
"resendDisabled": "Resend disabled",
"retriesDescription": "Maximum retries before the service is marked as down and a notification is sent",
"ignoredTLSError": "TLS/SSL errors have been ignored",
"ignoreTLSError": "Ignore TLS/SSL errors for HTTPS websites",
"ignoreTLSErrorGeneral": "Ignore TLS/SSL error for connection",
"upsideDownModeDescription": "Flip the status upside down. If the service is reachable, it is DOWN.",
@@ -97,8 +96,6 @@
"pushOthers": "Others",
"programmingLanguages": "Programming Languages",
"Save": "Save",
"Debug": "Debug",
"Copy": "Copy",
"Notifications": "Notifications",
"Not available, please setup.": "Not available, please set up.",
"Setup Notification": "Set Up Notification",
@@ -251,14 +248,6 @@
"PushUrl": "Push URL",
"HeadersInvalidFormat": "The request headers are not valid JSON: ",
"BodyInvalidFormat": "The request body is not valid JSON: ",
"CopyToClipboardError": "Couldn't copy to clipbard: {error}",
"CopyToClipboardSuccess": "Copied!",
"CurlDebugInfo": "To debug the monitor, you can either paste this into your own machines terminal or into the machines terminal which uptime kuma is running on and see what you are requesting.{newiline}Please be aware of networking differences like {firewalls}, {dns_resolvers} or {docker_networks}.",
"firewalls": "firewalls",
"dns resolvers": "dns resolvers",
"docker networks": "docker networks",
"CurlDebugInfoOAuth2CCUnsupported": "Full oauth client credential flow is not supported in {curl}.{newline}Please get a bearer token and pass it via the {oauth2_bearer} option.",
"CurlDebugInfoProxiesUnsupported": "Proxy support in the above {curl} command is currently not implemented.",
"Monitor History": "Monitor History",
"clearDataOlderThan": "Keep monitor history data for {0} days.",
"PasswordsDoNotMatch": "Passwords do not match.",
@@ -895,8 +884,6 @@
"cacheBusterParamDescription": "Randomly generated parameter to skip caches.",
"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.",
"Message format": "Message format",
"Send rich messages": "Send rich messages",
"Bitrix24 Webhook URL": "Bitrix24 Webhook URL",
"wayToGetBitrix24Webhook": "You can create a webhook by following the steps at {0}",
"bitrix24SupportUserID": "Enter your user ID in Bitrix24. You can find out the ID from the link by going to the user's profile.",
@@ -1027,29 +1014,5 @@
"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",
"Notification Channel": "Notification Channel",
"Sound": "Sound",
"Alphanumerical string and hyphens only": "Alphanumerical string and hyphens only",
"Arcade": "Arcade",
"Correct": "Correct",
"Fail":"Fail",
"Harp":"Harp",
"Reveal":"Reveal",
"Bubble":"Bubble",
"Doorbell":"Doorbell",
"Flute":"Flute",
"Money":"Money",
"Scifi":"Scifi",
"Clear":"Clear",
"Elevator":"Elevator",
"Guitar":"Guitar",
"Pop":"Pop",
"Custom sound to override default notification sound": "Custom sound to override default notification sound",
"Time Sensitive (iOS Only)": "Time Sensitive (iOS Only)",
"Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.": "Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.",
"From":"From",
"Can be found on:": "Can be found on: {0}",
"The phone number of the recipient in E.164 format.": "The phone number of the recipient in E.164 format.",
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.":"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies."
"record": "record"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -106,7 +106,7 @@
"disableauth.message2": "Egoera jakin batzuetarako diseinatuta dago, Uptime Kumaren {intendThirdPartyAuth} (Cloudflare Access, Authelia edo beste autentifikazio-mekanismo batzuk).",
"where you intend to implement third-party authentication": "aurrean hirugarrengo autentifikazio batzuek jartzeko",
"Please use this option carefully!": "Mesedez, kontuz erabili aukera hau!",
"Logout": "Itxi saioa",
"Logout": "Saioa amaitu",
"Leave": "Utzi",
"I understand, please disable": "Ulertzen dut, mesedez desgaitu",
"Confirm": "Baieztatu",
@@ -115,7 +115,7 @@
"Username": "Erabiltzailea",
"Password": "Pasahitza",
"Remember me": "Gogora nazazu",
"Login": "Hasi saioa",
"Login": "Saioa hasi",
"No Monitors, please": "Monitorizaziorik ez, mesedez",
"add one": "gehitu bat",
"Notification Type": "Jakinarazpen mota",
@@ -164,11 +164,11 @@
"Add New below or Select...": "Gehitu beste bat behean edo hautatu…",
"Tag with this name already exist.": "Izen hau duen etiketa dagoeneko badago.",
"Tag with this value already exist.": "Balio hau duen etiketa dagoeneko badago.",
"color": "Kolorea",
"color": "kolorea",
"value (optional)": "balioa (hautazkoa)",
"Gray": "Grisa",
"Red": "Gorria",
"Orange": "Laranja",
"Orange": "Naranja",
"Green": "Berdea",
"Blue": "Urdina",
"Indigo": "Indigo",
@@ -190,7 +190,7 @@
"Status Page": "Egoera orria",
"Status Pages": "Egoera orriak",
"defaultNotificationName": "Nire {notification} Alerta ({number})",
"here": "hemen",
"here": "Hemen",
"Required": "Beharrezkoa",
"telegram": "Telegram",
"ZohoCliq": "ZohoCliq",
@@ -582,10 +582,6 @@
"Mechanism": "Mekanismoa",
"Home": "Hasiera",
"filterActive": "Aktibo",
"filterActivePaused": "Pausatua",
"Expected Value": "Esperotako balioa",
"statusPageRefreshIn": "{0} barru freskatuko da.",
"now": "orain",
"time ago": "duela {0}",
"-year": "-urte"
"filterActivePaused": "Geldituta",
"Expected Value": "Esperotako balioa"
}

View File

@@ -1050,6 +1050,5 @@
"greater than": "supérieur à",
"less than or equal to": "inférieur ou égal à",
"greater than or equal to": "supérieur ou égal à",
"record": "enregistrer",
"shrinkDatabaseDescriptionSqlite": "Déclencher la commande {vacuum} pour la base de données SQLite. {auto_vacuum} est déjà activé, mais cela ne défragmente pas la base de données ni ne réorganise les pages individuelles de la base de données de la même manière que la commande {vacuum}."
"record": "enregistrer"
}

View File

@@ -1015,6 +1015,5 @@
"less than": "níos lú ná",
"greater than": "níos mó ná",
"less than or equal to": "níos lú ná nó cothrom le",
"record": "taifead",
"shrinkDatabaseDescriptionSqlite": "Bunachar sonraí truicear {vacuum} le haghaidh SQLite. Tá {auto_vacuum} cumasaithe cheana féin ach ní dhéanann sé seo scoilt ar an mbunachar sonraí ná athphacáil leathanaigh aonair an bhunachair sonraí mar a dhéanann an t-ordú {vacuum}."
"record": "taifead"
}

View File

@@ -1045,6 +1045,5 @@
"New Group": "Grup Baru",
"Group Name": "Nama Grup",
"OAuth2: Client Credentials": "OAuth2: Kredensial Klien",
"Authentication Method": "Metode Autentikasi",
"shrinkDatabaseDescriptionSqlite": "Memicu pangkalan data {vacuum} untuk SQLite. {auto_vacuum} sudah diaktifkan, tetapi tidak mendefragmentasi pangkalan data atau mengemas ulang halaman individual dari pangkalan data seperti yang dilakukan oleh perintah {vacuum}."
"Authentication Method": "Metode Autentikasi"
}

View File

@@ -989,9 +989,5 @@
"wayToGetThreemaGateway": "Możesz zarejestrować się w Threema Gateway {0}.",
"threemaSenderIdentityFormat": "8 znaków, zwykle zaczyna się od *",
"threemaBasicModeInfo": "Uwaga: Ta integracja korzysta z Threema Gateway w trybie podstawowym (szyfrowanie po stronie serwera). Więcej szczegółów można znaleźć {0}.",
"apiKeysDisabledMsg": "Klucze API są wyłączone, ponieważ wyłączone jest uwierzytelnianie.",
"-year": "-rok",
"and": "i",
"now": "teraz",
"cacheBusterParam": "Dodaj parametr {0}"
"apiKeysDisabledMsg": "Klucze API są wyłączone, ponieważ wyłączone jest uwierzytelnianie."
}

View File

@@ -1050,6 +1050,5 @@
"less than": "daha küçük",
"greater than or equal to": "büyük veya eşit",
"record": "kayıt",
"jsonQueryDescription": "JSON sorgusunu kullanarak sunucunun JSON yanıtından belirli verileri ayrıştırın ve çıkarın. JSON beklemiyorsanız ham yanıt için \"$\" sembolünü kullanın. Sonuç daha sonra metin olarak beklenen değerle karşılaştırılır. Belgeler için {0}'a bakın ve sorgularla denemeler yapmak için {1}'i kullanın.",
"shrinkDatabaseDescriptionSqlite": "SQLite için {vacuum} veritabanını tetikle. {auto_vacuum} zaten etkin ancak bu, {vacuum} komutunun yaptığı gibi veritabanını birleştirmez veya tek tek veritabanı sayfalarını yeniden paketlemez."
"jsonQueryDescription": "JSON sorgusunu kullanarak sunucunun JSON yanıtından belirli verileri ayrıştırın ve çıkarın. JSON beklemiyorsanız ham yanıt için \"$\" sembolünü kullanın. Sonuç daha sonra metin olarak beklenen değerle karşılaştırılır. Belgeler için {0}'a bakın ve sorgularla denemeler yapmak için {1}'i kullanın."
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1056,6 +1056,5 @@
"greater than": "більше, ніж",
"less than or equal to": "менше або дорівнює",
"greater than or equal to": "більше або дорівнює",
"record": "запис",
"shrinkDatabaseDescriptionSqlite": "Запускає команду {vacuum} для бази даних SQLite. Команда {auto_vacuum} вже увімкнена, але вона не дефрагментує базу даних і не перепаковує окремі сторінки бази даних так, як це робить команда {vacuum}."
"record": "запис"
}

View File

@@ -1052,6 +1052,5 @@
"less than or equal to": "不多于",
"greater than or equal to": "不少于",
"record": "记录",
"jsonQueryDescription": "使用 JSON 查询解析并提取服务器 JSON 响应中的特定数据,或者,如果不期望得到 JSON 响应,则可使用 \"$\" 获取原始响应。然后将结果转为字符串并与期望值进行字符串比较。有关更多文档,请参阅 {0},亦可使用 {1} 来尝试查询。",
"shrinkDatabaseDescriptionSqlite": "触发 SQLite 数据库的 {vacuum} 命令。{auto_vacuum} 已经启用,但它不会像 {vacuum} 命令那样对数据库进行碎片整理,也不会重新打包各个数据库页面。"
"jsonQueryDescription": "使用 JSON 查询解析并提取服务器 JSON 响应中的特定数据,或者,如果不期望得到 JSON 响应,则可使用 \"$\" 获取原始响应。然后将结果转为字符串并与期望值进行字符串比较。有关更多文档,请参阅 {0},亦可使用 {1} 来尝试查询。"
}

View File

@@ -141,21 +141,17 @@ export default {
});
socket.on("monitorList", (data) => {
this.assignMonitorUrlParser(data);
this.monitorList = data;
});
socket.on("updateMonitorIntoList", (data) => {
this.assignMonitorUrlParser(data);
Object.entries(data).forEach(([ monitorID, updatedMonitor ]) => {
this.monitorList[monitorID] = updatedMonitor;
// Add Helper function
Object.entries(data).forEach(([ monitorID, monitor ]) => {
monitor.getUrl = () => {
try {
return new URL(monitor.url);
} catch (_) {
return null;
}
};
});
});
socket.on("deleteMonitorFromList", (monitorID) => {
if (this.monitorList[monitorID]) {
delete this.monitorList[monitorID];
}
this.monitorList = data;
});
socket.on("monitorTypeList", (data) => {
@@ -293,23 +289,6 @@ export default {
location.reload();
});
},
/**
* parse all urls from list.
* @param {object} data Monitor data to modify
* @returns {object} list
*/
assignMonitorUrlParser(data) {
Object.entries(data).forEach(([ monitorID, monitor ]) => {
monitor.getUrl = () => {
try {
return new URL(monitor.url);
} catch (_) {
return null;
}
};
});
return data;
},
/**
* The storage currently in use

View File

@@ -565,8 +565,8 @@
<h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check" :title="monitor.ignoreTls ? $t('ignoredTLSError') : ''">
<input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox" :disabled="monitor.ignoreTls">
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check">
<input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox">
<label class="form-check-label" for="expiry-notification">
{{ $t("Certificate Expiry Notification") }}
</label>
@@ -982,23 +982,13 @@
<div class="fixed-bottom-bar p-3">
<button
id="monitor-submit-btn"
class="btn btn-primary me-2"
class="btn btn-primary"
type="submit"
:disabled="processing"
data-testid="save-button"
>
{{ $t("Save") }}
</button>
<button
v-if="monitor.type === 'http'"
id="monitor-debug-btn"
class="btn btn-outline-primary"
type="button"
:disabled="processing"
@click.stop="modal.show()"
>
{{ $t("Debug") }}
</button>
</div>
</div>
</form>
@@ -1010,58 +1000,9 @@
<RemoteBrowserDialog ref="remoteBrowserDialog" />
</div>
</transition>
<div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body">
<textarea id="curl-debug" v-model="curlCommand" class="form-control mb-3" readonly wrap="off"></textarea>
<button id="debug-copy-btn" class="btn btn-outline-primary position-absolute top-0 end-0 mt-3 me-3 border-0" type="button" @click.stop="copyToClipboard">
<font-awesome-icon icon="copy" />
</button>
<i18n-t keypath="CurlDebugInfo" tag="p" class="form-text">
<template #newiline>
<br>
</template>
<template #firewalls>
<a href="https://xkcd.com/2259/" target="_blank">{{ $t('firewalls') }}</a>
</template>
<template #dns_resolvers>
<a href="https://www.reddit.com/r/sysadmin/comments/rxho93/thank_you_for_the_running_its_always_dns_joke_its/" target="_blank">{{ $t('dns resolvers') }}</a>
</template>
<template #docker_networks>
<a href="https://youtu.be/bKFMS5C4CG0" target="_blank">{{ $t('docker networks') }}</a>
</template>
</i18n-t>
<div v-if="monitor.authMethod === 'oauth2-cc'" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoOAuth2CCUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
<template #newline>
<br>
</template>
<template #oauth2_bearer>
<code>--oauth2-bearer TOKEN</code>
</template>
</i18n-t>
</div>
<div v-if="monitor.proxyId" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoProxiesUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
</i18n-t>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from "bootstrap";
import VueMultiselect from "vue-multiselect";
import { useToast } from "vue-toastification";
import ActionSelect from "../components/ActionSelect.vue";
@@ -1076,10 +1017,8 @@ import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, sleep } fro
import { hostNameRegexPattern } from "../util-frontend";
import HiddenInput from "../components/HiddenInput.vue";
import EditMonitorConditions from "../components/EditMonitorConditions.vue";
import { version } from "../../package.json";
const userAgent = `'Uptime-Kuma/${version}'`;
const toast = useToast();
const toast = useToast;
const pushTokenLength = 32;
@@ -1142,7 +1081,6 @@ export default {
data() {
return {
modal: null,
minInterval: MIN_INTERVAL_SECOND,
maxInterval: MAX_INTERVAL_SECOND,
processing: false,
@@ -1170,53 +1108,6 @@ export default {
computed: {
curlCommand() {
const command = [ "curl", "--verbose", "--head", "--request", this.monitor.method, "\\\n", "--user-agent", userAgent, "\\\n" ];
if (this.monitor.ignoreTls) {
command.push("--insecure", "\\\n");
}
if (this.monitor.headers) {
try {
// trying to parse the supplied data as json to trim whitespace
for (const [ key, value ] of Object.entries(JSON.parse(this.monitor.headers))) {
command.push("--header", `'${key}: ${value}'`, "\\\n");
}
} catch (e) {
command.push("--header", `'${this.monitor.headers}'`, "\\\n");
}
}
if (this.monitor.authMethod === "basic") {
command.push("--user", `${this.monitor.basic_auth_user}:${this.monitor.basic_auth_pass}`, "--basic", "\\\n");
} else if (this.monitor.authmethod === "mtls") {
command.push("--cacert", `'${this.monitor.tlsCa}'`, "\\\n", "--key", `'${this.monitor.tlsKey}'`, "\\\n", "--cert", `'${this.monitor.tlsCert}'`, "\\\n");
} else if (this.monitor.authMethod === "ntlm") {
command.push("--user", `'${this.monitor.authDomain ? `${this.monitor.authDomain}/` : ""}${this.monitor.basic_auth_user}:${this.monitor.basic_auth_pass}'`, "--ntlm", "\\\n");
}
if (this.monitor.body && this.monitor.httpBodyEncoding === "json") {
let json = "";
try {
// trying to parse the supplied data as json to trim whitespace
json = JSON.stringify(JSON.parse(this.monitor.body));
} catch (e) {
json = this.monitor.body;
}
command.push("--header", "'Content-Type: application/json'", "\\\n", "--data", `'${json}'`, "\\\n");
} else if (this.monitor.body && this.monitor.httpBodyEncoding === "xml") {
command.push("--headers", "'Content-Type: application/xml'", "\\\n", "--data", `'${this.monitor.body}'`, "\\\n");
}
if (this.monitor.maxredirects) {
command.push("--location", "--max-redirs", this.monitor.maxredirects, "\\\n");
}
if (this.monitor.timeout) {
command.push("--max-time", this.monitor.timeout, "\\\n");
}
if (this.monitor.maxretries) {
command.push("--retry", this.monitor.maxretries, "\\\n");
}
command.push("--url", this.monitor.url);
return command.join(" ");
},
ipRegex() {
// Allow to test with simple dns server with port (127.0.0.1:5300)
@@ -1565,15 +1456,8 @@ message HealthCheckResponse {
}
this.monitor.game = newGameObject.keys[0];
},
"monitor.ignoreTls"(newVal) {
if (newVal) {
this.monitor.expiryNotification = false;
}
},
},
mounted() {
this.modal = new Modal(this.$refs.modal);
this.init();
let acceptedStatusCodeOptions = [
@@ -1614,14 +1498,6 @@ message HealthCheckResponse {
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
},
methods: {
async copyToClipboard() {
try {
await navigator.clipboard.writeText(this.curlCommand);
toast.success(this.$t("CopyToClipboardSuccess"));
} catch (err) {
toast.error(this.$t("CopyToClipboardError", { error: err.message }));
}
},
/**
* Initialize the edit monitor form
* @returns {void}
@@ -1813,6 +1689,7 @@ message HealthCheckResponse {
await this.startParentGroupMonitor();
}
this.processing = false;
this.$root.getMonitorList();
this.$router.push("/dashboard/" + res.monitorID);
} else {
this.processing = false;
@@ -1909,9 +1786,4 @@ message HealthCheckResponse {
textarea {
min-height: 200px;
}
#curl-debug {
font-family: monospace;
overflow: auto;
}
</style>

View File

@@ -68,17 +68,15 @@ const routes = [
},
],
},
],
},
{
path: "/add",
component: EditMonitor,
children: [
{
path: "/clone/:id",
component: EditMonitor,
},
]
{
path: "/add",
component: EditMonitor,
},
],
},
{
path: "/list",

View File

@@ -8,17 +8,34 @@
// Backend uses the compiled file util.js
// Frontend uses util.ts
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _a;
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.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.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.ucfirst = void 0;
const dayjs_1 = __importDefault(require("dayjs"));
const dayjs = require("dayjs");
const jsonata = require("jsonata");
const jsonata = __importStar(require("jsonata"));
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.appName = "Uptime Kuma";
@@ -66,7 +83,6 @@ exports.CONSOLE_STYLE_BgMagenta = "\x1b[45m";
exports.CONSOLE_STYLE_BgCyan = "\x1b[46m";
exports.CONSOLE_STYLE_BgWhite = "\x1b[47m";
exports.CONSOLE_STYLE_BgGray = "\x1b[100m";
const consoleModuleColors = [
exports.CONSOLE_STYLE_FgCyan,
exports.CONSOLE_STYLE_FgGreen,
@@ -125,10 +141,6 @@ function ucfirst(str) {
return firstLetter.toUpperCase() + str.substr(1);
}
exports.ucfirst = ucfirst;
function debug(msg) {
exports.log.log("", msg, "debug");
}
exports.debug = debug;
class Logger {
constructor() {
this.hideLog = {
@@ -156,8 +168,6 @@ class Logger {
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
return;
}
module = module.toUpperCase();
level = level.toUpperCase();
let now;
if (dayjs_1.default.tz) {
now = dayjs_1.default.tz(new Date()).format();
@@ -167,10 +177,20 @@ class Logger {
}
const levelColor = consoleLevelColors[level];
const moduleColor = consoleModuleColors[intHash(module, consoleModuleColors.length)];
let timePart;
let modulePart;
let levelPart;
let msgPart;
let timePart = now;
let modulePart = module;
let levelPart = level;
let msgPart = msg;
if (process.env.UPTIME_KUMA_LOG_FORMAT === "json") {
console.log(JSON.stringify({
time: timePart,
module: modulePart,
level: levelPart,
msg: typeof msg === "string" ? msg : JSON.stringify(msg),
}));
return;
}
module = module.toUpperCase();
if (exports.isNode) {
switch (level) {
case "DEBUG":
@@ -187,28 +207,17 @@ class Logger {
if (typeof msg === "string") {
msgPart = exports.CONSOLE_STYLE_FgRed + msg + exports.CONSOLE_STYLE_Reset;
}
else {
msgPart = msg;
}
break;
case "DEBUG":
if (typeof msg === "string") {
msgPart = exports.CONSOLE_STYLE_FgGray + msg + exports.CONSOLE_STYLE_Reset;
}
else {
msgPart = msg;
}
break;
default:
msgPart = msg;
break;
}
}
else {
timePart = now;
modulePart = `[${module}]`;
levelPart = `${level}:`;
msgPart = msg;
}
switch (level) {
case "ERROR":
@@ -231,23 +240,23 @@ class Logger {
}
}
info(module, msg) {
this.log(module, msg, "info");
this.log(module, msg, "INFO");
}
warn(module, msg) {
this.log(module, msg, "warn");
this.log(module, msg, "WARN");
}
error(module, msg) {
this.log(module, msg, "error");
this.log(module, msg, "ERROR");
}
debug(module, msg) {
this.log(module, msg, "debug");
this.log(module, msg, "DEBUG");
}
exception(module, exception, msg) {
let finalMessage = exception;
if (msg) {
finalMessage = `${msg}: ${exception}`;
}
this.log(module, finalMessage, "error");
this.log(module, finalMessage, "ERROR");
}
}
exports.log = new Logger();
@@ -458,4 +467,4 @@ async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue
throw new Error(`Error evaluating JSON query: ${err.message}. Response from server was: ${response}`);
}
}
exports.evaluateJsonQuery = evaluateJsonQuery;
exports.evaluateJsonQuery = evaluateJsonQuery;

View File

@@ -120,11 +120,7 @@ export const badgeConstants = {
defaultCertExpireDownDays: "7"
};
/**
* Flip the status of s between UP and DOWN if this is possible
* @param s {number} status
* @returns {number} flipped status
*/
/** Flip the status of s */
export function flipStatus(s: number) {
if (s === UP) {
return DOWN;
@@ -160,15 +156,6 @@ export function ucfirst(str: string) {
return firstLetter.toUpperCase() + str.substr(1);
}
/**
* @deprecated Use log.debug (https://github.com/louislam/uptime-kuma/pull/910)
* @param msg Message to write
* @returns {void}
*/
export function debug(msg: unknown) {
log.log("", msg, "debug");
}
class Logger {
/**
@@ -210,12 +197,13 @@ class Logger {
/**
* Write a message to the log
* @private
* @param module The module the log comes from
* @param msg Message to write
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
* @param level {"INFO"|"WARN"|"ERROR"|"DEBUG"} Log level
* @returns {void}
*/
log(module: string, msg: any, level: string) {
log(module: string, msg: unknown, level: "INFO"|"WARN"|"ERROR"|"DEBUG"): void {
if (level === "DEBUG" && !isDev) {
return;
}
@@ -224,9 +212,6 @@ class Logger {
return;
}
module = module.toUpperCase();
level = level.toUpperCase();
let now;
if (dayjs.tz) {
now = dayjs.tz(new Date()).format();
@@ -237,10 +222,23 @@ class Logger {
const levelColor = consoleLevelColors[level];
const moduleColor = consoleModuleColors[intHash(module, consoleModuleColors.length)];
let timePart: string;
let modulePart: string;
let levelPart: string;
let msgPart: string;
let timePart: string = now;
let modulePart: string = module;
let levelPart: string = level;
let msgPart: unknown = msg;
if (process.env.UPTIME_KUMA_LOG_FORMAT === "json") {
console.log(JSON.stringify({
time: timePart,
module: modulePart,
level: levelPart,
msg: typeof msg === "string" ? msg : JSON.stringify(msg),
}));
return;
}
// Console rendering:
module = module.toUpperCase();
if (isNode) {
// Add console colors
@@ -261,27 +259,18 @@ class Logger {
case "ERROR":
if (typeof msg === "string") {
msgPart = CONSOLE_STYLE_FgRed + msg + CONSOLE_STYLE_Reset;
} else {
msgPart = msg;
}
break;
case "DEBUG":
if (typeof msg === "string") {
msgPart = CONSOLE_STYLE_FgGray + msg + CONSOLE_STYLE_Reset;
} else {
msgPart = msg;
}
break;
default:
msgPart = msg;
break;
}
} else {
// No console colors
timePart = now;
modulePart = `[${module}]`;
levelPart = `${level}:`;
msgPart = msg;
}
// Write to console
@@ -312,8 +301,8 @@ class Logger {
* @param msg Message to write
* @returns {void}
*/
info(module: string, msg: unknown) {
this.log(module, msg, "info");
info(module: string, msg: string): void {
this.log(module, msg, "INFO");
}
/**
@@ -322,8 +311,8 @@ class Logger {
* @param msg Message to write
* @returns {void}
*/
warn(module: string, msg: unknown) {
this.log(module, msg, "warn");
warn(module: string, msg: string): void {
this.log(module, msg, "WARN");
}
/**
@@ -332,8 +321,8 @@ class Logger {
* @param msg Message to write
* @returns {void}
*/
error(module: string, msg: unknown) {
this.log(module, msg, "error");
error(module: string, msg: string): void {
this.log(module, msg, "ERROR");
}
/**
@@ -342,8 +331,8 @@ class Logger {
* @param msg Message to write
* @returns {void}
*/
debug(module: string, msg: unknown) {
this.log(module, msg, "debug");
debug(module: string, msg: string): void {
this.log(module, msg, "DEBUG");
}
/**
@@ -360,7 +349,7 @@ class Logger {
finalMessage = `${msg}: ${exception}`;
}
this.log(module, finalMessage, "error");
this.log(module, finalMessage, "ERROR");
}
}
@@ -404,7 +393,7 @@ export class TimeLogger {
* @param name Name of monitor
* @returns {void}
*/
print(name: string) {
print(name: string): void {
if (isDev && process.env.TIMELOGGER === "1") {
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
}

View File

@@ -0,0 +1,102 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
const { HiveMQContainer } = require("@testcontainers/hivemq");
const mqtt = require("mqtt");
const { MqttMonitorType } = require("../../server/monitor-types/mqtt");
const { UP, PENDING } = require("../../src/util");
/**
* Runs an MQTT test with the
* @param {string} mqttSuccessMessage the message that the monitor expects
* @param {null|"keyword"|"json-query"} mqttCheckType the type of check we perform
* @param {string} receivedMessage what message is recieved from the mqtt channel
* @returns {Promise<Heartbeat>} the heartbeat produced by the check
*/
async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) {
const hiveMQContainer = await new HiveMQContainer().start();
const connectionString = hiveMQContainer.getConnectionString();
const mqttMonitorType = new MqttMonitorType();
const monitor = {
jsonPath: "firstProp", // always return firstProp for the json-query monitor
hostname: connectionString.split(":", 2).join(":"),
mqttTopic: "test",
port: connectionString.split(":")[2],
mqttUsername: null,
mqttPassword: null,
interval: 20, // controls the timeout
mqttSuccessMessage: mqttSuccessMessage, // for keywords
expectedValue: mqttSuccessMessage, // for json-query
mqttCheckType: mqttCheckType,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const testMqttClient = mqtt.connect(hiveMQContainer.getConnectionString());
testMqttClient.on("connect", () => {
testMqttClient.subscribe("test", (error) => {
if (!error) {
testMqttClient.publish("test", receivedMessage);
}
});
});
try {
await mqttMonitorType.check(monitor, heartbeat, {});
} finally {
testMqttClient.end();
hiveMQContainer.stop();
}
return heartbeat;
}
describe("MqttMonitorType", {
concurrency: true,
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64")
}, () => {
test("valid keywords (type=default)", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
});
test("valid keywords (type=keyword)", async () => {
const heartbeat = await testMqtt("KEYWORD", "keyword", "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
});
test("invalid keywords (type=default)", async () => {
await assert.rejects(
testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
);
});
test("invalid keyword (type=keyword)", async () => {
await assert.rejects(
testMqtt("NOT_PRESENT", "keyword", "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
);
});
test("valid json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Message received, expected value is found");
});
test("invalid (because query fails) json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects(
testMqtt("[not_relevant]", "json-query", "{}"),
new Error("Message received but value is not equal to expected value, value was: [undefined]"),
);
});
test("invalid (because successMessage fails) json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects(
testMqtt("[wrong_success_messsage]", "json-query", "{\"firstProp\":\"present\"}"),
new Error("Message received but value is not equal to expected value, value was: [present]")
);
});
});

View File

@@ -1,10 +1,10 @@
const fs = require("fs");
const rmSync = require("../extra/fs-rmSync.js");
const path = "./data/test";
if (fs.existsSync(path)) {
fs.rmSync(path, {
rmSync(path, {
recursive: true,
force: true,
});
}