Merge branch 'master' into extracted-group-monitor

pull/4395/head
Frank Elsinga 1 month ago committed by GitHub
commit 17135240e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,28 +0,0 @@
# Codespaces
You can modifiy Uptime Kuma in your browser without setting up a local development.
![image](https://github.com/louislam/uptime-kuma/assets/1336778/31d9f06d-dd0b-4405-8e0d-a96586ee4595)
1. Click `Code` -> `Create codespace on master`
2. Wait a few minutes until you see there are two exposed ports
3. Go to the `3000` url, see if it is working
![image](https://github.com/louislam/uptime-kuma/assets/1336778/909b2eb4-4c5e-44e4-ac26-6d20ed856e7f)
## Frontend
Since the frontend is using [Vite.js](https://vitejs.dev/), all changes in this area will be hot-reloaded.
You don't need to restart the frontend, unless you try to add a new frontend dependency.
## Backend
The backend does not automatically hot-reload.
You will need to restart the backend after changing something using these steps:
1. Click `Terminal`
2. Click `Codespaces: server-dev` in the right panel
3. Press `Ctrl + C` to stop the server
4. Press `Up` to run `npm run start-server-dev`
![image](https://github.com/louislam/uptime-kuma/assets/1336778/e0c0a350-fe46-4588-9f37-e053c85834d1)

@ -1,23 +0,0 @@
{
"image": "mcr.microsoft.com/devcontainers/javascript-node:dev-18-bookworm",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"updateContentCommand": "npm ci",
"postCreateCommand": "",
"postAttachCommand": {
"frontend-dev": "npm run start-frontend-devcontainer",
"server-dev": "npm run start-server-dev",
"open-port": "gh codespace ports visibility 3001:public -c $CODESPACE_NAME"
},
"customizations": {
"vscode": {
"extensions": [
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"GitHub.copilot-chat"
]
}
},
"forwardPorts": [3000, 3001]
}

@ -1,7 +1,6 @@
/.idea
/node_modules
/data*
/cypress
/out
/test
/kubernetes
@ -18,7 +17,6 @@ README.md
.vscode
.eslint*
.stylelint*
/.devcontainer
/.github
yarn.lock
app.json

@ -1,8 +1,7 @@
module.exports = {
ignorePatterns: [
"test/*.js",
"test/cypress",
"server/modules/apicache/*",
"server/modules/*",
"src/util.js"
],
root: true,

@ -15,14 +15,14 @@ on:
jobs:
auto-test:
needs: [ check-linters, e2e-test ]
needs: [ check-linters ]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
node: [ 18, 20.5 ]
node: [ 18, 20 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
@ -42,10 +42,10 @@ 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: [ check-linters ]
needs: [ ]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
if: ${{ github.repository == 'louislam/uptime-kuma' }}
strategy:
matrix:
os: [ ARMv7 ]
@ -77,7 +77,7 @@ jobs:
- run: npm run lint:prod
e2e-test:
needs: [ check-linters ]
needs: [ ]
runs-on: ARM64
steps:
- run: git config --global core.autocrlf false # Mainly for Windows

@ -9,7 +9,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8
- uses: actions/stale@v9
with:
stale-issue-message: |-
We are clearing up our old `help`-issues and your issue has been open for 60 days with no activity.
@ -21,7 +21,7 @@ jobs:
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request'
exempt-issue-assignees: 'louislam'
operations-per-run: 200
- uses: actions/stale@v8
- uses: actions/stale@v9
with:
stale-issue-message: |-
This issue was marked as `cannot-reproduce` by a maintainer.

@ -1,6 +1,6 @@
# Project Info
First of all, I want to thank everyone who have wrote issues or shared pull requests for Uptime Kuma.
First of all, I want to thank everyone who has submitted 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 build into `dist`-directory and the server (`express.js`) exposes the `dist` directory as the root of the endpoint.
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 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 your and our maintainers time is precious, and we don't want to waste both time.
Both yours and our maintainers' time is precious, and we don't want to waste either.
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 by these rules:
Please make sure that you follow 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 came it does.
- test that your code does what you claim 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 shure you have tested the notification provider, please include screenshots of the following events in the pull-request description:
To make sure 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>
@ -127,7 +127,7 @@ Different guidelines exist for different types of pull requests (PRs):
- `server/monitor-types/MONITORING_TYPE.js` is the core of each monitor.
the `async check(...)`-function should:
- throw an error for each fault that is detected with an actionable error message
- in the happy-path, you should set `heartbeat.msg` to a successfull message and set `heartbeat.status = UP`
- in the happy-path, you should set `heartbeat.msg` to a successful message and set `heartbeat.status = UP`
- `server/uptime-kuma-server.js` is where the monitoring backend needs to be registered.
*If you have an idea how we can skip this step, we would love to hear about it ^^*
- `src/pages/EditMonitor.vue` is the shared frontend users interact with.
@ -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 you don't know if it will be merged or not.
This is especially important for a large pull request or when 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 so many configurations before you can finally start the app.
I personally do not like something that requires a lot of configuration 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
@ -236,12 +236,6 @@ The goal is to make the Uptime Kuma installation as easy as installing a mobile
- IDE that supports [`ESLint`](https://eslint.org/) and EditorConfig (I am using [`IntelliJ IDEA`](https://www.jetbrains.com/idea/))
- A SQLite GUI tool (f.ex. [`SQLite Expert Personal`](https://www.sqliteexpert.com/download.html) or [`DBeaver Community`](https://dbeaver.io/download/))
### GitHub Codespaces
If you don't want to setup an local environment, you can now develop on GitHub Codespaces, read more:
https://github.com/louislam/uptime-kuma/tree/master/.devcontainer
## Git Branches
- `master`: 2.X.X development. If you want to add a new feature, your pull request should base on this.
@ -266,7 +260,7 @@ Port `3000` and port `3001` will be used.
npm run dev
```
But sometimes, you would like to restart the server, but not the frontend, you can run these commands in two terminals:
But sometimes you may want to restart the server without restarting the frontend. In that case, you can run these commands in two terminals:
```bash
npm run start-frontend-dev
@ -415,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 which specialise in different areas.
This project has multiple maintainers who specialise in different areas.
Currently, there are 3 maintainers:
| Person | Role | Main Area |
@ -427,7 +421,33 @@ Currently, there are 3 maintainers:
### Procedures
We have a few procedures we follow. These are documented here:
- <details><summary>Set up a Docker Builder</summary>
<p>
- amd64, armv7 using local.
- arm64 using remote arm64 cpu, as the emulator is too slow and can no longer pass the `npm ci` command.
1. Add the public key to the remote server.
2. Add the remote context. The remote machine must be arm64 and installed Docker CE.
```
docker context create oracle-arm64-jp --docker "host=ssh://root@100.107.174.88"
```
3. Create a new builder.
```
docker buildx create --name kuma-builder --platform linux/amd64,linux/arm/v7
docker buildx use kuma-builder
docker buildx inspect --bootstrap
```
4. Append the remote context to the builder.
```
docker buildx create --append --name kuma-builder --platform linux/arm64 oracle-arm64-jp
```
5. Verify the builder and check if the builder is using `kuma-builder`.
```
docker buildx inspect kuma-builder
docker buildx ls
```
</p>
</details>
- <details><summary>Release</summary>
<p>
@ -490,28 +510,3 @@ We have a few procedures we follow. These are documented here:
</p>
</details>
### Set up a Docker Builder
- amd64, armv7 using local.
- arm64 using remote arm64 cpu, as the emulator is too slow and can no longer pass the `npm ci` command.
1. Add the public key to the remote server.
2. Add the remote context. The remote machine must be arm64 and installed Docker CE.
```
docker context create oracle-arm64-jp --docker "host=ssh://root@100.107.174.88"
```
3. Create a new builder.
```
docker buildx create --name kuma-builder --platform linux/amd64,linux/arm/v7
docker buildx use kuma-builder
docker buildx inspect --bootstrap
```
4. Append the remote context to the builder.
```
docker buildx create --append --name kuma-builder --platform linux/arm64 oracle-arm64-jp
```
5. Verify the builder and check if the builder is using `kuma-builder`.
```
docker buildx inspect kuma-builder
docker buildx ls
```

@ -1,28 +0,0 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
projectId: "vyjuem",
e2e: {
experimentalStudio: true,
setupNodeEvents(on, config) {
},
fixturesFolder: "test/cypress/fixtures",
screenshotsFolder: "test/cypress/screenshots",
videosFolder: "test/cypress/videos",
downloadsFolder: "test/cypress/downloads",
supportFile: "test/cypress/support/e2e.js",
baseUrl: "http://localhost:3002",
defaultCommandTimeout: 10000,
pageLoadTimeout: 60000,
viewportWidth: 1920,
viewportHeight: 1080,
specPattern: [
"test/cypress/e2e/setup.cy.js",
"test/cypress/e2e/**/*.js"
],
},
env: {
baseUrl: "http://localhost:3002",
},
});

@ -1,10 +0,0 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
supportFile: false,
specPattern: [
"test/cypress/unit/**/*.js"
],
}
});

@ -1,11 +1,11 @@
import { defineConfig, devices } from "@playwright/test";
const port = 30001;
const url = `http://localhost:${port}`;
export const url = `http://localhost:${port}`;
export default defineConfig({
// Look for test files in the "tests" directory, relative to this configuration file.
testDir: "../test/e2e",
testDir: "../test/e2e/specs",
outputDir: "../private/playwright-test-results",
fullyParallel: false,
locale: "en-US",
@ -40,9 +40,15 @@ export default defineConfig({
// Configure projects for major browsers.
projects: [
{
name: "chromium",
name: "run-once setup",
testMatch: /setup-process\.once\.js/,
use: { ...devices["Desktop Chrome"] },
},
{
name: "specs",
use: { ...devices["Desktop Chrome"] },
dependencies: [ "run-once setup" ],
},
/*
{
name: "firefox",
@ -52,7 +58,7 @@ export default defineConfig({
// Run your local dev server before starting the tests.
webServer: {
command: `node extra/remove-playwright-test-data.js && node server/server.js --port=${port} --data-dir=./data/playwright-test`,
command: `node extra/remove-playwright-test-data.js && cross-env NODE_ENV=development node server/server.js --port=${port} --data-dir=./data/playwright-test`,
url,
reuseExistingServer: false,
cwd: "../",

@ -16,9 +16,7 @@ export default defineConfig({
},
define: {
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
"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),
"process.env": {},
},
plugins: [
vue(),

@ -0,0 +1,16 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.string("snmp_oid").defaultTo(null);
table.enum("snmp_version", [ "1", "2c", "3" ]).defaultTo("2c");
table.string("json_path_operator").defaultTo(null);
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("snmp_oid");
table.dropColumn("snmp_version");
table.dropColumn("json_path_operator");
});
};

@ -0,0 +1,13 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.boolean("cache_bust").notNullable().defaultTo(false);
});
};
exports.down = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.dropColumn("cache_bust");
});
};

@ -0,0 +1,12 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.text("conditions").notNullable().defaultTo("[]");
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("conditions");
});
};

@ -3,7 +3,6 @@ FROM node:20-bookworm-slim AS base2-slim
ARG TARGETPLATFORM
# Specify --no-install-recommends to skip unused dependencies, make the base much smaller!
# apprise = for notifications (From testing repo)
# sqlite3 = for debugging
# iputils-ping = for ping
# util-linux = for setpriv (Should be dropped in 2.0.0?)
@ -12,10 +11,10 @@ ARG TARGETPLATFORM
# ca-certificates = keep the cert up-to-date
# sudo = for start service nscd with non-root user
# nscd = for better DNS caching
RUN echo "deb http://deb.debian.org/debian testing main" >> /etc/apt/sources.list && \
apt update && \
apt --yes --no-install-recommends -t testing install apprise sqlite3 ca-certificates && \
apt --yes --no-install-recommends -t stable install \
RUN apt update && \
apt --yes --no-install-recommends install \
sqlite3 \
ca-certificates \
iputils-ping \
util-linux \
dumb-init \
@ -25,6 +24,15 @@ RUN echo "deb http://deb.debian.org/debian testing main" >> /etc/apt/sources.lis
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove
# apprise = for notifications (Install from the deb package, as the stable one is too old) (workaround for #4867)
# Switching to testing repo is no longer working, as the testing repo is not bookworm anymore.
# python3-paho-mqtt (#4859)
RUN curl http://ftp.debian.org/debian/pool/main/a/apprise/apprise_1.8.0-2_all.deb --output apprise.deb && \
apt update && \
apt --yes --no-install-recommends install ./apprise.deb python3-paho-mqtt && \
rm -rf /var/lib/apt/lists/* && \
rm -f apprise.deb && \
apt --yes autoremove
# Install cloudflared
RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \
@ -42,7 +50,9 @@ COPY ./docker/etc/sudoers /etc/sudoers
# Full Base Image
# MariaDB, Chromium and fonts
FROM base2-slim AS base2
# Make sure to reuse the slim image here. Uncomment the above line if you want to build it from scratch.
# FROM base2-slim AS base2
FROM louislam/uptime-kuma:base2-slim AS base2
ENV UPTIME_KUMA_ENABLE_EMBEDDED_MARIADB=1
RUN apt update && \
apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk mariadb-server && \

@ -4,7 +4,6 @@ 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";
@ -29,8 +28,9 @@ function download(url) {
if (fs.existsSync("./dist")) {
if (fs.existsSync("./dist-backup")) {
rmSync("./dist-backup", {
recursive: true
fs.rmSync("./dist-backup", {
recursive: true,
force: true,
});
}
@ -43,8 +43,9 @@ function download(url) {
tarStream.on("close", () => {
if (fs.existsSync("./dist-backup")) {
rmSync("./dist-backup", {
recursive: true
fs.rmSync("./dist-backup", {
recursive: true,
force: true,
});
}
console.log("Done");

@ -1,23 +0,0 @@
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;

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

8352
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -27,9 +27,7 @@
"build": "vite build --config ./config/vite.config.js",
"test": "npm run test-backend && npm run test-e2e",
"test-with-build": "npm run build && npm test",
"test-backend": "node test/backend-test-entry.js",
"test-backend:14": "cross-env TEST_BACKEND=1 NODE_OPTIONS=\"--experimental-abortcontroller --no-warnings\" node--test test/backend-test",
"test-backend:18": "cross-env TEST_BACKEND=1 node --test test/backend-test",
"test-backend": "cross-env TEST_BACKEND=1 node --test test/backend-test",
"test-e2e": "playwright test --config ./config/playwright.config.js",
"test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063",
"playwright-codegen": "playwright codegen localhost:3000 --save-storage=./private/e2e-auth.json",
@ -49,7 +47,7 @@
"build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.23.13 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.23.14 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@ -70,16 +68,15 @@
"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",
"start-server-node14-win": "private\\node14\\node.exe server/server.js"
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X"
},
"dependencies": {
"@grpc/grpc-js": "~1.7.3",
"@grpc/grpc-js": "~1.8.22",
"@louislam/ping": "~0.4.4-mod.1",
"@louislam/sqlite3": "15.1.6",
"@vvo/tzdb": "^6.125.0",
"args-parser": "~1.3.0",
"axios": "~0.28.1",
"axios-ntlm": "1.3.0",
"badge-maker": "~3.3.1",
"bcryptjs": "~2.4.3",
"chardet": "~1.4.0",
@ -89,12 +86,14 @@
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
"compression": "~1.7.4",
"croner": "~6.0.5",
"croner": "~8.1.0",
"dayjs": "~1.11.5",
"dev-null": "^0.1.1",
"dotenv": "~16.0.3",
"express": "~4.19.2",
"express": "~4.21.0",
"express-basic-auth": "~1.2.1",
"express-static-gzip": "~2.1.7",
"feed": "^4.2.2",
"form-data": "~4.0.0",
"gamedig": "^4.2.0",
"html-escaper": "^3.0.3",
@ -112,12 +111,14 @@
"knex": "^2.4.2",
"limiter": "~2.1.0",
"liquidjs": "^10.7.0",
"marked": "^14.0.0",
"mitt": "~3.0.1",
"mongodb": "~4.17.1",
"mqtt": "~4.3.7",
"mssql": "~8.1.4",
"mssql": "~11.0.0",
"mysql2": "~3.9.6",
"nanoid": "~3.3.4",
"net-snmp": "^3.11.2",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.9.13",
@ -136,10 +137,9 @@
"redbean-node": "~0.3.0",
"redis": "~4.5.1",
"semver": "~7.5.4",
"socket.io": "~4.6.1",
"socket.io-client": "~4.6.1",
"socket.io": "~4.8.0",
"socket.io-client": "~4.8.0",
"socks-proxy-agent": "6.1.1",
"sqlite3": "~5.1.7",
"tar": "~6.2.1",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2",
@ -171,13 +171,12 @@
"cross-env": "~7.0.3",
"delay": "^5.0.0",
"dns2": "~2.0.1",
"dompurify": "~3.0.11",
"dompurify": "~3.1.7",
"eslint": "~8.14.0",
"eslint-plugin-jsdoc": "~46.4.6",
"eslint-plugin-vue": "~8.7.1",
"favico.js": "~0.3.10",
"get-port-please": "^3.1.1",
"marked": "~4.2.5",
"node-ssh": "~13.1.0",
"postcss-html": "~1.5.0",
"postcss-rtlcss": "~3.7.2",

@ -213,6 +213,32 @@ async function sendRemoteBrowserList(socket) {
return list;
}
/**
* Send list of monitor types to client
* @param {Socket} socket Socket.io socket instance
* @returns {Promise<void>}
*/
async function sendMonitorTypeList(socket) {
const result = Object.entries(UptimeKumaServer.monitorTypeList).map(([ key, type ]) => {
return [ key, {
supportsConditions: type.supportsConditions,
conditionVariables: type.conditionVariables.map(v => {
return {
id: v.id,
operators: v.operators.map(o => {
return {
id: o.id,
caption: o.caption,
};
}),
};
}),
}];
});
io.to(socket.userID).emit("monitorTypeList", Object.fromEntries(result));
}
module.exports = {
sendNotificationList,
sendImportantHeartbeatList,
@ -222,4 +248,5 @@ module.exports = {
sendInfo,
sendDockerHostList,
sendRemoteBrowserList,
sendMonitorTypeList,
};

@ -223,8 +223,11 @@ class Database {
fs.copyFileSync(Database.templatePath, Database.sqlitePath);
}
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
Dialect.prototype._driver = () => require("@louislam/sqlite3");
config = {
client: "sqlite3",
client: Dialect,
connection: {
filename: Database.sqlitePath,
acquireConnectionTimeout: acquireConnectionTimeout,

@ -239,19 +239,7 @@ class Maintenance extends BeanModel {
this.beanMeta.status = "under-maintenance";
clearTimeout(this.beanMeta.durationTimeout);
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
let duration;
if (customDuration > 0) {
duration = customDuration;
} else if (this.end_date) {
let d = dayjs(this.end_date).diff(dayjs(), "second");
if (d < this.duration) {
duration = d * 1000;
}
} else {
duration = this.duration * 1000;
}
let duration = this.inferDuration(customDuration);
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
@ -263,9 +251,21 @@ class Maintenance extends BeanModel {
};
// Create Cron
this.beanMeta.job = new Cron(this.cron, {
timezone: await this.getTimezone(),
}, startEvent);
if (this.strategy === "recurring-interval") {
// For recurring-interval, Croner needs to have interval and startAt
const startDate = dayjs(this.startDate);
const [ hour, minute ] = this.startTime.split(":");
const startDateTime = startDate.hour(hour).minute(minute);
this.beanMeta.job = new Cron(this.cron, {
timezone: await this.getTimezone(),
interval: this.interval_day * 24 * 60 * 60,
startAt: startDateTime.toISOString(),
}, startEvent);
} else {
this.beanMeta.job = new Cron(this.cron, {
timezone: await this.getTimezone(),
}, startEvent);
}
// Continue if the maintenance is still in the window
let runningTimeslot = this.getRunningTimeslot();
@ -311,6 +311,24 @@ class Maintenance extends BeanModel {
}
}
/**
* Calculate the maintenance duration
* @param {number} customDuration - The custom duration in milliseconds.
* @returns {number} The inferred duration in milliseconds.
*/
inferDuration(customDuration) {
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
if (customDuration > 0) {
return customDuration;
} else if (this.end_date) {
let d = dayjs(this.end_date).diff(dayjs(), "second");
if (d < this.duration) {
return d * 1000;
}
}
return this.duration * 1000;
}
/**
* Stop the maintenance
* @returns {void}
@ -395,10 +413,8 @@ class Maintenance extends BeanModel {
} else if (!this.strategy.startsWith("recurring-")) {
this.cron = "";
} else if (this.strategy === "recurring-interval") {
let array = this.start_time.split(":");
let hour = parseInt(array[0]);
let minute = parseInt(array[1]);
this.cron = minute + " " + hour + " */" + this.interval_day + " * *";
// For intervals, the pattern is calculated in the run function as the interval-option is set
this.cron = "* * * * *";
this.duration = this.calcDuration();
log.debug("maintenance", "Cron: " + this.cron);
log.debug("maintenance", "Duration: " + this.duration);

@ -2,7 +2,7 @@ const dayjs = require("dayjs");
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
SQL_DATETIME_FORMAT
SQL_DATETIME_FORMAT, evaluateJsonQuery
} = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
@ -17,7 +17,6 @@ const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { DockerHost } = require("../docker");
const Gamedig = require("gamedig");
const jsonata = require("jsonata");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const { UptimeCalculator } = require("../uptime-calculator");
@ -72,23 +71,12 @@ 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 {Promise<object>} Object ready to parse
* @returns {object} Object ready to parse
*/
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();
toJSON(preloadData = {}, includeSensitiveData = true) {
let screenshot = null;
@ -96,7 +84,7 @@ class Monitor extends BeanModel {
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
}
const path = await this.getPath();
const path = preloadData.paths.get(this.id) || [];
const pathName = path.join(" / ");
let data = {
@ -106,15 +94,15 @@ class Monitor extends BeanModel {
path,
pathName,
parent: this.parent,
childrenIDs: await Monitor.getAllChildrenIDs(this.id),
childrenIDs: preloadData.childrenIDs.get(this.id) || [],
url: this.url,
method: this.method,
hostname: this.hostname,
port: this.port,
maxretries: this.maxretries,
weight: this.weight,
active: await this.isActive(),
forceInactive: !await Monitor.isParentActive(this.id),
active: preloadData.activeStatus.get(this.id),
forceInactive: preloadData.forceInactive.get(this.id),
type: this.type,
timeout: this.timeout,
interval: this.interval,
@ -134,9 +122,9 @@ class Monitor extends BeanModel {
docker_container: this.docker_container,
docker_host: this.docker_host,
proxyId: this.proxy_id,
notificationIDList,
tags: tags,
maintenance: await Monitor.isUnderMaintenance(this.id),
notificationIDList: preloadData.notifications.get(this.id) || {},
tags: preloadData.tags.get(this.id) || [],
maintenance: preloadData.maintenanceStatus.get(this.id),
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage,
mqttCheckType: this.mqttCheckType,
@ -160,7 +148,12 @@ class Monitor extends BeanModel {
kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(),
kafkaProducerMessage: this.kafkaProducerMessage,
screenshot,
cacheBust: this.getCacheBust(),
remote_browser: this.remote_browser,
snmpOid: this.snmpOid,
jsonPathOperator: this.jsonPathOperator,
snmpVersion: this.snmpVersion,
conditions: JSON.parse(this.conditions),
};
if (includeSensitiveData) {
@ -197,16 +190,6 @@ 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
@ -293,6 +276,14 @@ class Monitor extends BeanModel {
return Boolean(this.grpcEnableTls);
}
/**
* Parse to boolean
* @returns {boolean} if cachebusting is enabled
*/
getCacheBust() {
return Boolean(this.cacheBust);
}
/**
* Get accepted status codes
* @returns {object} Accepted status codes
@ -465,6 +456,14 @@ class Monitor extends BeanModel {
options.data = bodyValue;
}
if (this.cacheBust) {
const randomFloatString = Math.random().toString(36);
const cacheBust = randomFloatString.substring(2);
options.params = {
uptime_kuma_cachebuster: cacheBust,
};
}
if (this.proxy_id) {
const proxy = await R.load("proxy", this.proxy_id);
@ -565,25 +564,15 @@ class Monitor extends BeanModel {
} else if (this.type === "json-query") {
let data = res.data;
// convert data to object
if (typeof data === "string" && res.headers["content-type"] !== "application/json") {
try {
data = JSON.parse(data);
} catch (_) {
// Failed to parse as JSON, just process it as a string
}
}
let expression = jsonata(this.jsonPath);
const { status, response } = await evaluateJsonQuery(data, this.jsonPath, this.jsonPathOperator, this.expectedValue);
let result = await expression.evaluate(data);
if (result.toString() === this.expectedValue) {
bean.msg += ", expected value is found";
if (status) {
bean.status = UP;
bean.msg = `JSON query passes (comparing ${response} ${this.jsonPathOperator} ${this.expectedValue})`;
} else {
throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
throw new Error(`JSON query does not pass (comparing ${response} ${this.jsonPathOperator} ${this.expectedValue})`);
}
}
} else if (this.type === "port") {
@ -1153,6 +1142,18 @@ 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
@ -1289,7 +1290,10 @@ 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";
@ -1300,7 +1304,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, await monitor.toJSON(false), heartbeatJSON);
await Notification.send(JSON.parse(notification.config), msg, monitor.toJSON(preloadData, false), heartbeatJSON);
} catch (e) {
log.error("monitor", "Cannot send notification to " + notification.name);
log.error("monitor", e);
@ -1462,6 +1466,111 @@ 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,
]);
}
/**
* 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, tag.name, tag.color
FROM monitor_tag
JOIN tag ON monitor_tag.tag_id = tag.id
WHERE monitor_tag.monitor_id IN (?)
`, [
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({
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
@ -1494,16 +1603,18 @@ 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
*/
async getPath() {
const path = [ this.name ];
static async getAllPath(monitorID, name) {
const path = [ name ];
if (this.parent === null) {
return path;
}
let parent = await Monitor.getParent(this.id);
let parent = await Monitor.getParent(monitorID);
while (parent !== null) {
path.unshift(parent.name);
parent = await Monitor.getParent(parent.id);

@ -4,6 +4,11 @@ const cheerio = require("cheerio");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const jsesc = require("jsesc");
const googleAnalytics = require("../google-analytics");
const { marked } = require("marked");
const { Feed } = require("feed");
const config = require("../config");
const { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE, DOWN } = require("../../src/util");
class StatusPage extends BeanModel {
@ -13,6 +18,24 @@ class StatusPage extends BeanModel {
*/
static domainMappingList = { };
/**
* Handle responses to RSS pages
* @param {Response} response Response object
* @param {string} slug Status page slug
* @returns {Promise<void>}
*/
static async handleStatusPageRSSResponse(response, slug) {
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (statusPage) {
response.send(await StatusPage.renderRSS(statusPage, slug));
} else {
response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
}
}
/**
* Handle responses to status page
* @param {Response} response Response object
@ -38,6 +61,38 @@ class StatusPage extends BeanModel {
}
}
/**
* SSR for RSS feed
* @param {statusPage} statusPage object
* @param {slug} slug from router
* @returns {Promise<string>} the rendered html
*/
static async renderRSS(statusPage, slug) {
const { heartbeats, statusDescription } = await StatusPage.getRSSPageData(statusPage);
let proto = config.isSSL ? "https" : "http";
let host = `${proto}://${config.hostname || "localhost"}:${config.port}/status/${slug}`;
const feed = new Feed({
title: "uptime kuma rss feed",
description: `current status: ${statusDescription}`,
link: host,
language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
updated: new Date(), // optional, default = today
});
heartbeats.forEach(heartbeat => {
feed.addItem({
title: `${heartbeat.name} is down`,
description: `${heartbeat.name} has been down since ${heartbeat.time}`,
id: heartbeat.monitorID,
date: new Date(heartbeat.time),
});
});
return feed.rss2();
}
/**
* SSR for status pages
* @param {string} indexHTML HTML page to render
@ -46,7 +101,11 @@ class StatusPage extends BeanModel {
*/
static async renderHTML(indexHTML, statusPage) {
const $ = cheerio.load(indexHTML);
const description155 = statusPage.description?.substring(0, 155) ?? "";
const description155 = marked(statusPage.description ?? "")
.replace(/<[^>]+>/gm, "")
.trim()
.substring(0, 155);
$("title").text(statusPage.title);
$("meta[name=description]").attr("content", description155);
@ -93,6 +152,109 @@ class StatusPage extends BeanModel {
return $.root().html();
}
/**
* @param {heartbeats} heartbeats from getRSSPageData
* @returns {number} status_page constant from util.ts
*/
static overallStatus(heartbeats) {
if (heartbeats.length === 0) {
return -1;
}
let status = STATUS_PAGE_ALL_UP;
let hasUp = false;
for (let beat of heartbeats) {
if (beat.status === MAINTENANCE) {
return STATUS_PAGE_MAINTENANCE;
} else if (beat.status === UP) {
hasUp = true;
} else {
status = STATUS_PAGE_PARTIAL_DOWN;
}
}
if (! hasUp) {
status = STATUS_PAGE_ALL_DOWN;
}
return status;
}
/**
* @param {number} status from overallStatus
* @returns {string} description
*/
static getStatusDescription(status) {
if (status === -1) {
return "No Services";
}
if (status === STATUS_PAGE_ALL_UP) {
return "All Systems Operational";
}
if (status === STATUS_PAGE_PARTIAL_DOWN) {
return "Partially Degraded Service";
}
if (status === STATUS_PAGE_ALL_DOWN) {
return "Degraded Service";
}
// TODO: show the real maintenance information: title, description, time
if (status === MAINTENANCE) {
return "Under maintenance";
}
return "?";
}
/**
* Get all data required for RSS
* @param {StatusPage} statusPage Status page to get data for
* @returns {object} Status page data
*/
static async getRSSPageData(statusPage) {
// get all heartbeats that correspond to this statusPage
const config = await statusPage.toPublicJSON();
// Public Group List
const showTags = !!statusPage.show_tags;
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);
let heartbeats = [];
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
for (const monitor of monitorGroup.monitorList) {
const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ monitor.id ]);
if (heartbeat) {
heartbeats.push({
...monitor,
status: heartbeat.status,
time: heartbeat.time
});
}
}
}
// calculate RSS feed description
let status = StatusPage.overallStatus(heartbeats);
let statusDescription = StatusPage.getStatusDescription(status);
// keep only DOWN heartbeats in the RSS feed
heartbeats = heartbeats.filter(heartbeat => heartbeat.status === DOWN);
return {
heartbeats,
statusDescription
};
}
/**
* Get all status page data in one call
* @param {StatusPage} statusPage Status page to get data for

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 CatButtes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,77 @@
'use strict';
// Original file https://raw.githubusercontent.com/elasticio/node-ntlm-client/master/lib/flags.js
module.exports.NTLMFLAG_NEGOTIATE_UNICODE = 1 << 0;
/* Indicates that Unicode strings are supported for use in security buffer
data. */
module.exports.NTLMFLAG_NEGOTIATE_OEM = 1 << 1;
/* Indicates that OEM strings are supported for use in security buffer data. */
module.exports.NTLMFLAG_REQUEST_TARGET = 1 << 2;
/* Requests that the server's authentication realm be included in the Type 2
message. */
/* unknown (1<<3) */
module.exports.NTLMFLAG_NEGOTIATE_SIGN = 1 << 4;
/* Specifies that authenticated communication between the client and server
should carry a digital signature (message integrity). */
module.exports.NTLMFLAG_NEGOTIATE_SEAL = 1 << 5;
/* Specifies that authenticated communication between the client and server
should be encrypted (message confidentiality). */
module.exports.NTLMFLAG_NEGOTIATE_DATAGRAM_STYLE = 1 << 6;
/* Indicates that datagram authentication is being used. */
module.exports.NTLMFLAG_NEGOTIATE_LM_KEY = 1 << 7;
/* Indicates that the LAN Manager session key should be used for signing and
sealing authenticated communications. */
module.exports.NTLMFLAG_NEGOTIATE_NETWARE = 1 << 8;
/* unknown purpose */
module.exports.NTLMFLAG_NEGOTIATE_NTLM_KEY = 1 << 9;
/* Indicates that NTLM authentication is being used. */
/* unknown (1<<10) */
module.exports.NTLMFLAG_NEGOTIATE_ANONYMOUS = 1 << 11;
/* Sent by the client in the Type 3 message to indicate that an anonymous
context has been established. This also affects the response fields. */
module.exports.NTLMFLAG_NEGOTIATE_DOMAIN_SUPPLIED = 1 << 12;
/* Sent by the client in the Type 1 message to indicate that a desired
authentication realm is included in the message. */
module.exports.NTLMFLAG_NEGOTIATE_WORKSTATION_SUPPLIED = 1 << 13;
/* Sent by the client in the Type 1 message to indicate that the client
workstation's name is included in the message. */
module.exports.NTLMFLAG_NEGOTIATE_LOCAL_CALL = 1 << 14;
/* Sent by the server to indicate that the server and client are on the same
machine. Implies that the client may use a pre-established local security
context rather than responding to the challenge. */
module.exports.NTLMFLAG_NEGOTIATE_ALWAYS_SIGN = 1 << 15;
/* Indicates that authenticated communication between the client and server
should be signed with a "dummy" signature. */
module.exports.NTLMFLAG_TARGET_TYPE_DOMAIN = 1 << 16;
/* Sent by the server in the Type 2 message to indicate that the target
authentication realm is a domain. */
module.exports.NTLMFLAG_TARGET_TYPE_SERVER = 1 << 17;
/* Sent by the server in the Type 2 message to indicate that the target
authentication realm is a server. */
module.exports.NTLMFLAG_TARGET_TYPE_SHARE = 1 << 18;
/* Sent by the server in the Type 2 message to indicate that the target
authentication realm is a share. Presumably, this is for share-level
authentication. Usage is unclear. */
module.exports.NTLMFLAG_NEGOTIATE_NTLM2_KEY = 1 << 19;
/* Indicates that the NTLM2 signing and sealing scheme should be used for
protecting authenticated communications. */
module.exports.NTLMFLAG_REQUEST_INIT_RESPONSE = 1 << 20;
/* unknown purpose */
module.exports.NTLMFLAG_REQUEST_ACCEPT_RESPONSE = 1 << 21;
/* unknown purpose */
module.exports.NTLMFLAG_REQUEST_NONNT_SESSION_KEY = 1 << 22;
/* unknown purpose */
module.exports.NTLMFLAG_NEGOTIATE_TARGET_INFO = 1 << 23;
/* Sent by the server in the Type 2 message to indicate that it is including a
Target Information block in the message. */
/* unknown (1<24) */
/* unknown (1<25) */
/* unknown (1<26) */
/* unknown (1<27) */
/* unknown (1<28) */
module.exports.NTLMFLAG_NEGOTIATE_128 = 1 << 29;
/* Indicates that 128-bit encryption is supported. */
module.exports.NTLMFLAG_NEGOTIATE_KEY_EXCHANGE = 1 << 30;
/* Indicates that the client will provide an encrypted master key in
the "Session Key" field of the Type 3 message. */
module.exports.NTLMFLAG_NEGOTIATE_56 = 1 << 31;
//# sourceMappingURL=flags.js.map

@ -0,0 +1,122 @@
'use strict';
// Original source at https://github.com/elasticio/node-ntlm-client/blob/master/lib/hash.js
var crypto = require('crypto');
function createLMResponse(challenge, lmhash) {
var buf = new Buffer.alloc(24), pwBuffer = new Buffer.alloc(21).fill(0);
lmhash.copy(pwBuffer);
calculateDES(pwBuffer.slice(0, 7), challenge).copy(buf);
calculateDES(pwBuffer.slice(7, 14), challenge).copy(buf, 8);
calculateDES(pwBuffer.slice(14), challenge).copy(buf, 16);
return buf;
}
function createLMHash(password) {
var buf = new Buffer.alloc(16), pwBuffer = new Buffer.alloc(14), magicKey = new Buffer.from('KGS!@#$%', 'ascii');
if (password.length > 14) {
buf.fill(0);
return buf;
}
pwBuffer.fill(0);
pwBuffer.write(password.toUpperCase(), 0, 'ascii');
return Buffer.concat([
calculateDES(pwBuffer.slice(0, 7), magicKey),
calculateDES(pwBuffer.slice(7), magicKey)
]);
}
function calculateDES(key, message) {
var desKey = new Buffer.alloc(8);
desKey[0] = key[0] & 0xFE;
desKey[1] = ((key[0] << 7) & 0xFF) | (key[1] >> 1);
desKey[2] = ((key[1] << 6) & 0xFF) | (key[2] >> 2);
desKey[3] = ((key[2] << 5) & 0xFF) | (key[3] >> 3);
desKey[4] = ((key[3] << 4) & 0xFF) | (key[4] >> 4);
desKey[5] = ((key[4] << 3) & 0xFF) | (key[5] >> 5);
desKey[6] = ((key[5] << 2) & 0xFF) | (key[6] >> 6);
desKey[7] = (key[6] << 1) & 0xFF;
for (var i = 0; i < 8; i++) {
var parity = 0;
for (var j = 1; j < 8; j++) {
parity += (desKey[i] >> j) % 2;
}
desKey[i] |= (parity % 2) === 0 ? 1 : 0;
}
var des = crypto.createCipheriv('DES-ECB', desKey, '');
return des.update(message);
}
function createNTLMResponse(challenge, ntlmhash) {
var buf = new Buffer.alloc(24), ntlmBuffer = new Buffer.alloc(21).fill(0);
ntlmhash.copy(ntlmBuffer);
calculateDES(ntlmBuffer.slice(0, 7), challenge).copy(buf);
calculateDES(ntlmBuffer.slice(7, 14), challenge).copy(buf, 8);
calculateDES(ntlmBuffer.slice(14), challenge).copy(buf, 16);
return buf;
}
function createNTLMHash(password) {
var md4sum = crypto.createHash('md4');
md4sum.update(new Buffer.from(password, 'ucs2'));
return md4sum.digest();
}
function createNTLMv2Hash(ntlmhash, username, authTargetName) {
var hmac = crypto.createHmac('md5', ntlmhash);
hmac.update(new Buffer.from(username.toUpperCase() + authTargetName, 'ucs2'));
return hmac.digest();
}
function createLMv2Response(type2message, username, ntlmhash, nonce, targetName) {
var buf = new Buffer.alloc(24), ntlm2hash = createNTLMv2Hash(ntlmhash, username, targetName), hmac = crypto.createHmac('md5', ntlm2hash);
//server challenge
type2message.challenge.copy(buf, 8);
//client nonce
buf.write(nonce || createPseudoRandomValue(16), 16, 'hex');
//create hash
hmac.update(buf.slice(8));
var hashedBuffer = hmac.digest();
hashedBuffer.copy(buf);
return buf;
}
function createNTLMv2Response(type2message, username, ntlmhash, nonce, targetName) {
var buf = new Buffer.alloc(48 + type2message.targetInfo.buffer.length), ntlm2hash = createNTLMv2Hash(ntlmhash, username, targetName), hmac = crypto.createHmac('md5', ntlm2hash);
//the first 8 bytes are spare to store the hashed value before the blob
//server challenge
type2message.challenge.copy(buf, 8);
//blob signature
buf.writeUInt32BE(0x01010000, 16);
//reserved
buf.writeUInt32LE(0, 20);
//timestamp
//TODO: we are loosing precision here since js is not able to handle those large integers
// maybe think about a different solution here
// 11644473600000 = diff between 1970 and 1601
var timestamp = ((Date.now() + 11644473600000) * 10000).toString(16);
var timestampLow = Number('0x' + timestamp.substring(Math.max(0, timestamp.length - 8)));
var timestampHigh = Number('0x' + timestamp.substring(0, Math.max(0, timestamp.length - 8)));
buf.writeUInt32LE(timestampLow, 24, false);
buf.writeUInt32LE(timestampHigh, 28, false);
//random client nonce
buf.write(nonce || createPseudoRandomValue(16), 32, 'hex');
//zero
buf.writeUInt32LE(0, 40);
//complete target information block from type 2 message
type2message.targetInfo.buffer.copy(buf, 44);
//zero
buf.writeUInt32LE(0, 44 + type2message.targetInfo.buffer.length);
hmac.update(buf.slice(8));
var hashedBuffer = hmac.digest();
hashedBuffer.copy(buf);
return buf;
}
function createPseudoRandomValue(length) {
var str = '';
while (str.length < length) {
str += Math.floor(Math.random() * 16).toString(16);
}
return str;
}
module.exports = {
createLMHash: createLMHash,
createNTLMHash: createNTLMHash,
createLMResponse: createLMResponse,
createNTLMResponse: createNTLMResponse,
createLMv2Response: createLMv2Response,
createNTLMv2Response: createNTLMv2Response,
createPseudoRandomValue: createPseudoRandomValue
};
//# sourceMappingURL=hash.js.map

@ -0,0 +1,220 @@
'use strict';
// Original file https://raw.githubusercontent.com/elasticio/node-ntlm-client/master/lib/ntlm.js
var os = require('os'), flags = require('./flags'), hash = require('./hash');
var NTLMSIGNATURE = "NTLMSSP\0";
function createType1Message(workstation, target) {
var dataPos = 32, pos = 0, buf = new Buffer.alloc(1024);
workstation = workstation === undefined ? os.hostname() : workstation;
target = target === undefined ? '' : target;
//signature
buf.write(NTLMSIGNATURE, pos, NTLMSIGNATURE.length, 'ascii');
pos += NTLMSIGNATURE.length;
//message type
buf.writeUInt32LE(1, pos);
pos += 4;
//flags
buf.writeUInt32LE(flags.NTLMFLAG_NEGOTIATE_OEM |
flags.NTLMFLAG_REQUEST_TARGET |
flags.NTLMFLAG_NEGOTIATE_NTLM_KEY |
flags.NTLMFLAG_NEGOTIATE_NTLM2_KEY |
flags.NTLMFLAG_NEGOTIATE_ALWAYS_SIGN, pos);
pos += 4;
//domain security buffer
buf.writeUInt16LE(target.length, pos);
pos += 2;
buf.writeUInt16LE(target.length, pos);
pos += 2;
buf.writeUInt32LE(target.length === 0 ? 0 : dataPos, pos);
pos += 4;
if (target.length > 0) {
dataPos += buf.write(target, dataPos, 'ascii');
}
//workstation security buffer
buf.writeUInt16LE(workstation.length, pos);
pos += 2;
buf.writeUInt16LE(workstation.length, pos);
pos += 2;
buf.writeUInt32LE(workstation.length === 0 ? 0 : dataPos, pos);
pos += 4;
if (workstation.length > 0) {
dataPos += buf.write(workstation, dataPos, 'ascii');
}
return 'NTLM ' + buf.toString('base64', 0, dataPos);
}
function decodeType2Message(str) {
if (str === undefined) {
throw new Error('Invalid argument');
}
//convenience
if (Object.prototype.toString.call(str) !== '[object String]') {
if (str.hasOwnProperty('headers') && str.headers.hasOwnProperty('www-authenticate')) {
str = str.headers['www-authenticate'];
}
else {
throw new Error('Invalid argument');
}
}
var ntlmMatch = /^NTLM ([^,\s]+)/.exec(str);
if (ntlmMatch) {
str = ntlmMatch[1];
}
var buf = new Buffer.from(str, 'base64'), obj = {};
//check signature
if (buf.toString('ascii', 0, NTLMSIGNATURE.length) !== NTLMSIGNATURE) {
throw new Error('Invalid message signature: ' + str);
}
//check message type
if (buf.readUInt32LE(NTLMSIGNATURE.length) !== 2) {
throw new Error('Invalid message type (no type 2)');
}
//read flags
obj.flags = buf.readUInt32LE(20);
obj.encoding = (obj.flags & flags.NTLMFLAG_NEGOTIATE_OEM) ? 'ascii' : 'ucs2';
obj.version = (obj.flags & flags.NTLMFLAG_NEGOTIATE_NTLM2_KEY) ? 2 : 1;
obj.challenge = buf.slice(24, 32);
//read target name
obj.targetName = (function () {
var length = buf.readUInt16LE(12);
//skipping allocated space
var offset = buf.readUInt32LE(16);
if (length === 0) {
return '';
}
if ((offset + length) > buf.length || offset < 32) {
throw new Error('Bad type 2 message');
}
return buf.toString(obj.encoding, offset, offset + length);
})();
//read target info
if (obj.flags & flags.NTLMFLAG_NEGOTIATE_TARGET_INFO) {
obj.targetInfo = (function () {
var info = {};
var length = buf.readUInt16LE(40);
//skipping allocated space
var offset = buf.readUInt32LE(44);
var targetInfoBuffer = new Buffer.alloc(length);
buf.copy(targetInfoBuffer, 0, offset, offset + length);
if (length === 0) {
return info;
}
if ((offset + length) > buf.length || offset < 32) {
throw new Error('Bad type 2 message');
}
var pos = offset;
while (pos < (offset + length)) {
var blockType = buf.readUInt16LE(pos);
pos += 2;
var blockLength = buf.readUInt16LE(pos);
pos += 2;
if (blockType === 0) {
//reached the terminator subblock
break;
}
var blockTypeStr = void 0;
switch (blockType) {
case 1:
blockTypeStr = 'SERVER';
break;
case 2:
blockTypeStr = 'DOMAIN';
break;
case 3:
blockTypeStr = 'FQDN';
break;
case 4:
blockTypeStr = 'DNS';
break;
case 5:
blockTypeStr = 'PARENT_DNS';
break;
default:
blockTypeStr = '';
break;
}
if (blockTypeStr) {
info[blockTypeStr] = buf.toString('ucs2', pos, pos + blockLength);
}
pos += blockLength;
}
return {
parsed: info,
buffer: targetInfoBuffer
};
})();
}
return obj;
}
function createType3Message(type2Message, username, password, workstation, target) {
var dataPos = 52, buf = new Buffer.alloc(1024);
if (workstation === undefined) {
workstation = os.hostname();
}
if (target === undefined) {
target = type2Message.targetName;
}
//signature
buf.write(NTLMSIGNATURE, 0, NTLMSIGNATURE.length, 'ascii');
//message type
buf.writeUInt32LE(3, 8);
if (type2Message.version === 2) {
dataPos = 64;
var ntlmHash = hash.createNTLMHash(password), nonce = hash.createPseudoRandomValue(16), lmv2 = hash.createLMv2Response(type2Message, username, ntlmHash, nonce, target), ntlmv2 = hash.createNTLMv2Response(type2Message, username, ntlmHash, nonce, target);
//lmv2 security buffer
buf.writeUInt16LE(lmv2.length, 12);
buf.writeUInt16LE(lmv2.length, 14);
buf.writeUInt32LE(dataPos, 16);
lmv2.copy(buf, dataPos);
dataPos += lmv2.length;
//ntlmv2 security buffer
buf.writeUInt16LE(ntlmv2.length, 20);
buf.writeUInt16LE(ntlmv2.length, 22);
buf.writeUInt32LE(dataPos, 24);
ntlmv2.copy(buf, dataPos);
dataPos += ntlmv2.length;
}
else {
var lmHash = hash.createLMHash(password), ntlmHash = hash.createNTLMHash(password), lm = hash.createLMResponse(type2Message.challenge, lmHash), ntlm = hash.createNTLMResponse(type2Message.challenge, ntlmHash);
//lm security buffer
buf.writeUInt16LE(lm.length, 12);
buf.writeUInt16LE(lm.length, 14);
buf.writeUInt32LE(dataPos, 16);
lm.copy(buf, dataPos);
dataPos += lm.length;
//ntlm security buffer
buf.writeUInt16LE(ntlm.length, 20);
buf.writeUInt16LE(ntlm.length, 22);
buf.writeUInt32LE(dataPos, 24);
ntlm.copy(buf, dataPos);
dataPos += ntlm.length;
}
//target name security buffer
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? target.length : target.length * 2, 28);
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? target.length : target.length * 2, 30);
buf.writeUInt32LE(dataPos, 32);
dataPos += buf.write(target, dataPos, type2Message.encoding);
//user name security buffer
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? username.length : username.length * 2, 36);
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? username.length : username.length * 2, 38);
buf.writeUInt32LE(dataPos, 40);
dataPos += buf.write(username, dataPos, type2Message.encoding);
//workstation name security buffer
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? workstation.length : workstation.length * 2, 44);
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? workstation.length : workstation.length * 2, 46);
buf.writeUInt32LE(dataPos, 48);
dataPos += buf.write(workstation, dataPos, type2Message.encoding);
if (type2Message.version === 2) {
//session key security buffer
buf.writeUInt16LE(0, 52);
buf.writeUInt16LE(0, 54);
buf.writeUInt32LE(0, 56);
//flags
buf.writeUInt32LE(type2Message.flags, 60);
}
return 'NTLM ' + buf.toString('base64', 0, dataPos);
}
module.exports = {
createType1Message: createType1Message,
decodeType2Message: decodeType2Message,
createType3Message: createType3Message
};
//# sourceMappingURL=ntlm.js.map

@ -0,0 +1,127 @@
"use strict";
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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NtlmClient = void 0;
var axios_1 = __importDefault(require("axios"));
var ntlm = __importStar(require("./ntlm"));
var https = __importStar(require("https"));
var http = __importStar(require("http"));
var dev_null_1 = __importDefault(require("dev-null"));
/**
* @param credentials An NtlmCredentials object containing the username and password
* @param AxiosConfig The Axios config for the instance you wish to create
*
* @returns This function returns an axios instance configured to use the provided credentials
*/
function NtlmClient(credentials, AxiosConfig) {
var _this = this;
var config = AxiosConfig !== null && AxiosConfig !== void 0 ? AxiosConfig : {};
if (!config.httpAgent) {
config.httpAgent = new http.Agent({ keepAlive: true });
}
if (!config.httpsAgent) {
config.httpsAgent = new https.Agent({ keepAlive: true });
}
var client = axios_1.default.create(config);
client.interceptors.response.use(function (response) {
return response;
}, function (err) { return __awaiter(_this, void 0, void 0, function () {
var error, t1Msg, t2Msg, t3Msg, stream_1;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
error = err.response;
if (!(error && error.status === 401
&& error.headers['www-authenticate']
&& error.headers['www-authenticate'].includes('NTLM'))) return [3 /*break*/, 3];
// This length check is a hack because SharePoint is awkward and will
// include the Negotiate option when responding with the T2 message
// There is nore we could do to ensure we are processing correctly,
// but this is the easiest option for now
if (error.headers['www-authenticate'].length < 50) {
t1Msg = ntlm.createType1Message(credentials.workstation, credentials.domain);
error.config.headers["Authorization"] = t1Msg;
}
else {
t2Msg = ntlm.decodeType2Message((error.headers['www-authenticate'].match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]);
t3Msg = ntlm.createType3Message(t2Msg, credentials.username, credentials.password, credentials.workstation, credentials.domain);
error.config.headers["X-retry"] = "false";
error.config.headers["Authorization"] = t3Msg;
}
if (!(error.config.responseType === "stream")) return [3 /*break*/, 2];
stream_1 = (_a = err.response) === null || _a === void 0 ? void 0 : _a.data;
if (!(stream_1 && !stream_1.readableEnded)) return [3 /*break*/, 2];
return [4 /*yield*/, new Promise(function (resolve) {
stream_1.pipe((0, dev_null_1.default)());
stream_1.once('close', resolve);
})];
case 1:
_b.sent();
_b.label = 2;
case 2: return [2 /*return*/, client(error.config)];
case 3: throw err;
}
});
}); });
return client;
}
exports.NtlmClient = NtlmClient;
//# sourceMappingURL=ntlmClient.js.map

@ -0,0 +1,71 @@
const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("./expression");
const { operatorMap } = require("./operators");
/**
* @param {ConditionExpression} expression Expression to evaluate
* @param {object} context Context to evaluate against; These are values for variables in the expression
* @returns {boolean} Whether the expression evaluates true or false
* @throws {Error}
*/
function evaluateExpression(expression, context) {
/**
* @type {import("./operators").ConditionOperator|null}
*/
const operator = operatorMap.get(expression.operator) || null;
if (operator === null) {
throw new Error("Unexpected expression operator ID '" + expression.operator + "'. Expected one of [" + operatorMap.keys().join(",") + "]");
}
if (!Object.prototype.hasOwnProperty.call(context, expression.variable)) {
throw new Error("Variable missing in context: " + expression.variable);
}
return operator.test(context[expression.variable], expression.value);
}
/**
* @param {ConditionExpressionGroup} group Group of expressions to evaluate
* @param {object} context Context to evaluate against; These are values for variables in the expression
* @returns {boolean} Whether the group evaluates true or false
* @throws {Error}
*/
function evaluateExpressionGroup(group, context) {
if (!group.children.length) {
throw new Error("ConditionExpressionGroup must contain at least one child.");
}
let result = null;
for (const child of group.children) {
let childResult;
if (child instanceof ConditionExpression) {
childResult = evaluateExpression(child, context);
} else if (child instanceof ConditionExpressionGroup) {
childResult = evaluateExpressionGroup(child, context);
} else {
throw new Error("Invalid child type in ConditionExpressionGroup. Expected ConditionExpression or ConditionExpressionGroup");
}
if (result === null) {
result = childResult; // Initialize result with the first child's result
} else if (child.andOr === LOGICAL.OR) {
result = result || childResult;
} else if (child.andOr === LOGICAL.AND) {
result = result && childResult;
} else {
throw new Error("Invalid logical operator in child of ConditionExpressionGroup. Expected 'and' or 'or'. Got '" + group.andOr + "'");
}
}
if (result === null) {
throw new Error("ConditionExpressionGroup did not result in a boolean.");
}
return result;
}
module.exports = {
evaluateExpression,
evaluateExpressionGroup,
};

@ -0,0 +1,111 @@
/**
* @readonly
* @enum {string}
*/
const LOGICAL = {
AND: "and",
OR: "or",
};
/**
* Recursively processes an array of raw condition objects and populates the given parent group with
* corresponding ConditionExpression or ConditionExpressionGroup instances.
* @param {Array} conditions Array of raw condition objects, where each object represents either a group or an expression.
* @param {ConditionExpressionGroup} parentGroup The parent group to which the instantiated ConditionExpression or ConditionExpressionGroup objects will be added.
* @returns {void}
*/
function processMonitorConditions(conditions, parentGroup) {
conditions.forEach(condition => {
const andOr = condition.andOr === LOGICAL.OR ? LOGICAL.OR : LOGICAL.AND;
if (condition.type === "group") {
const group = new ConditionExpressionGroup([], andOr);
// Recursively process the group's children
processMonitorConditions(condition.children, group);
parentGroup.children.push(group);
} else if (condition.type === "expression") {
const expression = new ConditionExpression(condition.variable, condition.operator, condition.value, andOr);
parentGroup.children.push(expression);
}
});
}
class ConditionExpressionGroup {
/**
* @type {ConditionExpressionGroup[]|ConditionExpression[]} Groups and/or expressions to test
*/
children = [];
/**
* @type {LOGICAL} Connects group result with previous group/expression results
*/
andOr;
/**
* @param {ConditionExpressionGroup[]|ConditionExpression[]} children Groups and/or expressions to test
* @param {LOGICAL} andOr Connects group result with previous group/expression results
*/
constructor(children = [], andOr = LOGICAL.AND) {
this.children = children;
this.andOr = andOr;
}
/**
* @param {Monitor} monitor Monitor instance
* @returns {ConditionExpressionGroup|null} A ConditionExpressionGroup with the Monitor's conditions
*/
static fromMonitor(monitor) {
const conditions = JSON.parse(monitor.conditions);
if (conditions.length === 0) {
return null;
}
const root = new ConditionExpressionGroup();
processMonitorConditions(conditions, root);
return root;
}
}
class ConditionExpression {
/**
* @type {string} ID of variable
*/
variable;
/**
* @type {string} ID of operator
*/
operator;
/**
* @type {string} Value to test with the operator
*/
value;
/**
* @type {LOGICAL} Connects expression result with previous group/expression results
*/
andOr;
/**
* @param {string} variable ID of variable to test against
* @param {string} operator ID of operator to test the variable with
* @param {string} value Value to test with the operator
* @param {LOGICAL} andOr Connects expression result with previous group/expression results
*/
constructor(variable, operator, value, andOr = LOGICAL.AND) {
this.variable = variable;
this.operator = operator;
this.value = value;
this.andOr = andOr;
}
}
module.exports = {
LOGICAL,
ConditionExpressionGroup,
ConditionExpression,
};

@ -0,0 +1,318 @@
class ConditionOperator {
id = undefined;
caption = undefined;
/**
* @type {mixed} variable
* @type {mixed} value
*/
test(variable, value) {
throw new Error("You need to override test()");
}
}
const OP_STR_EQUALS = "equals";
const OP_STR_NOT_EQUALS = "not_equals";
const OP_CONTAINS = "contains";
const OP_NOT_CONTAINS = "not_contains";
const OP_STARTS_WITH = "starts_with";
const OP_NOT_STARTS_WITH = "not_starts_with";
const OP_ENDS_WITH = "ends_with";
const OP_NOT_ENDS_WITH = "not_ends_with";
const OP_NUM_EQUALS = "num_equals";
const OP_NUM_NOT_EQUALS = "num_not_equals";
const OP_LT = "lt";
const OP_GT = "gt";
const OP_LTE = "lte";
const OP_GTE = "gte";
/**
* Asserts a variable is equal to a value.
*/
class StringEqualsOperator extends ConditionOperator {
id = OP_STR_EQUALS;
caption = "equals";
/**
* @inheritdoc
*/
test(variable, value) {
return variable === value;
}
}
/**
* Asserts a variable is not equal to a value.
*/
class StringNotEqualsOperator extends ConditionOperator {
id = OP_STR_NOT_EQUALS;
caption = "not equals";
/**
* @inheritdoc
*/
test(variable, value) {
return variable !== value;
}
}
/**
* Asserts a variable contains a value.
* Handles both Array and String variable types.
*/
class ContainsOperator extends ConditionOperator {
id = OP_CONTAINS;
caption = "contains";
/**
* @inheritdoc
*/
test(variable, value) {
if (Array.isArray(variable)) {
return variable.includes(value);
}
return variable.indexOf(value) !== -1;
}
}
/**
* Asserts a variable does not contain a value.
* Handles both Array and String variable types.
*/
class NotContainsOperator extends ConditionOperator {
id = OP_NOT_CONTAINS;
caption = "not contains";
/**
* @inheritdoc
*/
test(variable, value) {
if (Array.isArray(variable)) {
return !variable.includes(value);
}
return variable.indexOf(value) === -1;
}
}
/**
* Asserts a variable starts with a value.
*/
class StartsWithOperator extends ConditionOperator {
id = OP_STARTS_WITH;
caption = "starts with";
/**
* @inheritdoc
*/
test(variable, value) {
return variable.startsWith(value);
}
}
/**
* Asserts a variable does not start with a value.
*/
class NotStartsWithOperator extends ConditionOperator {
id = OP_NOT_STARTS_WITH;
caption = "not starts with";
/**
* @inheritdoc
*/
test(variable, value) {
return !variable.startsWith(value);
}
}
/**
* Asserts a variable ends with a value.
*/
class EndsWithOperator extends ConditionOperator {
id = OP_ENDS_WITH;
caption = "ends with";
/**
* @inheritdoc
*/
test(variable, value) {
return variable.endsWith(value);
}
}
/**
* Asserts a variable does not end with a value.
*/
class NotEndsWithOperator extends ConditionOperator {
id = OP_NOT_ENDS_WITH;
caption = "not ends with";
/**
* @inheritdoc
*/
test(variable, value) {
return !variable.endsWith(value);
}
}
/**
* Asserts a numeric variable is equal to a value.
*/
class NumberEqualsOperator extends ConditionOperator {
id = OP_NUM_EQUALS;
caption = "equals";
/**
* @inheritdoc
*/
test(variable, value) {
return variable === Number(value);
}
}
/**
* Asserts a numeric variable is not equal to a value.
*/
class NumberNotEqualsOperator extends ConditionOperator {
id = OP_NUM_NOT_EQUALS;
caption = "not equals";
/**
* @inheritdoc
*/
test(variable, value) {
return variable !== Number(value);
}
}
/**
* Asserts a variable is less than a value.
*/
class LessThanOperator extends ConditionOperator {
id = OP_LT;
caption = "less than";
/**
* @inheritdoc
*/
test(variable, value) {
return variable < Number(value);
}
}
/**
* Asserts a variable is greater than a value.
*/
class GreaterThanOperator extends ConditionOperator {
id = OP_GT;
caption = "greater than";
/**
* @inheritdoc
*/
test(variable, value) {
return variable > Number(value);
}
}
/**
* Asserts a variable is less than or equal to a value.
*/
class LessThanOrEqualToOperator extends ConditionOperator {
id = OP_LTE;
caption = "less than or equal to";
/**
* @inheritdoc
*/
test(variable, value) {
return variable <= Number(value);
}
}
/**
* Asserts a variable is greater than or equal to a value.
*/
class GreaterThanOrEqualToOperator extends ConditionOperator {
id = OP_GTE;
caption = "greater than or equal to";
/**
* @inheritdoc
*/
test(variable, value) {
return variable >= Number(value);
}
}
const operatorMap = new Map([
[ OP_STR_EQUALS, new StringEqualsOperator ],
[ OP_STR_NOT_EQUALS, new StringNotEqualsOperator ],
[ OP_CONTAINS, new ContainsOperator ],
[ OP_NOT_CONTAINS, new NotContainsOperator ],
[ OP_STARTS_WITH, new StartsWithOperator ],
[ OP_NOT_STARTS_WITH, new NotStartsWithOperator ],
[ OP_ENDS_WITH, new EndsWithOperator ],
[ OP_NOT_ENDS_WITH, new NotEndsWithOperator ],
[ OP_NUM_EQUALS, new NumberEqualsOperator ],
[ OP_NUM_NOT_EQUALS, new NumberNotEqualsOperator ],
[ OP_LT, new LessThanOperator ],
[ OP_GT, new GreaterThanOperator ],
[ OP_LTE, new LessThanOrEqualToOperator ],
[ OP_GTE, new GreaterThanOrEqualToOperator ],
]);
const defaultStringOperators = [
operatorMap.get(OP_STR_EQUALS),
operatorMap.get(OP_STR_NOT_EQUALS),
operatorMap.get(OP_CONTAINS),
operatorMap.get(OP_NOT_CONTAINS),
operatorMap.get(OP_STARTS_WITH),
operatorMap.get(OP_NOT_STARTS_WITH),
operatorMap.get(OP_ENDS_WITH),
operatorMap.get(OP_NOT_ENDS_WITH)
];
const defaultNumberOperators = [
operatorMap.get(OP_NUM_EQUALS),
operatorMap.get(OP_NUM_NOT_EQUALS),
operatorMap.get(OP_LT),
operatorMap.get(OP_GT),
operatorMap.get(OP_LTE),
operatorMap.get(OP_GTE)
];
module.exports = {
OP_STR_EQUALS,
OP_STR_NOT_EQUALS,
OP_CONTAINS,
OP_NOT_CONTAINS,
OP_STARTS_WITH,
OP_NOT_STARTS_WITH,
OP_ENDS_WITH,
OP_NOT_ENDS_WITH,
OP_NUM_EQUALS,
OP_NUM_NOT_EQUALS,
OP_LT,
OP_GT,
OP_LTE,
OP_GTE,
operatorMap,
defaultStringOperators,
defaultNumberOperators,
ConditionOperator,
};

@ -0,0 +1,31 @@
/**
* Represents a variable used in a condition and the set of operators that can be applied to this variable.
*
* A `ConditionVariable` holds the ID of the variable and a list of operators that define how this variable can be evaluated
* in conditions. For example, if the variable is a request body or a specific field in a request, the operators can include
* operations such as equality checks, comparisons, or other custom evaluations.
*/
class ConditionVariable {
/**
* @type {string}
*/
id;
/**
* @type {import("./operators").ConditionOperator[]}
*/
operators = {};
/**
* @param {string} id ID of variable
* @param {import("./operators").ConditionOperator[]} operators Operators the condition supports
*/
constructor(id, operators = []) {
this.id = id;
this.operators = operators;
}
}
module.exports = {
ConditionVariable,
};

@ -1,13 +1,22 @@
const { MonitorType } = require("./monitor-type");
const { UP } = require("../../src/util");
const { UP, DOWN } = require("../../src/util");
const dayjs = require("dayjs");
const { dnsResolve } = require("../util-server");
const { R } = require("redbean-node");
const { ConditionVariable } = require("../monitor-conditions/variables");
const { defaultStringOperators } = require("../monitor-conditions/operators");
const { ConditionExpressionGroup } = require("../monitor-conditions/expression");
const { evaluateExpressionGroup } = require("../monitor-conditions/evaluator");
class DnsMonitorType extends MonitorType {
name = "dns";
supportsConditions = true;
conditionVariables = [
new ConditionVariable("record", defaultStringOperators ),
];
/**
* @inheritdoc
*/
@ -18,28 +27,48 @@ class DnsMonitorType extends MonitorType {
let dnsRes = await dnsResolve(monitor.hostname, monitor.dns_resolve_server, monitor.port, monitor.dns_resolve_type);
heartbeat.ping = dayjs().valueOf() - startTime;
if (monitor.dns_resolve_type === "A" || monitor.dns_resolve_type === "AAAA" || monitor.dns_resolve_type === "TXT" || monitor.dns_resolve_type === "PTR") {
dnsMessage += "Records: ";
dnsMessage += dnsRes.join(" | ");
} else if (monitor.dns_resolve_type === "CNAME" || monitor.dns_resolve_type === "PTR") {
dnsMessage += dnsRes[0];
} else if (monitor.dns_resolve_type === "CAA") {
dnsMessage += dnsRes[0].issue;
} else if (monitor.dns_resolve_type === "MX") {
dnsRes.forEach(record => {
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
});
dnsMessage = dnsMessage.slice(0, -2);
} else if (monitor.dns_resolve_type === "NS") {
dnsMessage += "Servers: ";
dnsMessage += dnsRes.join(" | ");
} else if (monitor.dns_resolve_type === "SOA") {
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
} else if (monitor.dns_resolve_type === "SRV") {
dnsRes.forEach(record => {
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
});
dnsMessage = dnsMessage.slice(0, -2);
const conditions = ConditionExpressionGroup.fromMonitor(monitor);
let conditionsResult = true;
const handleConditions = (data) => conditions ? evaluateExpressionGroup(conditions, data) : true;
switch (monitor.dns_resolve_type) {
case "A":
case "AAAA":
case "TXT":
case "PTR":
dnsMessage = `Records: ${dnsRes.join(" | ")}`;
conditionsResult = dnsRes.some(record => handleConditions({ record }));
break;
case "CNAME":
dnsMessage = dnsRes[0];
conditionsResult = handleConditions({ record: dnsRes[0] });
break;
case "CAA":
dnsMessage = dnsRes[0].issue;
conditionsResult = handleConditions({ record: dnsRes[0].issue });
break;
case "MX":
dnsMessage = dnsRes.map(record => `Hostname: ${record.exchange} - Priority: ${record.priority}`).join(" | ");
conditionsResult = dnsRes.some(record => handleConditions({ record: record.exchange }));
break;
case "NS":
dnsMessage = `Servers: ${dnsRes.join(" | ")}`;
conditionsResult = dnsRes.some(record => handleConditions({ record }));
break;
case "SOA":
dnsMessage = `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
conditionsResult = handleConditions({ record: dnsRes.nsname });
break;
case "SRV":
dnsMessage = dnsRes.map(record => `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight}`).join(" | ");
conditionsResult = dnsRes.some(record => handleConditions({ record: record.name }));
break;
}
if (monitor.dns_last_result !== dnsMessage && dnsMessage !== undefined) {
@ -47,7 +76,7 @@ class DnsMonitorType extends MonitorType {
}
heartbeat.msg = dnsMessage;
heartbeat.status = UP;
heartbeat.status = conditionsResult ? UP : DOWN;
}
}

@ -4,7 +4,6 @@ const { MongoClient } = require("mongodb");
const jsonata = require("jsonata");
class MongodbMonitorType extends MonitorType {
name = "mongodb";
/**
@ -49,8 +48,7 @@ class MongodbMonitorType extends MonitorType {
* Connect to and run MongoDB command on a MongoDB database
* @param {string} connectionString The database connection string
* @param {object} command MongoDB command to run on the database
* @returns {Promise<(string[] | object[] | object)>} Response from
* server
* @returns {Promise<(string[] | object[] | object)>} Response from server
*/
async runMongodbCommand(connectionString, command) {
let client = await MongoClient.connect(connectionString);

@ -1,6 +1,19 @@
class MonitorType {
name = undefined;
/**
* Whether or not this type supports monitor conditions. Controls UI visibility in monitor form.
* @type {boolean}
*/
supportsConditions = false;
/**
* Variables supported by this type. e.g. an HTTP type could have a "response_code" variable to test against.
* This property controls the choices displayed in the monitor edit form.
* @type {import("../monitor-conditions/variables").ConditionVariable[]}
*/
conditionVariables = [];
/**
* Run the monitoring check on the given monitor
* @param {Monitor} monitor Monitor to check
@ -11,7 +24,6 @@ class MonitorType {
async check(monitor, heartbeat, server) {
throw new Error("You need to override check()");
}
}
module.exports = {

@ -4,15 +4,10 @@ const mqtt = require("mqtt");
const jsonata = require("jsonata");
class MqttMonitorType extends MonitorType {
name = "mqtt";
/**
* Run the monitoring check on the MQTT monitor
* @param {Monitor} monitor Monitor to check
* @param {Heartbeat} heartbeat Monitor heartbeat to update
* @param {UptimeKumaServer} server Uptime Kuma server
* @returns {Promise<void>}
* @inheritdoc
*/
async check(monitor, heartbeat, server) {
const receivedMessage = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {

@ -0,0 +1,63 @@
const { MonitorType } = require("./monitor-type");
const { UP, log, evaluateJsonQuery } = require("../../src/util");
const snmp = require("net-snmp");
class SNMPMonitorType extends MonitorType {
name = "snmp";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
let session;
try {
const sessionOptions = {
port: monitor.port || "161",
retries: monitor.maxretries,
timeout: monitor.timeout * 1000,
version: snmp.Version[monitor.snmpVersion],
};
session = snmp.createSession(monitor.hostname, monitor.radiusPassword, sessionOptions);
// Handle errors during session creation
session.on("error", (error) => {
throw new Error(`Error creating SNMP session: ${error.message}`);
});
const varbinds = await new Promise((resolve, reject) => {
session.get([ monitor.snmpOid ], (error, varbinds) => {
error ? reject(error) : resolve(varbinds);
});
});
log.debug("monitor", `SNMP: Received varbinds (Type: ${snmp.ObjectType[varbinds[0].type]} Value: ${varbinds[0].value})`);
if (varbinds.length === 0) {
throw new Error(`No varbinds returned from SNMP session (OID: ${monitor.snmpOid})`);
}
if (varbinds[0].type === snmp.ObjectType.NoSuchInstance) {
throw new Error(`The SNMP query returned that no instance exists for OID ${monitor.snmpOid}`);
}
// We restrict querying to one OID per monitor, therefore `varbinds[0]` will always contain the value we're interested in.
const value = varbinds[0].value;
const { status, response } = await evaluateJsonQuery(value, monitor.jsonPath, monitor.jsonPathOperator, monitor.expectedValue);
if (status) {
heartbeat.status = UP;
heartbeat.msg = `JSON query passes (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`;
} else {
throw new Error(`JSON query does not pass (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`);
}
} finally {
if (session) {
session.close();
}
}
}
}
module.exports = {
SNMPMonitorType,
};

@ -2,23 +2,13 @@ const { MonitorType } = require("./monitor-type");
const { UP } = require("../../src/util");
const childProcessAsync = require("promisify-child-process");
/**
* A TailscalePing class extends the MonitorType.
* It runs Tailscale ping to monitor the status of a specific node.
*/
class TailscalePing extends MonitorType {
name = "tailscale-ping";
/**
* Checks the ping status of the URL associated with the monitor.
* It then parses the Tailscale ping command output to update the heatrbeat.
* @param {object} monitor The monitor object associated with the check.
* @param {object} heartbeat The heartbeat object to update.
* @returns {Promise<void>}
* @throws Error if checking Tailscale ping encounters any error
* @inheritdoc
*/
async check(monitor, heartbeat) {
async check(monitor, heartbeat, _server) {
try {
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
this.parseTailscaleOutput(tailscaleOutput, heartbeat);

@ -0,0 +1,35 @@
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;

@ -87,7 +87,6 @@ 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";

@ -33,26 +33,6 @@ class Discord extends NotificationProvider {
return okMsg;
}
let address;
switch (monitorJSON["type"]) {
case "ping":
address = monitorJSON["hostname"];
break;
case "port":
case "dns":
case "gamedig":
case "steam":
address = monitorJSON["hostname"];
if (monitorJSON["port"]) {
address += ":" + monitorJSON["port"];
}
break;
default:
address = monitorJSON["url"];
break;
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] === DOWN) {
let discorddowndata = {
@ -68,7 +48,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
value: this.extractAddress(monitorJSON),
},
{
name: `Time (${heartbeatJSON["timezone"]})`,
@ -105,7 +85,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
value: this.extractAddress(monitorJSON),
},
{
name: `Time (${heartbeatJSON["timezone"]})`,

@ -25,25 +25,29 @@ class Feishu extends NotificationProvider {
if (heartbeatJSON["status"] === DOWN) {
let downdata = {
msg_type: "post",
content: {
post: {
zh_cn: {
title: "UptimeKuma Alert: [Down] " + monitorJSON["name"],
content: [
[
{
tag: "text",
text:
"[Down] " +
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
},
],
],
msg_type: "interactive",
card: {
config: {
update_multi: false,
wide_screen_mode: true,
},
header: {
title: {
tag: "plain_text",
content: "UptimeKuma Alert: [Down] " + monitorJSON["name"],
},
template: "red",
},
},
elements: [
{
tag: "div",
text: {
tag: "lark_md",
content: getContent(heartbeatJSON),
},
}
]
}
};
await axios.post(notification.feishuWebHookUrl, downdata);
return okMsg;
@ -51,25 +55,29 @@ class Feishu extends NotificationProvider {
if (heartbeatJSON["status"] === UP) {
let updata = {
msg_type: "post",
content: {
post: {
zh_cn: {
title: "UptimeKuma Alert: [Up] " + monitorJSON["name"],
content: [
[
{
tag: "text",
text:
"[Up] " +
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
},
],
],
msg_type: "interactive",
card: {
config: {
update_multi: false,
wide_screen_mode: true,
},
header: {
title: {
tag: "plain_text",
content: "UptimeKuma Alert: [UP] " + monitorJSON["name"],
},
template: "green",
},
},
elements: [
{
tag: "div",
text: {
tag: "lark_md",
content: getContent(heartbeatJSON),
},
},
]
}
};
await axios.post(notification.feishuWebHookUrl, updata);
return okMsg;
@ -80,4 +88,17 @@ class Feishu extends NotificationProvider {
}
}
/**
* Get content
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {string} Return Successful Message
*/
function getContent(heartbeatJSON) {
return [
"**Message**: " + heartbeatJSON["msg"],
"**Ping**: " + (heartbeatJSON["ping"] == null ? "N/A" : heartbeatJSON["ping"] + " ms"),
`**Time (${heartbeatJSON["timezone"]})**: ${heartbeatJSON["localDateTime"]}`
].join("\n");
}
module.exports = Feishu;

@ -1,4 +1,3 @@
const { log } = require("../../src/util");
const NotificationProvider = require("./notification-provider");
const {
relayInit,
@ -12,16 +11,7 @@ const {
// polyfills for node versions
const semver = require("semver");
const nodeVersion = process.version;
if (semver.lt(nodeVersion, "16.0.0")) {
log.warn("monitor", "Node <= 16 is unsupported for nostr, sorry :(");
} else if (semver.lt(nodeVersion, "18.0.0")) {
// polyfills for node 16
global.crypto = require("crypto");
global.WebSocket = require("isomorphic-ws");
if (typeof crypto !== "undefined" && !crypto.subtle && crypto.webcrypto) {
crypto.subtle = crypto.webcrypto.subtle;
}
} else if (semver.lt(nodeVersion, "20.0.0")) {
if (semver.lt(nodeVersion, "20.0.0")) {
// polyfills for node 18
global.crypto = require("crypto");
global.WebSocket = require("isomorphic-ws");

@ -19,6 +19,36 @@ class NotificationProvider {
throw new Error("Have to override Notification.send(...)");
}
/**
* Extracts the address from a monitor JSON object based on its type.
* @param {?object} monitorJSON Monitor details (For Up/Down only)
* @returns {string} The extracted address based on the monitor type.
*/
extractAddress(monitorJSON) {
if (!monitorJSON) {
return "";
}
switch (monitorJSON["type"]) {
case "push":
return "Heartbeat";
case "ping":
return monitorJSON["hostname"];
case "port":
case "dns":
case "gamedig":
case "steam":
if (monitorJSON["port"]) {
return monitorJSON["hostname"] + ":" + monitorJSON["port"];
}
return monitorJSON["hostname"];
default:
if (![ "https://", "http://", "" ].includes(monitorJSON["url"])) {
return monitorJSON["url"];
}
return "";
}
}
/**
* Throws an error
* @param {any} error The error to throw

@ -0,0 +1,47 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Onesender extends NotificationProvider {
name = "Onesender";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let data = {
heartbeat: heartbeatJSON,
monitor: monitorJSON,
msg,
to: notification.onesenderReceiver,
type: "text",
recipient_type: "individual",
text: {
body: msg
}
};
if (notification.onesenderTypeReceiver === "private") {
data.to = notification.onesenderReceiver + "@s.whatsapp.net";
} else {
data.recipient_type = "group";
data.to = notification.onesenderReceiver + "@g.us";
}
let config = {
headers: {
"Authorization": "Bearer " + notification.onesenderToken,
}
};
await axios.post(notification.onesenderURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Onesender;

@ -1,3 +1,6 @@
const { getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
@ -23,6 +26,12 @@ class Pushover extends NotificationProvider {
"html": 1,
};
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorJSON) {
data["url"] = baseURL + getMonitorRelativeURL(monitorJSON.id);
data["url_title"] = "Link to Monitor";
}
if (notification.pushoverdevice) {
data.device = notification.pushoverdevice;
}

@ -11,8 +11,13 @@ class ServerChan extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
// serverchan3 requires sending via ft07.com
const url = String(notification.serverChanSendKey).startsWith("sctp")
? `https://${notification.serverChanSendKey}.push.ft07.com/send`
: `https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`;
try {
await axios.post(`https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`, {
await axios.post(url, {
"title": this.checkStatus(heartbeatJSON, monitorJSON),
"desp": msg,
});

@ -32,28 +32,7 @@ class SevenIO extends NotificationProvider {
return okMsg;
}
let address = "";
switch (monitorJSON["type"]) {
case "ping":
address = monitorJSON["hostname"];
break;
case "port":
case "dns":
case "gamedig":
case "steam":
address = monitorJSON["hostname"];
if (monitorJSON["port"]) {
address += ":" + monitorJSON["port"];
}
break;
default:
if (![ "https://", "http://", "" ].includes(monitorJSON["url"])) {
address = monitorJSON["url"];
}
break;
}
let address = this.extractAddress(monitorJSON);
if (address !== "") {
address = `(${address}) `;
}

@ -0,0 +1,52 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN } = require("../../src/util");
class SIGNL4 extends NotificationProvider {
name = "SIGNL4";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let data = {
heartbeat: heartbeatJSON,
monitor: monitorJSON,
msg,
// Source system
"X-S4-SourceSystem": "UptimeKuma",
monitorUrl: this.extractAddress(monitorJSON),
};
const config = {
headers: {
"Content-Type": "application/json"
}
};
if (heartbeatJSON == null) {
// Test alert
data.title = "Uptime Kuma Alert";
data.message = msg;
} else if (heartbeatJSON.status === UP) {
data.title = "Uptime Kuma Monitor ✅ Up";
data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID;
data["X-S4-Status"] = "resolved";
} else if (heartbeatJSON.status === DOWN) {
data.title = "Uptime Kuma Monitor 🔴 Down";
data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID;
data["X-S4-Status"] = "new";
}
await axios.post(notification.webhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SIGNL4;

@ -48,7 +48,8 @@ class Slack extends NotificationProvider {
}
if (monitorJSON.url) {
const address = this.extractAddress(monitorJSON);
if (address) {
actions.push({
"type": "button",
"text": {
@ -56,7 +57,7 @@ class Slack extends NotificationProvider {
"text": "Visit site",
},
"value": "Site",
"url": monitorJSON.url,
"url": address,
});
}
@ -139,17 +140,22 @@ class Slack extends NotificationProvider {
const title = "Uptime Kuma Alert";
let data = {
"text": `${title}\n${msg}`,
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
"attachments": [
"attachments": [],
};
if (notification.slackrichmessage) {
data.attachments.push(
{
"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);

@ -93,12 +93,7 @@ class SMTP extends NotificationProvider {
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
monitorHostnameOrURL = monitorJSON["url"];
} else {
monitorHostnameOrURL = monitorJSON["hostname"];
}
monitorHostnameOrURL = this.extractAddress(monitorJSON);
}
let serviceStatus = "⚠️ Test";

@ -34,25 +34,7 @@ class Squadcast extends NotificationProvider {
data.status = "resolve";
}
let address;
switch (monitorJSON["type"]) {
case "ping":
address = monitorJSON["hostname"];
break;
case "port":
case "dns":
case "steam":
address = monitorJSON["hostname"];
if (monitorJSON["port"]) {
address += ":" + monitorJSON["port"];
}
break;
default:
address = monitorJSON["url"];
break;
}
data.tags["AlertAddress"] = address;
data.tags["AlertAddress"] = this.extractAddress(monitorJSON);
monitorJSON["tags"].forEach(tag => {
data.tags[tag["name"]] = {

@ -216,21 +216,6 @@ class Teams extends NotificationProvider {
return okMsg;
}
let monitorUrl;
switch (monitorJSON["type"]) {
case "http":
case "keywork":
monitorUrl = monitorJSON["url"];
break;
case "docker":
monitorUrl = monitorJSON["docker_host"];
break;
default:
monitorUrl = monitorJSON["hostname"];
break;
}
const baseURL = await setting("primaryBaseURL");
let dashboardUrl;
if (baseURL) {
@ -240,7 +225,7 @@ class Teams extends NotificationProvider {
const payload = this._notificationPayloadFactory({
heartbeatJSON: heartbeatJSON,
monitorName: monitorJSON.name,
monitorUrl: monitorUrl,
monitorUrl: this.extractAddress(monitorJSON),
dashboardUrl: dashboardUrl,
});

@ -10,11 +10,22 @@ 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}`, {
"title": "Uptime-Kuma",
"body": msg,
});
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, data);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);

@ -0,0 +1,77 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Threema extends NotificationProvider {
name = "threema";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const url = "https://msgapi.threema.ch/send_simple";
const config = {
headers: {
"Accept": "*/*",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
}
};
const data = {
from: notification.threemaSenderIdentity,
secret: notification.threemaSecret,
text: msg
};
switch (notification.threemaRecipientType) {
case "identity":
data.to = notification.threemaRecipient;
break;
case "phone":
data.phone = notification.threemaRecipient;
break;
case "email":
data.email = notification.threemaRecipient;
break;
default:
throw new Error(`Unsupported recipient type: ${notification.threemaRecipientType}`);
}
try {
await axios.post(url, new URLSearchParams(data), config);
return "Threema notification sent successfully.";
} catch (error) {
const errorMessage = this.handleApiError(error);
this.throwGeneralAxiosError(errorMessage);
}
}
/**
* Handle Threema API errors
* @param {any} error The error to handle
* @returns {string} Additional error context
*/
handleApiError(error) {
if (!error.response) {
return error.message;
}
switch (error.response.status) {
case 400:
return "Invalid recipient identity or account not set up for basic mode (400).";
case 401:
return "Incorrect API identity or secret (401).";
case 402:
return "No credits remaining (402).";
case 404:
return "Recipient not found (404).";
case 413:
return "Message is too long (413).";
case 500:
return "Temporary internal server error (500).";
default:
return error.message;
}
}
}
module.exports = Threema;

@ -32,20 +32,17 @@ class WeCom extends NotificationProvider {
* @returns {object} Message
*/
composeMessage(heartbeatJSON, msg) {
let title;
let title = "UptimeKuma Message";
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 + msg
content: title + "\n" + msg
}
};
}

@ -0,0 +1,51 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class WPush extends NotificationProvider {
name = "WPush";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
const context = {
"title": this.checkStatus(heartbeatJSON, monitorJSON),
"content": msg,
"apikey": notification.wpushAPIkey,
"channel": notification.wpushChannel
};
const result = await axios.post("https://api.wpush.cn/api/v1/send", context);
if (result.data.code !== 0) {
throw result.data.message;
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
/**
* Get the formatted title for message
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
* @param {?object} monitorJSON Monitor details (For Up/Down only)
* @returns {string} Formatted title
*/
checkStatus(heartbeatJSON, monitorJSON) {
let title = "UptimeKuma Message";
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
title = "UptimeKuma Monitor Up " + monitorJSON["name"];
}
if (heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
title = "UptimeKuma Monitor Down " + monitorJSON["name"];
}
return title;
}
}
module.exports = WPush;

@ -13,9 +13,9 @@ class ZohoCliq extends NotificationProvider {
*/
_statusMessageFactory = (status, monitorName) => {
if (status === DOWN) {
return `🔴 Application [${monitorName}] went down\n`;
return `🔴 [${monitorName}] went down\n`;
} else if (status === UP) {
return `✅ Application [${monitorName}] is back online\n`;
return `### ✅ [${monitorName}] is back online\n`;
}
return "Notification\n";
};
@ -46,16 +46,11 @@ class ZohoCliq extends NotificationProvider {
monitorUrl,
}) => {
const payload = [];
payload.push("### Uptime Kuma\n");
payload.push(this._statusMessageFactory(status, monitorName));
payload.push(`*Description:* ${monitorMessage}`);
if (monitorName) {
payload.push(`*Monitor:* ${monitorName}`);
}
if (monitorUrl && monitorUrl !== "https://") {
payload.push(`*URL:* [${monitorUrl}](${monitorUrl})`);
payload.push(`*URL:* ${monitorUrl}`);
}
return payload;
@ -87,24 +82,10 @@ class ZohoCliq extends NotificationProvider {
return okMsg;
}
let url;
switch (monitorJSON["type"]) {
case "http":
case "keywork":
url = monitorJSON["url"];
break;
case "docker":
url = monitorJSON["docker_host"];
break;
default:
url = monitorJSON["hostname"];
break;
}
const payload = this._notificationPayloadFactory({
monitorMessage: heartbeatJSON.msg,
monitorName: monitorJSON.name,
monitorUrl: url,
monitorUrl: this.extractAddress(monitorJSON),
status: heartbeatJSON.status
});

@ -11,6 +11,7 @@ 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");
@ -42,6 +43,7 @@ const Pushy = require("./notification-providers/pushy");
const RocketChat = require("./notification-providers/rocket-chat");
const SerwerSMS = require("./notification-providers/serwersms");
const Signal = require("./notification-providers/signal");
const SIGNL4 = require("./notification-providers/signl4");
const Slack = require("./notification-providers/slack");
const SMSPartner = require("./notification-providers/smspartner");
const SMSEagle = require("./notification-providers/smseagle");
@ -51,6 +53,7 @@ const Stackfield = require("./notification-providers/stackfield");
const Teams = require("./notification-providers/teams");
const TechulusPush = require("./notification-providers/techulus-push");
const Telegram = require("./notification-providers/telegram");
const Threema = require("./notification-providers/threema");
const Twilio = require("./notification-providers/twilio");
const Splunk = require("./notification-providers/splunk");
const Webhook = require("./notification-providers/webhook");
@ -63,6 +66,8 @@ const SevenIO = require("./notification-providers/sevenio");
const Whapi = require("./notification-providers/whapi");
const GtxMessaging = require("./notification-providers/gtx-messaging");
const Cellsynt = require("./notification-providers/cellsynt");
const Onesender = require("./notification-providers/onesender");
const Wpush = require("./notification-providers/wpush");
class Notification {
@ -91,6 +96,7 @@ class Notification {
new SMSC(),
new DingDing(),
new Discord(),
new Elks(),
new Feishu(),
new FreeMobile(),
new GoogleChat(),
@ -110,6 +116,7 @@ class Notification {
new Ntfy(),
new Octopush(),
new OneBot(),
new Onesender(),
new Opsgenie(),
new PagerDuty(),
new FlashDuty(),
@ -123,6 +130,7 @@ class Notification {
new ServerChan(),
new SerwerSMS(),
new Signal(),
new SIGNL4(),
new SMSManager(),
new SMSPartner(),
new Slack(),
@ -133,6 +141,7 @@ class Notification {
new Teams(),
new TechulusPush(),
new Telegram(),
new Threema(),
new Twilio(),
new Splunk(),
new Webhook(),
@ -143,6 +152,7 @@ class Notification {
new Whapi(),
new GtxMessaging(),
new Cellsynt(),
new Wpush(),
];
for (let item of list) {
if (! item.name) {

@ -232,8 +232,8 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
let requestedDuration = request.params.duration !== undefined ? request.params.duration : "24h";
const overrideValue = value && parseFloat(value);
if (requestedDuration === "24") {
requestedDuration = "24h";
if (/^[0-9]+$/.test(requestedDuration)) {
requestedDuration = `${requestedDuration}h`;
}
let publicMonitor = await R.getRow(`
@ -265,7 +265,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
// build a label string. If a custom label is given, override the default one (requestedDuration)
badgeValues.label = filterAndJoin([
labelPrefix,
label ?? `Uptime (${requestedDuration}${labelSuffix})`,
label ?? `Uptime (${requestedDuration.slice(0, -1)}${labelSuffix})`,
]);
badgeValues.message = filterAndJoin([ prefix, cleanUptime, suffix ]);
}
@ -302,8 +302,8 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
let requestedDuration = request.params.duration !== undefined ? request.params.duration : "24h";
const overrideValue = value && parseFloat(value);
if (requestedDuration === "24") {
requestedDuration = "24h";
if (/^[0-9]+$/.test(requestedDuration)) {
requestedDuration = `${requestedDuration}h`;
}
// Check if monitor is public
@ -325,7 +325,7 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
badgeValues.labelColor = labelColor ?? "";
// build a lable string. If a custom label is given, override the default one (requestedDuration)
badgeValues.label = filterAndJoin([ labelPrefix, label ?? `Avg. Ping (${requestedDuration}${labelSuffix})` ]);
badgeValues.label = filterAndJoin([ labelPrefix, label ?? `Avg. Ping (${requestedDuration.slice(0, -1)}${labelSuffix})` ]);
badgeValues.message = filterAndJoin([ prefix, avgPing, suffix ]);
}

@ -18,6 +18,11 @@ router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
router.get("/status/:slug/rss", cache("5 minutes"), async (request, response) => {
let slug = request.params.slug;
await StatusPage.handleStatusPageRSSResponse(response, slug);
});
router.get("/status", cache("5 minutes"), async (request, response) => {
let slug = "default";
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);

@ -19,7 +19,7 @@ const nodeVersion = process.versions.node;
// Get the required Node.js version from package.json
const requiredNodeVersions = require("../package.json").engines.node;
const bannedNodeVersions = " < 14 || 20.0.* || 20.1.* || 20.2.* || 20.3.* ";
const bannedNodeVersions = " < 18 || 20.0.* || 20.1.* || 20.2.* || 20.3.* ";
console.log(`Your Node.js version: ${nodeVersion}`);
const semver = require("semver");
@ -132,9 +132,9 @@ const twoFAVerifyOptions = {
const testMode = !!args["test"] || false;
// Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList, sendRemoteBrowserList } = require("./client");
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList, sendRemoteBrowserList, sendMonitorTypeList } = require("./client");
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
const { databaseSocketHandler } = require("./socket-handlers/database-socket-handler");
const { remoteBrowserSocketHandler } = require("./socket-handlers/remote-browser-socket-handler");
const TwoFA = require("./2fa");
const StatusPage = require("./model/status_page");
@ -246,6 +246,36 @@ let needSetup = false;
log.debug("test", request.body);
response.send("OK");
});
const fs = require("fs");
app.get("/_e2e/take-sqlite-snapshot", async (request, response) => {
await Database.close();
try {
fs.cpSync(Database.sqlitePath, `${Database.sqlitePath}.e2e-snapshot`);
} catch (err) {
throw new Error("Unable to copy SQLite DB.");
}
await Database.connect();
response.send("Snapshot taken.");
});
app.get("/_e2e/restore-sqlite-snapshot", async (request, response) => {
if (!fs.existsSync(`${Database.sqlitePath}.e2e-snapshot`)) {
throw new Error("Snapshot doesn't exist.");
}
await Database.close();
try {
fs.cpSync(`${Database.sqlitePath}.e2e-snapshot`, Database.sqlitePath);
} catch (err) {
throw new Error("Unable to copy snapshot file.");
}
await Database.connect();
response.send("Snapshot restored.");
});
}
// Robots.txt
@ -686,6 +716,8 @@ let needSetup = false;
monitor.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
monitor.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
monitor.conditions = JSON.stringify(monitor.conditions);
bean.import(monitor);
bean.user_id = socket.userID;
@ -695,13 +727,13 @@ let needSetup = false;
await updateMonitorNotification(bean.id, notificationIDList);
await server.sendMonitorList(socket);
await server.sendUpdateMonitorIntoList(socket, bean.id);
if (monitor.active !== false) {
await startMonitor(socket.userID, bean.id);
}
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
log.info("monitor", `Added Monitor: ${bean.id} User ID: ${socket.userID}`);
callback({
ok: true,
@ -826,11 +858,17 @@ let needSetup = false;
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
bean.cacheBust = monitor.cacheBust;
bean.kafkaProducerSsl = monitor.kafkaProducerSsl;
bean.kafkaProducerAllowAutoTopicCreation =
monitor.kafkaProducerAllowAutoTopicCreation;
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
bean.remote_browser = monitor.remote_browser;
bean.snmpVersion = monitor.snmpVersion;
bean.snmpOid = monitor.snmpOid;
bean.jsonPathOperator = monitor.jsonPathOperator;
bean.timeout = monitor.timeout;
bean.conditions = JSON.stringify(monitor.conditions);
bean.validate();
@ -842,11 +880,11 @@ let needSetup = false;
await updateMonitorNotification(bean.id, monitor.notificationIDList);
if (await bean.isActive()) {
if (await Monitor.isActive(bean.id, bean.active)) {
await restartMonitor(socket.userID, bean.id);
}
await server.sendMonitorList(socket);
await server.sendUpdateMonitorIntoList(socket, bean.id);
callback({
ok: true,
@ -886,14 +924,17 @@ let needSetup = false;
log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`);
let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [
let monitor = 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: await bean.toJSON(),
monitor: monitor.toJSON(preloadData),
});
} catch (e) {
@ -944,7 +985,7 @@ let needSetup = false;
try {
checkLogin(socket);
await startMonitor(socket.userID, monitorID);
await server.sendMonitorList(socket);
await server.sendUpdateMonitorIntoList(socket, monitorID);
callback({
ok: true,
@ -964,7 +1005,7 @@ let needSetup = false;
try {
checkLogin(socket);
await pauseMonitor(socket.userID, monitorID);
await server.sendMonitorList(socket);
await server.sendUpdateMonitorIntoList(socket, monitorID);
callback({
ok: true,
@ -1010,8 +1051,7 @@ let needSetup = false;
msg: "successDeleted",
msgi18n: true,
});
await server.sendMonitorList(socket);
await server.sendDeleteMonitorFromList(socket, monitorID);
} catch (e) {
callback({
@ -1636,17 +1676,18 @@ async function afterLogin(socket, user) {
sendDockerHostList(socket),
sendAPIKeyList(socket),
sendRemoteBrowserList(socket),
sendMonitorTypeList(socket),
]);
await StatusPage.sendStatusPageList(io, socket);
const monitorPromises = [];
for (let monitorID in monitorList) {
await sendHeartbeatList(socket, monitorID);
monitorPromises.push(sendHeartbeatList(socket, monitorID));
monitorPromises.push(Monitor.sendStats(io, monitorID, user.id));
}
for (let monitorID in monitorList) {
await Monitor.sendStats(io, monitorID, user.id);
}
await Promise.all(monitorPromises);
// Set server timezone from client browser if not set
// It should be run once only

@ -6,7 +6,7 @@ const Database = require("../database");
* @param {Socket} socket Socket.io instance
* @returns {void}
*/
module.exports = (socket) => {
module.exports.databaseSocketHandler = (socket) => {
// Post or edit incident
socket.on("getDatabaseSize", async (callback) => {

@ -29,8 +29,13 @@ function getGameList() {
return gameList;
}
/**
* Handler for general events
* @param {Socket} socket Socket.io instance
* @param {UptimeKumaServer} server Uptime Kuma server
* @returns {void}
*/
module.exports.generalSocketHandler = (socket, server) => {
socket.on("initServerTimezone", async (timezone) => {
try {
checkLogin(socket);

@ -543,7 +543,9 @@ class UptimeCalculator {
if (type === "minute" && num > 24 * 60) {
throw new Error("The maximum number of minutes is 1440");
}
if (type === "day" && num > 365) {
throw new Error("The maximum number of days is 365");
}
// Get the current time period key based on the type
let key = this.getKey(this.getCurrentDate(), type);
@ -741,20 +743,36 @@ class UptimeCalculator {
}
/**
* Get the uptime data by duration
* @param {'24h'|'30d'|'1y'} duration Only accept 24h, 30d, 1y
* Get the uptime data for given duration.
* @param {string} duration A string with a number and a unit (m,h,d,w,M,y), such as 24h, 30d, 1y.
* @returns {UptimeDataResult} UptimeDataResult
* @throws {Error} Invalid duration
* @throws {Error} Invalid duration / Unsupported unit
*/
getDataByDuration(duration) {
if (duration === "24h") {
return this.get24Hour();
} else if (duration === "30d") {
return this.get30Day();
} else if (duration === "1y") {
return this.get1Year();
} else {
throw new Error("Invalid duration");
const durationNumStr = duration.slice(0, -1);
if (!/^[0-9]+$/.test(durationNumStr)) {
throw new Error(`Invalid duration: ${duration}`);
}
const num = Number(durationNumStr);
const unit = duration.slice(-1);
switch (unit) {
case "m":
return this.getData(num, "minute");
case "h":
return this.getData(num, "hour");
case "d":
return this.getData(num, "day");
case "w":
return this.getData(7 * num, "day");
case "M":
return this.getData(30 * num, "day");
case "y":
return this.getData(365 * num, "day");
default:
throw new Error(`Unsupported unit (${unit}) for badge duration ${duration}`
);
}
}

@ -114,6 +114,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
UptimeKumaServer.monitorTypeList["group"] = new GroupMonitorType();
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
// Allow all CORS origins (polling) in development
@ -205,24 +206,56 @@ 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) {
let result = {};
async getMonitorJSONList(userID, monitorID = null) {
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
userID,
]);
let query = " user_id = ? ";
let queryParams = [ userID ];
for (let monitor of monitorList) {
result[monitor.id] = await monitor.toJSON();
if (monitorID) {
query += "AND id = ? ";
queryParams.push(monitorID);
}
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;
}
@ -519,4 +552,6 @@ const { TailscalePing } = require("./monitor-types/tailscale-ping");
const { DnsMonitorType } = require("./monitor-types/dns");
const { MqttMonitorType } = require("./monitor-types/mqtt");
const { GroupMonitorType } = require("./monitor-types/group");
const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb");
const Monitor = require("./model/monitor");

@ -11,7 +11,7 @@ const mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
const mysql = require("mysql2");
const { NtlmClient } = require("axios-ntlm");
const { NtlmClient } = require("./modules/axios-ntlm/lib/ntlmClient.js");
const { Settings } = require("./settings");
const grpc = require("@grpc/grpc-js");
const protojs = require("protobufjs");

@ -576,6 +576,12 @@ optgroup {
outline: none !important;
}
.prism-editor__container {
.important {
font-weight: var(--bs-body-font-weight) !important;
}
}
h5.settings-subheading::after {
content: "";
display: block;

@ -0,0 +1,152 @@
<template>
<div class="monitor-condition mb-3" data-testid="condition">
<button
v-if="!isInGroup || !isFirst || !isLast"
class="btn btn-outline-danger remove-button"
type="button"
:aria-label="$t('conditionDelete')"
data-testid="remove-condition"
@click="remove"
>
<font-awesome-icon icon="trash" />
</button>
<select v-if="!isFirst" v-model="model.andOr" class="form-select and-or-select" data-testid="condition-and-or">
<option value="and">{{ $t("and") }}</option>
<option value="or">{{ $t("or") }}</option>
</select>
<select v-model="model.variable" class="form-select" data-testid="condition-variable">
<option
v-for="variable in conditionVariables"
:key="variable.id"
:value="variable.id"
>
{{ $t(variable.id) }}
</option>
</select>
<select v-model="model.operator" class="form-select" data-testid="condition-operator">
<option
v-for="operator in getVariableOperators(model.variable)"
:key="operator.id"
:value="operator.id"
>
{{ $t(operator.caption) }}
</option>
</select>
<input
v-model="model.value"
type="text"
class="form-control"
:aria-label="$t('conditionValuePlaceholder')"
data-testid="condition-value"
required
/>
</div>
</template>
<script>
export default {
name: "EditMonitorCondition",
props: {
/**
* The monitor condition
*/
modelValue: {
type: Object,
required: true,
},
/**
* Whether this is the first condition
*/
isFirst: {
type: Boolean,
required: true,
},
/**
* Whether this is the last condition
*/
isLast: {
type: Boolean,
required: true,
},
/**
* Whether this condition is in a group
*/
isInGroup: {
type: Boolean,
required: false,
default: false,
},
/**
* Variable choices
*/
conditionVariables: {
type: Array,
required: true,
},
},
emits: [ "update:modelValue", "remove" ],
computed: {
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
}
},
methods: {
remove() {
this.$emit("remove", this.model);
},
getVariableOperators(variableId) {
return this.conditionVariables.find(v => v.id === variableId)?.operators ?? [];
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.monitor-condition {
display: flex;
flex-wrap: wrap;
}
.remove-button {
justify-self: flex-end;
margin-bottom: 12px;
margin-left: auto;
}
@container (min-width: 500px) {
.monitor-condition {
display: flex;
flex-wrap: nowrap;
}
.remove-button {
margin-bottom: 0;
margin-left: 10px;
order: 100;
}
.and-or-select {
width: auto;
}
}
</style>

@ -0,0 +1,189 @@
<template>
<div class="condition-group mb-3" data-testid="condition-group">
<div class="d-flex">
<select v-if="!isFirst" v-model="model.andOr" class="form-select" style="width: auto;" data-testid="condition-group-and-or">
<option value="and">{{ $t("and") }}</option>
<option value="or">{{ $t("or") }}</option>
</select>
</div>
<div class="condition-group-inner mt-2 pa-2">
<div class="condition-group-conditions">
<template v-for="(child, childIndex) in model.children" :key="childIndex">
<EditMonitorConditionGroup
v-if="child.type === 'group'"
v-model="model.children[childIndex]"
:is-first="childIndex === 0"
:get-new-group="getNewGroup"
:get-new-condition="getNewCondition"
:condition-variables="conditionVariables"
@remove="removeChild"
/>
<EditMonitorCondition
v-else
v-model="model.children[childIndex]"
:is-first="childIndex === 0"
:is-last="childIndex === model.children.length - 1"
:is-in-group="true"
:condition-variables="conditionVariables"
@remove="removeChild"
/>
</template>
</div>
<div class="condition-group-actions mt-3">
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-condition-button" @click="addCondition">
{{ $t("conditionAdd") }}
</button>
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-group-button" @click="addGroup">
{{ $t("conditionAddGroup") }}
</button>
<button
class="btn btn-outline-danger"
type="button"
:aria-label="$t('conditionDeleteGroup')"
data-testid="remove-condition-group"
@click="remove"
>
<font-awesome-icon icon="trash" />
</button>
</div>
</div>
</div>
</template>
<script>
import EditMonitorCondition from "./EditMonitorCondition.vue";
export default {
name: "EditMonitorConditionGroup",
components: {
EditMonitorCondition,
},
props: {
/**
* The condition group
*/
modelValue: {
type: Object,
required: true,
},
/**
* Whether this is the first condition
*/
isFirst: {
type: Boolean,
required: true,
},
/**
* Function to generate a new group model
*/
getNewGroup: {
type: Function,
required: true,
},
/**
* Function to generate a new condition model
*/
getNewCondition: {
type: Function,
required: true,
},
/**
* Variable choices
*/
conditionVariables: {
type: Array,
required: true,
},
},
emits: [ "update:modelValue", "remove" ],
computed: {
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
}
},
methods: {
addGroup() {
const conditions = [ ...this.model.children ];
conditions.push(this.getNewGroup());
this.model.children = conditions;
},
addCondition() {
const conditions = [ ...this.model.children ];
conditions.push(this.getNewCondition());
this.model.children = conditions;
},
remove() {
this.$emit("remove", this.model);
},
removeChild(child) {
const idx = this.model.children.indexOf(child);
if (idx !== -1) {
this.model.children.splice(idx, 1);
}
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.condition-group-inner {
background: rgba(0, 0, 0, 0.05);
padding: 20px;
}
.dark .condition-group-inner {
background: rgba(255, 255, 255, 0.05);
}
.condition-group-conditions {
container-type: inline-size;
}
.condition-group-actions {
display: grid;
gap: 10px;
}
// Delete button
.condition-group-actions > :last-child {
margin-left: auto;
margin-top: 14px;
}
@container (min-width: 400px) {
.condition-group-actions {
display: flex;
}
// Delete button
.condition-group-actions > :last-child {
margin-left: auto;
margin-top: 0;
}
.btn-delete-group {
margin-left: auto;
}
}
</style>

@ -0,0 +1,149 @@
<template>
<div class="monitor-conditions">
<label class="form-label">{{ $t("Conditions") }}</label>
<div class="monitor-conditions-conditions">
<template v-for="(condition, conditionIndex) in model" :key="conditionIndex">
<EditMonitorConditionGroup
v-if="condition.type === 'group'"
v-model="model[conditionIndex]"
:is-first="conditionIndex === 0"
:get-new-group="getNewGroup"
:get-new-condition="getNewCondition"
:condition-variables="conditionVariables"
@remove="removeCondition"
/>
<EditMonitorCondition
v-else
v-model="model[conditionIndex]"
:is-first="conditionIndex === 0"
:is-last="conditionIndex === model.length - 1"
:condition-variables="conditionVariables"
@remove="removeCondition"
/>
</template>
</div>
<div class="monitor-conditions-buttons">
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-condition-button" @click="addCondition">
{{ $t("conditionAdd") }}
</button>
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-group-button" @click="addGroup">
{{ $t("conditionAddGroup") }}
</button>
</div>
</div>
</template>
<script>
import EditMonitorConditionGroup from "./EditMonitorConditionGroup.vue";
import EditMonitorCondition from "./EditMonitorCondition.vue";
export default {
name: "EditMonitorConditions",
components: {
EditMonitorConditionGroup,
EditMonitorCondition,
},
props: {
/**
* The monitor conditions
*/
modelValue: {
type: Array,
required: true,
},
conditionVariables: {
type: Array,
required: true,
},
},
emits: [ "update:modelValue" ],
computed: {
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
}
},
created() {
if (this.model.length === 0) {
this.addCondition();
}
},
methods: {
getNewGroup() {
return {
type: "group",
children: [ this.getNewCondition() ],
andOr: "and",
};
},
getNewCondition() {
const firstVariable = this.conditionVariables[0]?.id || null;
const firstOperator = this.getVariableOperators(firstVariable)[0] || null;
return {
type: "expression",
variable: firstVariable,
operator: firstOperator?.id || null,
value: "",
andOr: "and",
};
},
addGroup() {
const conditions = [ ...this.model ];
conditions.push(this.getNewGroup());
this.$emit("update:modelValue", conditions);
},
addCondition() {
const conditions = [ ...this.model ];
conditions.push(this.getNewCondition());
this.$emit("update:modelValue", conditions);
},
removeCondition(condition) {
const conditions = [ ...this.model ];
const idx = conditions.indexOf(condition);
if (idx !== -1) {
conditions.splice(idx, 1);
this.$emit("update:modelValue", conditions);
}
},
getVariableOperators(variableId) {
return this.conditionVariables.find(v => v.id === variableId)?.operators ?? [];
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.monitor-conditions,
.monitor-conditions-conditions {
container-type: inline-size;
}
.monitor-conditions-buttons {
display: grid;
gap: 10px;
}
@container (min-width: 400px) {
.monitor-conditions-buttons {
display: flex;
}
}
</style>

@ -14,7 +14,7 @@
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
>
<div>{{ timeSinceFirstBeat }} ago</div>
<div>{{ timeSinceFirstBeat }}</div>
<div v-if="$root.styleElapsedTime === 'with-line'" class="connecting-line"></div>
<div>{{ timeSinceLastBeat }}</div>
</div>
@ -184,11 +184,11 @@ export default {
}
if (seconds < tolerance) {
return "now";
return this.$t("now");
} else if (seconds < 60 * 60) {
return (seconds / 60).toFixed(0) + "m ago";
return this.$t("time ago", [ (seconds / 60).toFixed(0) + "m" ]);
} else {
return (seconds / 60 / 60).toFixed(0) + "h ago";
return this.$t("time ago", [ (seconds / 60 / 60).toFixed(0) + "h" ]);
}
}
},

@ -45,7 +45,7 @@
</span>
</div>
</div>
<div ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle">
<div ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle" data-testid="monitor-list">
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
</div>

@ -43,12 +43,15 @@
<div v-if="!isCollapsed" class="childs">
<MonitorListItem
v-for="(item, index) in sortedChildMonitorList"
:key="index" :monitor="item"
:key="index"
:monitor="item"
:isSelectMode="isSelectMode"
:isSelected="isSelected"
:select="select"
:deselect="deselect"
:depth="depth + 1"
:filter-func="filterFunc"
:sort-func="sortFunc"
/>
</div>
</transition>

@ -118,6 +118,7 @@ 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",
@ -135,6 +136,7 @@ export default {
"ntfy": "Ntfy",
"octopush": "Octopush",
"OneBot": "OneBot",
"Onesender": "Onesender",
"Opsgenie": "Opsgenie",
"PagerDuty": "PagerDuty",
"PagerTree": "PagerTree",
@ -144,6 +146,7 @@ export default {
"pushy": "Pushy",
"rocket.chat": "Rocket.Chat",
"signal": "Signal",
"SIGNL4": "SIGNL4",
"slack": "Slack",
"squadcast": "SquadCast",
"SMSEagle": "SMSEagle",
@ -152,6 +155,7 @@ export default {
"stackfield": "Stackfield",
"teams": "Microsoft Teams",
"telegram": "Telegram",
"threema": "Threema",
"twilio": "Twilio",
"Splunk": "Splunk",
"webhook": "Webhook",
@ -177,6 +181,7 @@ export default {
"WeCom": "WeCom (企业微信群机器人)",
"ServerChan": "ServerChan (Server酱)",
"smsc": "SMSC",
"WPush": "WPush(wpush.cn)",
};
// Sort by notification name

@ -7,12 +7,12 @@
:animation="100"
>
<template #item="group">
<div class="mb-5 ">
<div class="mb-5" data-testid="group">
<!-- Group Title -->
<h2 class="group-title">
<font-awesome-icon v-if="editMode && showGroupDrag" icon="arrows-alt-v" class="action drag me-3" />
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeGroup(group.index)" />
<Editable v-model="group.element.name" :contenteditable="editMode" tag="span" />
<Editable v-model="group.element.name" :contenteditable="editMode" tag="span" data-testid="group-name" />
</h2>
<div class="shadow-box monitor-list mt-4 position-relative">
@ -31,9 +31,9 @@
item-key="id"
>
<template #item="monitor">
<div class="item">
<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)" />
@ -45,10 +45,11 @@
class="item-name"
target="_blank"
rel="noopener noreferrer"
data-testid="monitor-name"
>
{{ monitor.element.name }}
</a>
<p v-else class="item-name"> {{ monitor.element.name }} </p>
<p v-else class="item-name" data-testid="monitor-name"> {{ monitor.element.name }} </p>
<span
title="Setting"
@ -66,11 +67,11 @@
<Tag :item="{name: $t('Cert Exp.'), value: formattedCertExpiryMessage(monitor), color: certExpiryColor(monitor)}" :size="'sm'" />
</div>
<div v-if="showTags">
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" data-testid="monitor-tag" />
</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>

@ -14,6 +14,7 @@
type="button"
class="btn btn-outline-secondary btn-add"
:disabled="processing"
data-testid="add-tag-button"
@click.stop="showAddDialog"
>
<font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }}
@ -59,6 +60,7 @@
v-model="newDraftTag.name" class="form-control"
:class="{'is-invalid': validateDraftTag.nameInvalid}"
:placeholder="$t('Name')"
data-testid="tag-name-input"
@keydown.enter.prevent="onEnter"
/>
<div class="invalid-feedback">
@ -76,6 +78,7 @@
label="name"
select-label=""
deselect-label=""
data-testid="tag-color-select"
>
<template #option="{ option }">
<div
@ -103,6 +106,7 @@
v-model="newDraftTag.value" class="form-control"
:class="{'is-invalid': validateDraftTag.valueInvalid}"
:placeholder="$t('value (optional)')"
data-testid="tag-value-input"
@keydown.enter.prevent="onEnter"
/>
<div class="invalid-feedback">
@ -114,6 +118,7 @@
type="button"
class="btn btn-secondary float-end"
:disabled="processing || validateDraftTag.invalid"
data-testid="tag-submit-button"
@click.stop="addDraftTag"
>
{{ $t("Add") }}

@ -0,0 +1,48 @@
<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>

@ -0,0 +1,81 @@
<template>
<div class="mb-3">
<label for="host-onesender" class="form-label">{{ $t("Host Onesender") }}</label>
<input
id="host-onesender"
v-model="$parent.notification.onesenderURL"
type="url"
placeholder="https://xxxxxxxxxxx.com/api/v1/messages"
pattern="https?://.+"
class="form-control"
required
/>
</div>
<div class="mb-3">
<label for="receiver-onesender" class="form-label">{{ $t("Token Onesender") }}</label>
<HiddenInput id="receiver-onesender" v-model="$parent.notification.onesenderToken" :required="true" autocomplete="false"></HiddenInput>
<i18n-t tag="div" keypath="wayToGetOnesenderUrlandToken" class="form-text">
<a href="https://onesender.net/" target="_blank">{{ $t("here") }}</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="webhook-request-body" class="form-label">{{ $t("Recipient Type") }}</label>
<select
id="webhook-request-body"
v-model="$parent.notification.onesenderTypeReceiver"
class="form-select"
required
>
<option value="private">{{ $t("Private Number") }}</option>
<option value="group">{{ $t("Group ID") }}</option>
</select>
</div>
<div v-if="$parent.notification.onesenderTypeReceiver == 'private'" class="form-text">{{ $t("privateOnesenderDesc", ['"application/json"']) }}</div>
<div v-else class="form-text">{{ $t("groupOnesenderDesc") }}</div>
<div class="mb-3">
<input
id="type-receiver-onesender"
v-model="$parent.notification.onesenderReceiver"
type="text"
placeholder="628123456789 or 628123456789-34534"
class="form-control"
required
/>
</div>
<div class="mb-3">
<input
id="type-receiver-onesender"
v-model="computedReceiverResult"
type="text"
class="form-control"
disabled
/>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
data() {
return {};
},
computed: {
computedReceiverResult() {
let receiver = this.$parent.notification.onesenderReceiver;
return this.$parent.notification.onesenderTypeReceiver === "private" ? receiver + "@s.whatsapp.net" : receiver + "@g.us";
},
},
};
</script>
<style lang="scss" scoped>
textarea {
min-height: 200px;
}
</style>

@ -0,0 +1,16 @@
<template>
<div class="mb-3">
<label for="signl4-webhook-url" class="form-label">{{ $t("SIGNL4 Webhook URL") }}</label>
<input
id="signl4-webhook-url"
v-model="$parent.notification.webhookURL"
type="url"
pattern="https?://.+"
class="form-control"
required
/>
<i18n-t tag="div" keypath="signl4Docs" class="form-text">
<a href="https://docs.signl4.com/integrations/uptime-kuma/uptime-kuma.html" target="_blank">SIGNL4 Docs</a>
</i18n-t>
</div>
</template>

@ -3,7 +3,7 @@
<label for="smspartner-key" class="form-label">{{ $t("API Key") }}</label>
<HiddenInput id="smspartner-key" v-model="$parent.notification.smspartnerApikey" :required="true" autocomplete="new-password"></HiddenInput>
<div class="form-text">
<i18n-t keypath="smspartnerApiurl" as="div" class="form-text">
<i18n-t keypath="smspartnerApiurl" tag="div" class="form-text">
<a href="https://my.smspartner.fr/dashboard/api" target="_blank">my.smspartner.fr/dashboard/api</a>
</i18n-t>
</div>
@ -12,7 +12,7 @@
<label for="smspartner-phone-number" class="form-label">{{ $t("smspartnerPhoneNumber") }}</label>
<input id="smspartner-phone-number" v-model="$parent.notification.smspartnerPhoneNumber" type="text" minlength="3" maxlength="20" pattern="^[\d+,]+$" class="form-control" required>
<div class="form-text">
<i18n-t keypath="smspartnerPhoneNumberHelptext" as="div" class="form-text">
<i18n-t keypath="smspartnerPhoneNumberHelptext" tag="div" class="form-text">
<code>+336xxxxxxxx</code>
<code>+496xxxxxxxx</code>
<code>,</code>

@ -9,6 +9,12 @@
<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;">

@ -4,6 +4,53 @@
<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>
@ -16,5 +63,19 @@ 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>

@ -0,0 +1,87 @@
<template>
<div class="mb-3">
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipientType") }}</label>
<select
id="threema-recipient" v-model="$parent.notification.threemaRecipientType" required
class="form-select"
>
<option value="identity">{{ $t("threemaRecipientTypeIdentity") }}</option>
<option value="phone">{{ $t("threemaRecipientTypePhone") }}</option>
<option value="email">{{ $t("threemaRecipientTypeEmail") }}</option>
</select>
</div>
<div v-if="$parent.notification.threemaRecipientType === 'identity'" class="mb-3">
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipient") }} {{ $t("threemaRecipientTypeIdentity") }}</label>
<input
id="threema-recipient"
v-model="$parent.notification.threemaRecipient"
class="form-control"
minlength="8"
maxlength="8"
pattern="[A-Z0-9]{8}"
required
type="text"
>
<div class="form-text">
<p>{{ $t("threemaRecipientTypeIdentityFormat") }}</p>
</div>
</div>
<div v-else-if="$parent.notification.threemaRecipientType === 'phone'" class="mb-3">
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipient") }} {{ $t("threemaRecipientTypePhone") }}</label>
<input
id="threema-recipient"
v-model="$parent.notification.threemaRecipient"
class="form-control"
maxlength="15"
pattern="\d{1,15}"
required
type="text"
>
<div class="form-text">
<p>{{ $t("threemaRecipientTypePhoneFormat") }}</p>
</div>
</div>
<div v-else-if="$parent.notification.threemaRecipientType === 'email'" class="mb-3">
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipient") }} {{ $t("threemaRecipientTypeEmail") }}</label>
<input
id="threema-recipient"
v-model="$parent.notification.threemaRecipient"
class="form-control"
maxlength="254"
required
type="email"
>
</div>
<div class="mb-3">
<label class="form-label" for="threema-sender">{{ $t("threemaSenderIdentity") }}</label>
<input
id="threema-sender"
v-model="$parent.notification.threemaSenderIdentity"
class="form-control"
minlength="8"
maxlength="8"
pattern="^\*[A-Z0-9]{7}$"
required
type="text"
>
<div class="form-text">
<p>{{ $t("threemaSenderIdentityFormat") }}</p>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="threema-secret">{{ $t("threemaApiAuthenticationSecret") }}</label>
<HiddenInput
id="threema-secret" v-model="$parent.notification.threemaSecret" required
autocomplete="false"
></HiddenInput>
</div>
<i18n-t class="form-text" keypath="wayToGetThreemaGateway" tag="div">
<a href="https://threema.ch/en/gateway" target="_blank">{{ $t("here") }}</a>
</i18n-t>
<i18n-t class="form-text" keypath="threemaBasicModeInfo" tag="div">
<a href="https://gateway.threema.ch/en/developer/api" target="_blank">{{ $t("here") }}</a>
</i18n-t>
</template>
<script lang="ts" setup>
import HiddenInput from "../HiddenInput.vue";
</script>

@ -0,0 +1,31 @@
<template>
<div class="mb-3">
<label for="wpush-apikey" class="form-label">WPush {{ $t("API Key") }}</label>
<HiddenInput id="wpush-apikey" v-model="$parent.notification.wpushAPIkey" :required="true" autocomplete="new-password" placeholder="WPushxxxxx"></HiddenInput>
</div>
<div class="mb-3">
<label for="wpush-channel" class="form-label">发送通道</label>
<select id="wpush-channel" v-model="$parent.notification.wpushChannel" class="form-select" required>
<option value="wechat">微信</option>
<option value="sms">短信</option>
<option value="mail">邮件</option>
<option value="feishu">飞书</option>
<option value="dingtalk">钉钉</option>
<option value="wechat_work">企业微信</option>
</select>
</div>
<i18n-t tag="p" keypath="More info on:">
<a href="https://wpush.cn/" rel="noopener noreferrer" target="_blank">https://wpush.cn/</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

@ -9,6 +9,7 @@ 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";
@ -29,6 +30,7 @@ import Nostr from "./Nostr.vue";
import Ntfy from "./Ntfy.vue";
import Octopush from "./Octopush.vue";
import OneBot from "./OneBot.vue";
import Onesender from "./Onesender.vue";
import Opsgenie from "./Opsgenie.vue";
import PagerDuty from "./PagerDuty.vue";
import FlashDuty from "./FlashDuty.vue";
@ -52,6 +54,7 @@ import STMP from "./SMTP.vue";
import Teams from "./Teams.vue";
import TechulusPush from "./TechulusPush.vue";
import Telegram from "./Telegram.vue";
import Threema from "./Threema.vue";
import Twilio from "./Twilio.vue";
import Webhook from "./Webhook.vue";
import WeCom from "./WeCom.vue";
@ -61,6 +64,8 @@ import Splunk from "./Splunk.vue";
import SevenIO from "./SevenIO.vue";
import Whapi from "./Whapi.vue";
import Cellsynt from "./Cellsynt.vue";
import WPush from "./WPush.vue";
import SIGNL4 from "./SIGNL4.vue";
/**
* Manage all notification form.
@ -78,6 +83,7 @@ const NotificationFormList = {
"smsc": SMSC,
"DingDing": DingDing,
"discord": Discord,
"Elks": Elks,
"Feishu": Feishu,
"FreeMobile": FreeMobile,
"GoogleChat": GoogleChat,
@ -97,6 +103,7 @@ const NotificationFormList = {
"ntfy": Ntfy,
"octopush": Octopush,
"OneBot": OneBot,
"Onesender": Onesender,
"Opsgenie": Opsgenie,
"PagerDuty": PagerDuty,
"FlashDuty": FlashDuty,
@ -110,6 +117,7 @@ const NotificationFormList = {
"rocket.chat": RocketChat,
"serwersms": SerwerSMS,
"signal": Signal,
"SIGNL4": SIGNL4,
"SMSManager": SMSManager,
"SMSPartner": SMSPartner,
"slack": Slack,
@ -119,6 +127,7 @@ const NotificationFormList = {
"stackfield": Stackfield,
"teams": Teams,
"telegram": Telegram,
"threema": Threema,
"twilio": Twilio,
"Splunk": Splunk,
"webhook": Webhook,
@ -130,6 +139,7 @@ const NotificationFormList = {
"whapi": Whapi,
"gtxmessaging": GtxMessaging,
"Cellsynt": Cellsynt,
"WPush": WPush
};
export default NotificationFormList;

@ -1,53 +1,63 @@
<template>
<div>
<div class="add-btn">
<button class="btn btn-primary me-2" type="button" @click="$refs.apiKeyDialog.show()">
<font-awesome-icon icon="plus" /> {{ $t("Add API Key") }}
</button>
<div
v-if="settings.disableAuth"
class="mt-5 d-flex align-items-center justify-content-center my-3"
>
{{ $t("apiKeysDisabledMsg") }}
</div>
<div v-else>
<div class="add-btn">
<button class="btn btn-primary me-2" type="button" @click="$refs.apiKeyDialog.show()">
<font-awesome-icon icon="plus" /> {{ $t("Add API Key") }}
</button>
</div>
<div>
<span v-if="Object.keys(keyList).length === 0" class="d-flex align-items-center justify-content-center my-3">
{{ $t("No API Keys") }}
</span>
<div
v-for="(item, index) in keyList"
:key="index"
class="item"
:class="item.status"
>
<div class="left-part">
<div
class="circle"
></div>
<div class="info">
<div class="title">{{ item.name }}</div>
<div class="status">
{{ $t("apiKey-" + item.status) }}
</div>
<div class="date">
{{ $t("Created") }}: {{ item.createdDate }}
</div>
<div class="date">
{{ $t("Expires") }}: {{ item.expires || $t("Never") }}
<div>
<span
v-if="Object.keys(keyList).length === 0"
class="d-flex align-items-center justify-content-center my-3"
>
{{ $t("No API Keys") }}
</span>
<div
v-for="(item, index) in keyList"
:key="index"
class="item"
:class="item.status"
>
<div class="left-part">
<div class="circle"></div>
<div class="info">
<div class="title">{{ item.name }}</div>
<div class="status">
{{ $t("apiKey-" + item.status) }}
</div>
<div class="date">
{{ $t("Created") }}: {{ item.createdDate }}
</div>
<div class="date">
{{ $t("Expires") }}:
{{ item.expires || $t("Never") }}
</div>
</div>
</div>
</div>
<div class="buttons">
<div class="btn-group" role="group">
<button v-if="item.active" class="btn btn-normal" @click="disableDialog(item.id)">
<font-awesome-icon icon="pause" /> {{ $t("Disable") }}
</button>
<div class="buttons">
<div class="btn-group" role="group">
<button v-if="item.active" class="btn btn-normal" @click="disableDialog(item.id)">
<font-awesome-icon icon="pause" /> {{ $t("Disable") }}
</button>
<button v-if="!item.active" class="btn btn-primary" @click="enableKey(item.id)">
<font-awesome-icon icon="play" /> {{ $t("Enable") }}
</button>
<button v-if="!item.active" class="btn btn-primary" @click="enableKey(item.id)">
<font-awesome-icon icon="play" /> {{ $t("Enable") }}
</button>
<button class="btn btn-danger" @click="deleteDialog(item.id)">
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
</button>
<button class="btn btn-danger" @click="deleteDialog(item.id)">
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
</button>
</div>
</div>
</div>
</div>
@ -88,6 +98,9 @@ export default {
let result = Object.values(this.$root.apiKeyList);
return result;
},
settings() {
return this.$parent.$parent.$parent.settings;
},
},
methods: {
@ -126,9 +139,11 @@ export default {
* @returns {void}
*/
disableKey() {
this.$root.getSocket().emit("disableAPIKey", this.selectedKeyID, (res) => {
this.$root.toastRes(res);
});
this.$root
.getSocket()
.emit("disableAPIKey", this.selectedKeyID, (res) => {
this.$root.toastRes(res);
});
},
/**
@ -146,113 +161,113 @@ export default {
</script>
<style lang="scss" scoped>
@import "../../assets/vars.scss";
@import "../../assets/vars.scss";
.mobile {
.item {
flex-direction: column;
align-items: flex-start;
margin-bottom: 20px;
}
.mobile {
.item {
flex-direction: column;
align-items: flex-start;
margin-bottom: 20px;
}
.add-btn {
padding-top: 20px;
padding-bottom: 20px;
}
.add-btn {
padding-top: 20px;
padding-bottom: 20px;
}
.item {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
border-radius: 10px;
transition: all ease-in-out 0.15s;
justify-content: space-between;
padding: 10px;
min-height: 90px;
margin-bottom: 5px;
&:hover {
background-color: $highlight-white;
}
.item {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
border-radius: 10px;
transition: all ease-in-out 0.15s;
justify-content: space-between;
padding: 10px;
min-height: 90px;
margin-bottom: 5px;
&.active {
.circle {
background-color: $primary;
}
}
&:hover {
background-color: $highlight-white;
&.inactive {
.circle {
background-color: $danger;
}
}
&.active {
.circle {
background-color: $primary;
}
&.expired {
.left-part {
opacity: 0.3;
}
&.inactive {
.circle {
background-color: $danger;
}
.circle {
background-color: $dark-font-color;
}
}
&.expired {
.left-part {
opacity: 0.3;
}
.left-part {
display: flex;
gap: 12px;
align-items: center;
.circle {
background-color: $dark-font-color;
}
.circle {
width: 25px;
height: 25px;
border-radius: 50rem;
}
.left-part {
display: flex;
gap: 12px;
align-items: center;
.circle {
width: 25px;
height: 25px;
border-radius: 50rem;
.info {
.title {
font-weight: bold;
font-size: 20px;
}
.info {
.title {
font-weight: bold;
font-size: 20px;
}
.status {
font-size: 14px;
}
.status {
font-size: 14px;
}
}
}
.buttons {
display: flex;
gap: 8px;
flex-direction: row-reverse;
.buttons {
display: flex;
gap: 8px;
flex-direction: row-reverse;
.btn-group {
width: 310px;
}
.btn-group {
width: 310px;
}
}
.date {
margin-top: 5px;
display: block;
font-size: 14px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 20px;
padding: 0 10px;
width: fit-content;
.dark & {
color: white;
background-color: rgba(255, 255, 255, 0.1);
}
}
.date {
margin-top: 5px;
display: block;
font-size: 14px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 20px;
padding: 0 10px;
width: fit-content;
.dark & {
color: white;
background-color: rgba(255, 255, 255, 0.1);
}
}
.dark {
.item {
&:hover {
background-color: $dark-bg2;
}
.dark {
.item {
&:hover {
background-color: $dark-bg2;
}
}
}
</style>

@ -32,7 +32,14 @@
<button class="btn btn-outline-info me-2" @click="shrinkDatabase">
{{ $t("Shrink Database") }} ({{ databaseSizeDisplay }})
</button>
<div class="form-text mt-2 mb-4 ms-2">{{ $t("shrinkDatabaseDescription") }}</div>
<i18n-t tag="div" keypath="shrinkDatabaseDescriptionSqlite" class="form-text mt-2 mb-4 ms-2">
<template #vacuum>
<code>VACUUM</code>
</template>
<template #auto_vacuum>
<code>AUTO_VACUUM</code>
</template>
</i18n-t>
</div>
<button
id="clearAllStats-btn"

@ -6,6 +6,7 @@ const languageList = {
"cs-CZ": "Čeština",
"zh-HK": "繁體中文 (香港)",
"bg-BG": "Български",
"be": "Беларуская",
"de-DE": "Deutsch (Deutschland)",
"de-CH": "Deutsch (Schweiz)",
"nl-NL": "Nederlands",

@ -388,7 +388,6 @@
"Discard": "تجاهل",
"Cancel": "يلغي",
"Powered by": "مشغل بواسطة",
"shrinkDatabaseDescription": "تشغيل فراغ قاعدة البيانات لـ SQLite. إذا تم إنشاء قاعدة البيانات الخاصة بك بعد تمكين 1.10.0 AUTO_VACUUM بالفعل ولا يلزم هذا الإجراء.",
"serwersms": "Serwersms.pl",
"serwersmsAPIUser": "اسم مستخدم API (بما في ذلك بادئة WebAPI_)",
"serwersmsAPIPassword": "كلمة مرور API",

@ -278,7 +278,6 @@
"Discard": "تجاهل",
"Cancel": "يلغي",
"Powered by": "مشغل بواسطة",
"shrinkDatabaseDescription": "تشغيل فراغ قاعدة البيانات لـ SQLite. إذا تم إنشاء قاعدة البيانات الخاصة بك بعد تمكين 1.10.0 AUTO_VACUUM بالفعل ولا يلزم هذا الإجراء.",
"Customize": "يعدل أو يكيف",
"Custom Footer": "تذييل مخصص",
"Custom CSS": "لغة تنسيق ويب حسب الطلب",

@ -0,0 +1,937 @@
{
"Edit": "Змяніць",
"-hour": "-гадзін",
"ignoreTLSErrorGeneral": "Ігнараваць памылку TLS/SSL для злучэння",
"pushOthers": "Іншыя",
"Yes": "Так",
"Show URI": "Паказаць URI",
"Tags": "Тэгі",
"Tag with this value already exist.": "Тэг з такім значэннем ужо існуе.",
"color": "Колер",
"value (optional)": "значэнне (неабавязкова)",
"Gray": "Шэры",
"Red": "Чырвоны",
"Orange": "Аранжавы",
"Search monitored sites": "Пошук адсочваемых сайтаў",
"Avg. Ping": "Сярэдні пінг",
"Body": "Цела",
"Headers": "Загалоўкі",
"Create Incident": "Стварыць інцыдэнт",
"Style": "Стыль",
"Proxies": "Проксі",
"default": "Па змаўчанні",
"enabled": "Уключана",
"setAsDefault": "Усталяваць па змаўчанні",
"Remove the expiry notification": "Выдаліць дату сканчэньня тэрміну дзеяння абвесткі",
"Refresh Interval": "Інтэрвал абнаўлення",
"Refresh Interval Description": "Старонка статусу будзе цалкам абнаўляць сайт кожныя {0} секунд",
"maintenanceStatus-ended": "Скончыўся(ліся)",
"Select message type": "Выберыце тып паведамлення",
"Create new forum post": "Стварыць новы пост",
"postToExistingThread": "Стварыць пост у гэтай галіне",
"forumPostName": "Назва паста",
"e.g. {discordThreadID}": "Напр. {discordThreadID}",
"Number": "Нумар",
"lineDevConsoleTo": "Кансоль распрацошчыкаў Line - {0}",
"recurringIntervalMessage": "Запускаць 1 раз кожны дзень | Запускаць 1 раз кожныя {0} дзён",
"affectedMonitorsDescription": "Выберыце маніторы, якія будуць затронутыя падчас тэхабслугоўвання",
"pushoversounds gamelan": "Гамелан",
"pushoversounds incoming": "Уваходны",
"pushoversounds climb": "Падым (доўгі)",
"wayToGetKookBotToken": "Стварыце праграму і атрымайце токен бота па адрасу {0}",
"Device": "Прылада",
"Huawei": "Huawei",
"Expiry date": "Дата сканчэння",
"Don't expire": "Не сканчаецца",
"Badge URL": "URL значка",
"nostrRelays": "Рэле Nostr",
"gamedigGuessPort": "Gamedig: Угадай порт",
"GrafanaOncallUrl": "URL-адрас Grafana Oncall",
"API URL": "API URL-адрас",
"Originator type": "Тып крыніцы",
"Destination": "Прызначэнне",
"languageName": "Беларуская",
"setupDatabaseChooseDatabase": "Якую базу даных вы хацелі б выкарыстоўваць?",
"setupDatabaseEmbeddedMariaDB": "Вам не трэба нічога наладжваць. У гэты Docker-вобраз аўтаматычна ўбудавана і наладжана MariaDB. Uptime Kuma будзе падключацца да гэтай базы даных праз unix-socket.",
"setupDatabaseMariaDB": "Падключыцца да знешняй базы даных MariaDB. Вам трэба задаць інфармацыю аб падлучэнні да базы даных.",
"setupDatabaseSQLite": "Просты файл базы даных, рэкамендуецца для невялікіх разгортванняў. Да версіі 2.0.0 Uptime Kuma выкарыстоўваў SQLite як базу даных па змаўчанні.",
"settingUpDatabaseMSG": "Настраиваем базу даных. Гэта можа заняць некаторы час, калі ласка, пачакайце.",
"dbName": "Назва базы даных",
"Settings": "Налады",
"Dashboard": "Панэль кіравання",
"Help": "Дапамога",
"New Update": "Даступна абнаўленне",
"Language": "Мова",
"Appearance": "Знешні выгляд",
"Theme": "Тэма",
"General": "Агульныя",
"Game": "Гульня",
"Primary Base URL": "Асноўны URL",
"Version": "Версія",
"Check Update On GitHub": "Праверыць абнаўленні ў GitHub",
"List": "Спіс",
"Home": "Галоўная",
"Add": "Дадаць",
"Add New Monitor": "Дадаць новы манітор",
"Quick Stats": "Статыстыка",
"Up": "Працуе",
"Down": "Не працуе",
"Pending": "У чаканні",
"statusMaintenance": "Тэхабслугоўванне",
"Maintenance": "Тэхабслугоўванне",
"Unknown": "Невядома",
"Cannot connect to the socket server": "Немагчыма падключыцца да сервера",
"Reconnecting...": "Падключэнне...",
"General Monitor Type": "Агульны Тып Манітора",
"Passive Monitor Type": "Пасіўны Тып Манітора",
"Specific Monitor Type": "Спецыфічны Тып Манітора",
"markdownSupported": "Падтрымліваецца сінтаксіс Markdown",
"pauseDashboardHome": "Паўза",
"Pause": "Паўза",
"Name": "Назва",
"Status": "Статус",
"DateTime": "Дата і час",
"Message": "Паведамленне",
"No important events": "Няма важных падзей",
"Resume": "Узнавіць",
"Delete": "Выдаліць",
"Current": "Бягучы",
"Uptime": "Час працы",
"Cert Exp.": "Сертыфікат сконч.",
"Monitor": "Манітор | Маніторы",
"day": "дзень | дзён",
"-day": "-дзён",
"hour": "гадзіна",
"Response": "Адказ",
"Ping": "Пінг",
"Monitor Type": "Тып манітора",
"Keyword": "Ключавое слова",
"Invert Keyword": "Інвертаваць ключавое слова",
"Friendly Name": "Назва",
"URL": "URL-спасылка",
"Hostname": "Адрас хоста",
"Expected Value": "Чаканае значэнне",
"Json Query": "JSON Запыт",
"Host URL": "URL Хоста",
"locally configured mail transfer agent": "Наладжаны лакальна агент перадачы паштовых паведамленняў",
"Port": "Порт",
"Heartbeat Interval": "Частата апытання",
"Request Timeout": "Тайм-Аут запыту",
"timeoutAfter": "Тайм-Аут праз {0} секундаў",
"Retries": "Спробы",
"Heartbeat Retry Interval": "Інтэрвал паўтору апытання",
"Resend Notification if Down X times consecutively": "Паўторная адпраўка абвесткі пры адключэнні некалькі раз",
"Advanced": "Дадаткова",
"checkEverySecond": "Праверка кожныя {0} секунд",
"retryCheckEverySecond": "Паўтараць кожныя {0} секунд",
"resendEveryXTimes": "Перасылаць кожныя {0} раз",
"resendDisabled": "Перасылка адключана",
"retriesDescription": "Максімальная колькасць спробаў перад адзнакай службы, як недаступная, і адпраўкай абвесткі",
"ignoreTLSError": "Ігнараваць памылкі TLS/SSL для HTTPS сайтаў",
"upsideDownModeDescription": "Змяніць статус службы на ПРАЦУЕ, калі яна даступная, а пазначаецца як НЕ ПРАЦУЕ.",
"maxRedirectDescription": "Максімальная колькасць перанакіраванняў. Пастаўце 0, каб адключыць перанакіраванні.",
"Upside Down Mode": "Рэжым змены статусу",
"Max. Redirects": "Макс. колькасць перанакіраванняў",
"Accepted Status Codes": "Дапушчальныя коды статуса",
"Push URL": "URL-спасылка пуш абвестак",
"needPushEvery": "Да гэтага URL неабходна звяртацца кожныя {0} секунд.",
"pushOptionalParams": "Неабавязковыя параметры: {0}",
"pushViewCode": "Як выкарыстоўваць манітор Push? (Паглядзець код)",
"programmingLanguages": "Мовы праграмавання",
"Save": "Захаваць",
"Notifications": "Апавяшчэнні",
"Not available, please setup.": "Апавяшчэнні недаступныя, патрабуецца налада.",
"Setup Notification": "Наладзіць апавяшчэнні",
"Light": "Светлая",
"Dark": "Цёмная",
"Auto": "Як у сістэме",
"Theme - Heartbeat Bar": "Тэма - радка частаты апытання",
"styleElapsedTime": "Мінулы час пад радком частаты апытання",
"styleElapsedTimeShowNoLine": "Паказаць (Без лініі)",
"styleElapsedTimeShowWithLine": "Паказаць (З лініяй)",
"Normal": "Звычайны",
"Bottom": "Унізе",
"None": "Адсутнічае",
"Timezone": "Часавы пояс TZ",
"Search Engine Visibility": "Бачнасць у пошукавых сістэмах",
"Allow indexing": "Дазволіць індэксацыю",
"Discourage search engines from indexing site": "Забараніць індэксацыю",
"Change Password": "Змяніць пароль",
"Current Password": "Бягучы пароль",
"New Password": "Новы пароль",
"Repeat New Password": "Паўтарыць новы пароль",
"Update Password": "Абнавіць пароль",
"Disable Auth": "Адключыць аўтарызацыю",
"Enable Auth": "Уключыць аўтарызацыю",
"disableauth.message1": "Вы ўпэўнены, што хочаце {disableAuth}?",
"disable authentication": "адключыць аўтарызацыю",
"disableauth.message2": "Гэта падыходзіць для {intendThirdPartyAuth} перад адкрыццём Uptime Kuma, такіх як Cloudflare Access, Authelia або іншыя.",
"where you intend to implement third-party authentication": "тых, у каго настроена старонняя сістэма аўтарызацыі",
"Please use this option carefully!": "Выкарыстоўвайце гэтую наладу асцярожна!",
"Logout": "Выйсці",
"Leave": "Пакінуць",
"I understand, please disable": "Я разумею, усё роўна адключыць",
"Confirm": "Пацвердзіць",
"No": "Не",
"Username": "Лагін",
"Password": "Пароль",
"Remember me": "Запомніць мяне",
"Login": "Уваход у сістэму",
"No Monitors, please": "Няма манітораў, калі ласка",
"add one": "дадаць",
"Notification Type": "Тып абвесткі",
"Email": "Электронная пошта",
"Test": "Тэст",
"Certificate Info": "Інфармацыя пра сертыфікат",
"Resolver Server": "DNS сервер",
"Resource Record Type": "Тып рэсурснай запісі",
"Last Result": "Апошні вынік",
"Create your admin account": "Стварыце акаўнт адміністратара",
"Repeat Password": "Паўтарыць пароль",
"Import Backup": "Імпартаваць Backup",
"Export Backup": "Спампаваць Backup",
"Export": "Экспарт",
"Import": "Імпарт",
"respTime": "Час адказу (мс)",
"notAvailableShort": "N/A",
"Default enabled": "Па змаўчанні ўключана",
"Apply on all existing monitors": "Ужыць да ўсіх існуючых манітораў",
"Create": "Стварыць",
"Clear Data": "Выдаліць даныя",
"Events": "Падзеі",
"Heartbeats": "Апытанні",
"Auto Get": "Аўта-атрыманне",
"Schedule maintenance": "Запланаваць тэхабслугоўванне",
"Affected Monitors": "Задзейнічаныя Маніторы",
"Pick Affected Monitors...": "Выберыце Задзейнічаныя Маніторы…",
"Start of maintenance": "Пачатак тэхабслугоўвання",
"All Status Pages": "Усе старонкі статусаў",
"Select status pages...": "Выберыце старонку статуса…",
"alertNoFile": "Выберыце файл для імпарту.",
"alertWrongFileType": "Выберыце JSON-файл.",
"Clear all statistics": "Ачысціць усю статыстыку",
"Skip existing": "Прапусціць існуючыя",
"Overwrite": "Перазапісаць",
"Options": "Опцыі",
"Keep both": "Пакінуць абодва",
"Verify Token": "Праверыць токен",
"Setup 2FA": "Налады 2FA",
"Enable 2FA": "Уключыць 2FA",
"Disable 2FA": "Адключыць 2FA",
"2FA Settings": "Налады 2FA",
"Two Factor Authentication": "Двухфактарная аўтэнтыфікацыя",
"filterActive": "Актыўны",
"filterActivePaused": "На паўзе",
"Active": "Актыўна",
"Inactive": "Неактыўна",
"Token": "Токен",
"Add New Tag": "Дадаць тэг",
"Add New below or Select...": "Дадаць новы або выбраць…",
"Tag with this name already exist.": "Тэг з такім імем ужо існуе.",
"Green": "Зялёны",
"Blue": "Сіні",
"Indigo": "Індыга",
"Purple": "Пурпуровы",
"Pink": "Ружовы",
"Custom": "Сваёродны",
"Search...": "Пошук…",
"Avg. Response": "Сярэдні адказ",
"Entry Page": "Галоўная",
"statusPageNothing": "Нічога няма, дадайце групу або манітор.",
"statusPageRefreshIn": "Абнаўленне праз: {0}",
"No Services": "Няма сэрвісаў",
"All Systems Operational": "Усе сістэмы працуюць",
"Partially Degraded Service": "Часткова працуючы сэрвіс",
"Degraded Service": "Пашкоджаная служба",
"Add Group": "Дадаць групу",
"Add a monitor": "Дадаць манітор",
"Edit Status Page": "Рэдагаваць старонку статусаў",
"Go to Dashboard": "Перайсці да панэлі кіравання",
"Status Page": "Старонка статуса",
"Status Pages": "Старонкі статуса",
"defaultNotificationName": "Абвесткі {notification} ({number})",
"here": "тут",
"Required": "Абавязкова",
"Post URL": "Post URL",
"Content Type": "Тып кантэнту",
"webhookJsonDesc": "{0} падыходзіць для любых сучасных HTTP-сервераў, напрыклад Express.js",
"webhookFormDataDesc": "{multipart} падыходзіць для PHP. JSON-вывад неабходна будзе апрацаваць з дапамогай {decodeFunction}",
"liquidIntroduction": "Шаблоннасьць дасягаецца з дапамогай мовы шаблонаў Liquid. Інструкцыі па выкарыстаньні прадстаўлены ў раздзеле {0}. Вось даступныя зменныя:",
"templateMsg": "паведамленне апавешчання",
"templateHeartbeatJSON": "аб'ект, які апісвае сігнал",
"templateMonitorJSON": "аб'ект, які апісвае манітор",
"templateLimitedToUpDownNotifications": "даступна толькі для апавешчанняў UP/DOWN",
"templateLimitedToUpDownCertNotifications": "даступна толькі для апавешчанняў UP/DOWN і аб заканчэньні тэрміну дзеяньня сертыфіката",
"webhookAdditionalHeadersTitle": "Дадатковыя Загалоўкі",
"webhookAdditionalHeadersDesc": "Устанаўлівае дадатковыя загалоўкі, якія адпраўляюцца з дапамогай вэб-хука. Кожны загаловак павінен быць вызначаны як JSON ключ/значэнне.",
"webhookBodyPresetOption": "Прэсет - {0}",
"webhookBodyCustomOption": "Карыстацкі аб'ект",
"Webhook URL": "URL вэбхука",
"Application Token": "Токен праграмы",
"Server URL": "URL сервера",
"Priority": "Прыярытэт",
"emojiCheatSheet": "Шпаргалка па Emoji: {0}",
"Read more": "Падрабязней",
"appriseInstalled": "Апавяшчэнне ўсталявана.",
"appriseNotInstalled": "Апавяшчэнне не ўсталявана. {0}",
"Method": "Метад",
"PushUrl": "URL пуша",
"HeadersInvalidFormat": "Загалоўкі запыту не з'яўляюцца валідным JSON: ",
"BodyInvalidFormat": "Цела запыту не з'яўляецца валідным JSON: ",
"Monitor History": "Гісторыя маніторынгу",
"clearDataOlderThan": "Захоўваць статыстыку за {0} дзён.",
"PasswordsDoNotMatch": "Паролі не супадаюць.",
"records": "запісы",
"One record": "Адзін запіс",
"steamApiKeyDescription": "Для маніторынгу гульнявога сервера Steam вам патрэбны Web-API ключ Steam. Зарэгістраваць яго можна тут: ",
"Current User": "Бягучы карыстальнік",
"topic": "Тэма",
"topicExplanation": "MQTT топік для маніторынгу",
"successKeyword": "Ключавое слова паспяховасці",
"successKeywordExplanation": "Ключавое слова MQTT, якое будзе лічыцца паспяховым",
"recent": "Апошняе",
"Reset Token": "Скід токена",
"Done": "Гатова",
"Info": "Інфа",
"Security": "Бяспека",
"Steam API Key": "Steam API-Ключ",
"Shrink Database": "Сціснуць базу даных",
"Pick a RR-Type...": "Выберыце RR-Тып…",
"Pick Accepted Status Codes...": "Выберыце прынятыя коды статуса…",
"Default": "Па змаўчанні",
"HTTP Options": "HTTP Опцыі",
"Title": "Назва інцыдэнту",
"Content": "Змест інцыдэнту",
"info": "ІНФА",
"warning": "УВАГА",
"danger": "ПАМЫЛКА",
"error": "памылка",
"critical": "крытычна",
"primary": "АСНОЎНЫ",
"light": "СВЕТЛЫ",
"dark": "ЦЁМНЫ",
"Post": "Апублікаваць",
"Please input title and content": "Калі ласка, увядзіце назву і змест",
"Created": "Створана",
"Last Updated": "Апошняе абнаўленне",
"Switch to Light Theme": "Светлая тэма",
"Switch to Dark Theme": "Цёмная тэма",
"Show Tags": "Паказаць тэгі",
"Hide Tags": "Схаваць тэгі",
"Description": "Апісанне",
"No monitors available.": "Няма даступных манітораў.",
"Add one": "Дадаць новы",
"No Monitors": "Маніторы адсутнічаюць",
"Untitled Group": "Група без назвы",
"Services": "Службы",
"Powered by": "Працуе на",
"Discard": "Скасаваць",
"Cancel": "Скасаваць",
"Select": "Выбраць",
"selectedMonitorCount": "Выбрана: {0}",
"Check/Uncheck": "Адзначыць/Зняць",
"shrinkDatabaseDescription": "Уключае VACUUM для базы даных SQLite. Калі ваша база даных была створана на версіі 1.10.0 і больш, AUTO_VACUUM ужо ўключаны і гэтае дзеянне не патрабуецца.",
"Customize": "Персаналізаваць",
"Custom Footer": "Карыстацкі footer",
"Custom CSS": "Карыстацкі CSS",
"enableProxyDescription": "Гэты проксі не будзе ўплываць на запыты манітора, пакуль ён не будзе актываваны. Вы можаце кантраляваць часовае адключэнне проксі для ўсіх манітораў праз статус актывацыі.",
"deleteStatusPageMsg": "Вы сапраўды хочаце выдаліць гэтую старонку статуса?",
"deleteProxyMsg": "Вы сапраўды хочаце выдаліць гэты проксі для ўсіх манітораў?",
"proxyDescription": "Проксі павінны быць прывязаныя да манітора, каб працаваць.",
"setAsDefaultProxyDescription": "Гэты проксі будзе па змаўчанні ўключаны для новых манітораў. Вы ўсё яшчэ можаце асобна адключаць проксі ў кожным маніторы.",
"Certificate Chain": "Ланцуг сертыфікатаў",
"Valid": "Дзейны",
"Invalid": "Нядзейсны",
"User": "Карыстальнік",
"Page Not Found": "Старонка не знойдзена",
"Installed": "Усталявана",
"Not installed": "Не ўсталявана",
"Running": "Працуе",
"Not running": "Не працуе",
"Remove Token": "Выдаліць токен",
"Start": "Пачаць",
"Stop": "Спыніць",
"Add New Status Page": "Дадаць старонку статуса",
"Slug": "Slug",
"Accept characters:": "Прымаць сімвалы:",
"startOrEndWithOnly": "Пачынаецца або заканчваецца толькі на {0}",
"No consecutive dashes": "Без паслядоўных тырэ",
"statusPageSpecialSlugDesc": "Спецыяльны значок {0}: гэтая старонка будзе адлюстроўвацца, калі значок не пазначаны",
"Next": "Далей",
"The slug is already taken. Please choose another slug.": "Гэты slug ужо заняты. Калі ласка, выберыце іншы slug.",
"No Proxy": "Без проксі",
"Authentication": "Аўтэнтыфікацыя",
"HTTP Basic Auth": "HTTP Аўтарызацыя",
"New Status Page": "Новая старонка статуса",
"Reverse Proxy": "Зваротны проксі",
"Backup": "Рэзервовая копія",
"About": "Аб праграме",
"wayToGetCloudflaredURL": "(Спампаваць cloudflared з {0})",
"cloudflareWebsite": "Вэб-сайт Cloudflare",
"Message:": "Паведамленне:",
"Don't know how to get the token? Please read the guide:": "Не ведаеце, як атрымаць токен? Калі ласка, прачытайце кіраўніцтва:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Бягучае злучэнне можа быць страчана, калі вы ў цяперашні час падключаецеся праз тунэль Cloudflare. Вы ўпэўнены, што хочаце яго спыніць? Увядзіце свой бягучы пароль, каб пацвердзіць гэта.",
"HTTP Headers": "Загалоўкі HTTP",
"Trust Proxy": "Давераны проксі",
"Other Software": "Іншае праграмнае забеспячэнне",
"For example: nginx, Apache and Traefik.": "Напрыклад: nginx, Apache і Traefik.",
"Please read": "Калі ласка, прачытайце",
"Subject:": "Тэма:",
"Valid To:": "Дзейсны да:",
"Days Remaining:": "Засталося дзён:",
"Issuer:": "Выдавец:",
"Fingerprint:": "Адбітак:",
"No status pages": "Няма старонак статуса",
"Domain Name Expiry Notification": "Абвестка пра сканчэнне тэрміну дзеяння даменнай назвы",
"Add a new expiry notification day": "Дадаць новы дзень абвесткі пра сканчэньне тэрміну дзеяння",
"Proxy": "Проксі",
"Date Created": "Дата стварэння",
"Footer Text": "Тэкст у ніжнім калонтытуле",
"Show Powered By": "Паказаць на чым створана",
"Domain Names": "Даменныя імёны",
"signedInDisp": "Вы ўвайшлі як {0}",
"signedInDispDisabled": "Аўтэнтыфікацыя адключана.",
"RadiusSecret": "Сакрэт Radius",
"RadiusSecretDescription": "Агульны сакрэт паміж кліентам і серверам",
"RadiusCalledStationId": "Ідэнтыфікатар вызываемай станцыі",
"RadiusCalledStationIdDescription": "Ідэнтыфікатар вызываемага прылады",
"RadiusCallingStationId": "Ідэнтыфікатар вызывальніка станцыі",
"RadiusCallingStationIdDescription": "Ідэнтыфікатар вызывальніка прылады",
"Certificate Expiry Notification": "Абвестка пра сканчэнне тэрміну дзеяння сертыфіката",
"API Username": "Імя карыстальніка API",
"API Key": "API ключ",
"Show update if available": "Паказваць даступныя абнаўленні",
"Also check beta release": "Праверыць абнаўленні для бета версій",
"Using a Reverse Proxy?": "Выкарыстоўваеце зваротны проксі?",
"Check how to config it for WebSocket": "Праверце, як наладзіць яго для WebSocket",
"Steam Game Server": "Гульнявы сервер Steam",
"Most likely causes:": "Найбольш верагодныя прычыны:",
"The resource is no longer available.": "Рэсурс больш не даступны.",
"There might be a typing error in the address.": "У адрасе можа быць памылка ў друку.",
"What you can try:": "Што вы можаце паспрабаваць:",
"Retype the address.": "Паўтарыце адрас.",
"Go back to the previous page.": "Вярнуцца на папярэднюю старонку.",
"Coming Soon": "Хутка",
"Connection String": "Радок падлучэння",
"Query": "Запыт",
"settingsCertificateExpiry": "Сканчэнне TLS сертыфіката",
"certificationExpiryDescription": "HTTPS Маніторы ініцыююць абвестку, калі срок дзеяння сертыфіката TLS скончыцца:",
"Setup Docker Host": "Налада Docker Host",
"Connection Type": "Тып злучэння",
"Docker Daemon": "Дэман Docker",
"noDockerHostMsg": "Не даступна. Спачатку наладзце хост Docker.",
"DockerHostRequired": "Усталюйце хост Docker для гэтага манітора.",
"deleteDockerHostMsg": "Вы сапраўды хочаце выдаліць гэты вузел docker для ўсіх манітораў?",
"socket": "Сокет",
"tcp": "TCP / HTTP",
"tailscalePingWarning": "Для таго, каб выкарыстоўваць манітор Tailscale Ping, неабходна ўсталяваць Uptime Kuma без Docker, а таксама ўсталяваць на сервер кліент Tailscale.",
"Docker Container": "Docker кантэйнер",
"Container Name / ID": "Назва кантэйнера / ID",
"Docker Host": "Хост Docker",
"Docker Hosts": "Хосты Docker",
"Domain": "Дамен",
"Workstation": "Рабочая станцыя",
"Packet Size": "Памер пакета",
"Bot Token": "Токен бота",
"wayToGetTelegramToken": "Вы можаце атрымаць токен тут - {0}.",
"Chat ID": "ID чата",
"telegramMessageThreadID": "(Неабавязкова) ID ланцуга паведамленняў",
"telegramMessageThreadIDDescription": "Неабавязковы ўнікальны ідэнтыфікатар для ланцуга паведамленняў (тэмы) форума; толькі для форумаў-супергруп",
"telegramSendSilently": "Адправіць без гуку",
"telegramSendSilentlyDescription": "Карыстальнікі атрымаюць абвестку без гуку.",
"telegramProtectContent": "Забараніць перасылку/захаванне",
"telegramProtectContentDescription": "Калі ўключана, паведамленні бота ў Telegram будуць забароненыя для перасылкі і захавання.",
"supportTelegramChatID": "Падтрымліваюцца ID чатаў, груп і каналаў",
"wayToGetTelegramChatID": "Вы можаце атрымаць ID вашага чата, адправіўшы паведамленне боту і перайсці па гэтаму URL для прагляду chat_id:",
"YOUR BOT TOKEN HERE": "ВАШ ТОКЕН БОТА ТУТ",
"chatIDNotFound": "ID чата не знойдзены; спачатку адпраўце паведамленне боту",
"disableCloudflaredNoAuthMsg": "Вы знаходзіцеся ў рэжыме без аўтарызацыі, пароль не патрабуецца.",
"trustProxyDescription": "Давяраць загалоўкам 'X-Forwarded-*'. Калі вы хочаце атрымаць правільны IP-адрас кліента, а ваш Uptime Kuma знаходзіцца пад Nginx або Apache, вам след включить гэты параметр.",
"wayToGetLineNotifyToken": "Вы можаце атрымаць токен доступу ў {0}",
"Examples": "Прыклады",
"Home Assistant URL": "URL-адрас Home Assistant",
"Long-Lived Access Token": "Токен доступу з доўгім тэрмінам службы",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Токен доступу з доўгім тэрмінам дзеяння можна стварыць, націснуўшы на імя вашага профілю (ўнізе злева) і пракруціўшы яго ўніз, потым націсніце Стварыць токен. ",
"Notification Service": "Служба абвестак",
"default: notify all devices": "па змаўчанні: апавяшчаць усе прылады",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Спіс службаў абвестак можна знайсці ў Home Assistant у раздзеле \"Інструменты распрацоўніка > Службы\", выканаўшы пошук па слове \"абвестка\", каб знайсці назву вашага прылады/тэлефона.",
"Automations can optionally be triggered in Home Assistant:": "Пры жаданні аўтаматызацыю можна актываваць у Home Assistant.:",
"Trigger type:": "Тып трыгера:",
"Event type:": "Тып падзеі:",
"Event data:": "даныя падзеі:",
"Then choose an action, for example switch the scene to where an RGB light is red.": "Затым выберыце дзеянне, напрыклад, пераключыце сцэну на чырвоны індыкатар RGB..",
"Frontend Version": "Версія інтэрфейса",
"Frontend Version do not match backend version!": "Версія інтэрфейса не адпавядае версіі сервернай часткі!",
"backupOutdatedWarning": "Састарэла: гэтая функцыя рэзервовага капіявання больш не падтрымліваецца. Праз даданыя шмат функцый, яна не можа стварыць або аднавіць поўную рэзервовую копію.",
"backupRecommend": "Зрабіце рэзервовую копію таму або папцы з данымі (./data/) напрамую.",
"Optional": "Неабавязкова",
"or": "або",
"sameAsServerTimezone": "Аналагічна часавому поясу сервера",
"startDateTime": "Пачатковая дата і час",
"endDateTime": "Канчатковая дата і час",
"cronExpression": "Выраз для Cron",
"cronSchedule": "Расклад: ",
"invalidCronExpression": "Няправільны выраз Cron: {0}",
"recurringInterval": "Інтэрвал",
"Recurring": "Паўторны",
"strategyManual": "Актыўны/Неактыўны Ручным спосабам",
"warningTimezone": "Выкарыстоўваецца часавы пояс сервера",
"weekdayShortMon": "Пн",
"weekdayShortTue": "Аўт",
"weekdayShortWed": "Ср",
"weekdayShortThu": "Чт",
"weekdayShortFri": "Пт",
"weekdayShortSat": "Сб",
"weekdayShortSun": "Нд",
"dayOfWeek": "Дзень тыдня",
"dayOfMonth": "Дзень месяца",
"lastDay": "Апошні дзень",
"lastDay1": "Апошні дзень месяца",
"lastDay2": "Другі апошні дзень месяца",
"lastDay3": "Трэці апошні дзень месяца",
"maintenanceStatus-scheduled": "Запланавана(ы)",
"maintenanceStatus-unknown": "Невядома",
"lastDay4": "Чацвёрты апошні дзень месяца",
"No Maintenance": "Няма тэхабслугоўванняў",
"pauseMaintenanceMsg": "Вы ўпэўненыя, што хочаце паставіць на паўзу?",
"maintenanceStatus-under-maintenance": "На тэхабслугоўванні",
"maintenanceStatus-inactive": "Неактыўны",
"Display Timezone": "Паказаць часавы пояс",
"Server Timezone": "Часавы пояс сервера",
"statusPageMaintenanceEndDate": "Канец",
"IconUrl": "URL значка",
"Enable DNS Cache": "(Састарэла) Уключыць DNS кэш для манітораў HTTP(S)",
"Enable": "Уключыць",
"Disable": "Адключыць",
"enableNSCD": "Уключыць NSCD (Name Service Cache Daemon) для кэшавання ўсіх DNS-запытаў",
"chromeExecutable": "Выканаўчы файл Chrome/Chromium",
"chromeExecutableAutoDetect": "Аўтавызначэнне",
"chromeExecutableDescription": "Для карыстальнікаў Docker, калі Chromium яшчэ не ўсталяваны, можа спатрэбіцца некалькі хвілін для ўсталявання і адлюстравання выніку тэставання. Ён займае 1 ГБ дыскавага прастору.",
"dnsCacheDescription": "Гэта можа не працаваць на некаторых IPv6 асяроддзях, адключыце гэта, калі ў вас узнікаюць праблемы.",
"Single Maintenance Window": "Адзінае акно тэхабслугоўвання",
"Maintenance Time Window of a Day": "Суточны інтэрвал для тэхабслугоўвання",
"Effective Date Range": "Даты дзеяння (Неабавязкова)",
"Schedule Maintenance": "Запланаваць тэхабслугоўванне",
"Edit Maintenance": "Рэдагаваць тэхабслугоўванне",
"Date and Time": "Дата і час",
"DateTime Range": "Дыяпазон даты і часу",
"loadingError": "Немагчыма атрымаць даныя, калі ласка паспрабуйце пазней.",
"plugin": "Плагін | Плагіны",
"install": "Усталяваць",
"installing": "Усталяваецца",
"uninstall": "Выдаліць",
"uninstalling": "Выдаляецца",
"confirmUninstallPlugin": "Вы ўпэўнены, што хочаце выдаліць гэты плагін?",
"notificationRegional": "Рэгіянальны",
"Clone Monitor": "Копія",
"Clone": "Кланаваць",
"cloneOf": "Копія {0}",
"smtp": "Email (SMTP)",
"secureOptionNone": "Няма / STARTTLS (25, 587)",
"secureOptionTLS": "TLS (465)",
"Ignore TLS Error": "Ігнараваць памылкі TLS",
"From Email": "Ад каго",
"emailCustomisableContent": "Наладжвальны змест",
"smtpLiquidIntroduction": "Наступныя два поля з'яўляюцца шабланізаванымі з дапамогай мовы шаблонаў Liquid. Інструкцыі па іх выкарыстаньні прадстаўлены ў раздзеле {0}. Вось даступныя зменныя:",
"emailCustomSubject": "Свая тэма",
"leave blank for default subject": "пакіньце пустым для тэмы па змаўчаньні",
"emailCustomBody": "Карыстацкі аб'ект",
"leave blank for default body": "пакіньце пустым для аб'екта па змаўчаньні",
"emailTemplateServiceName": "Назва сэрвіса",
"emailTemplateHostnameOrURL": "Назва хоста або URL",
"emailTemplateStatus": "Статус",
"emailTemplateMonitorJSON": "аб'ект, які апісвае манітор",
"emailTemplateHeartbeatJSON": "аб'ект, які апісвае сігнал",
"emailTemplateMsg": "паведамленне апавешчання",
"emailTemplateLimitedToUpDownNotification": "даступны толькі для сігналаў UP/DOWN, у адваротным выпадку null",
"To Email": "Каму",
"smtpCC": "Копія",
"smtpBCC": "Схаваная копія",
"Discord Webhook URL": "Discord вэбхук URL",
"wayToGetDiscordURL": "Вы можаце стварыць яго ў наладах канала \"Налады -> Інтэграцыі -> Стварыць Вэбхук\"",
"Bot Display Name": "Адлюстраваная назва бота",
"Prefix Custom Message": "Свой прэфікс паведамлення",
"Hello @everyone is...": "Прывітанне {'@'}everyone гэта…",
"wayToGetTeamsURL": "Як стварыць URL вэбхука вы можаце даведацца тут - {0}.",
"wayToGetZohoCliqURL": "Вы можаце даведацца, як стварыць webhook URL тут {0}.",
"needSignalAPI": "Вам патрэбны кліент Signal з падтрымкай REST API.",
"Channel access token": "Токен доступу да канала",
"wayToCheckSignalURL": "Перайдзіце па гэтаму URL, каб даведацца, як наладзіць такі кліент:",
"Recipients": "Атрымальнікі",
"Access Token": "Токен доступу",
"Channel access token (Long-lived)": "Токен доступу да канала (даўгавечны)",
"Line Developers Console": "Кансоль распрацошчыкаў Line",
"Basic Settings": "Базавыя налады",
"User ID": "ID карыстальніка",
"Your User ID": "Ваш ідэнтыфікатар карыстальніка",
"Messaging API": "API паведамленняў",
"wayToGetLineChannelToken": "Спачатку зайдзіце ў {0}, стварыце правайдэра і канал (API паведамленняў), потым вы зможаце атрымаць токен доступу да канала і ID карыстальніка з вышэйзгаданых пунктаў меню.",
"Icon URL": "URL значка",
"aboutIconURL": "Вы можаце ўставіць спасылку на значок ў поле \"URL значка\" каб змяніць малюнак профілю па змаўчанні. Не выкарыстоўваецца, калі зададзена значок Emoji.",
"aboutMattermostChannelName": "Вы можаце перавызначыць канал па змаўчанні, у які вэбхук піша, уведаўшы імя канала ў поле \"Імя канала\". Гэта неабходна ўключыць у наладах вэбхука Mattermost. Напрыклад: #other-channel",
"dataRetentionTimeError": "Перыяд захавання павінен быць 0 або больш",
"infiniteRetention": "Выберыце 0 для бясконцага захавання.",
"confirmDeleteTagMsg": "Вы сапраўды хочаце выдаліць гэты тэг? Маніторы, звязаныя з гэтым тэгам не будуць выдаленыя.",
"enableGRPCTls": "Дазволіць адпраўляць gRPC запыт праз TLS злучэнне",
"grpcMethodDescription": "Імя метада пераўтвараецца ў фармат camelCase, напрыклад, sayHello, check і г.д.",
"acceptedStatusCodesDescription": "Выберыце коды статусаў для вызначэння даступнасці службы.",
"deleteMonitorMsg": "Вы сапраўды хочаце выдаліць гэты манітор?",
"deleteMaintenanceMsg": "Вы сапраўды хочаце выдаліць гэтае тэхабслугоўванне?",
"deleteNotificationMsg": "Вы сапраўды хочаце выдаліць гэтую абвестку для ўсіх манітораў?",
"dnsPortDescription": "Па змаўчанні порт DNS сервера - 53. Мы можаце змяніць яго ў любы час.",
"resolverserverDescription": "Cloudflare з'яўляецца серверам па змаўчанні. Вы заўсёды можаце змяніць гэты сервер.",
"rrtypeDescription": "Выберыце тып рэсурснага запісу, які вы хочаце адсочваць",
"pauseMonitorMsg": "Вы сапраўды хочаце прыпыніць?",
"enableDefaultNotificationDescription": "Для кожнага новага манітора гэта апавяшчэнне будзе ўключана па змаўчанні. Вы ўсё яшчэ можаце адключыць апавяшчэнні ў кожным маніторы асобна.",
"clearEventsMsg": "Вы сапраўды хочаце выдаліць усю статыстыку падзей гэтага манітора?",
"clearHeartbeatsMsg": "Вы сапраўды хочаце выдаліць усю статыстыку апытанняў гэтага манітора?",
"confirmClearStatisticsMsg": "Вы сапраўды хочаце выдаліць УСЮ статыстыку?",
"importHandleDescription": "Выберыце \"Прапусціць існуючыя\", калі вы хочаце прапусціць кожны манітор або апавяшчэнне з такой жа назвай. \"Перазапісаць\" выдаліць кожны існуючы манітор або апавяшчэнне і дадаць зноў. Варыянт \"Не правяраць\" прымусова адновіць усе маніторы і апавяшчэнні, нават калі яны ўжо існуюць.",
"twoFAVerifyLabel": "Увядзіце свой токен, каб праверыць працу 2FA:",
"tokenValidSettingsMsg": "Токен сапраўдны! Цяпер вы можаце захаваць налады 2FA.",
"confirmImportMsg": "Вы сапраўды хочаце аднавіць рэзервовую копію? Пераканайцеся, што вы выбралі правільны варыянт імпарту.",
"confirmEnableTwoFAMsg": "Вы сапраўды хочаце ўключыць 2FA?",
"confirmDisableTwoFAMsg": "Вы сапраўды хочаце адключыць 2FA?",
"affectedStatusPages": "Паказваць абвестку аб тэхабслугоўванні на выбраных старонках статуса",
"atLeastOneMonitor": "Выберыце больш за адзін затрагаваны манітор",
"passwordNotMatchMsg": "Уведзеныя паролі не супадаюць.",
"notificationDescription": "Прымацаваць абвесткі да манітораў.",
"keywordDescription": "Пошук слова ў чыстым HTML або ў JSON-адказе (адчувальны да рэгістра).",
"invertKeywordDescription": "Шукаць, каб ключавое слова адсутнічала, а не прысутнічала.",
"jsonQueryDescription": "Выконайце json-запыт да адказу і праверце наяўнасць чаканага значэння (вяртанае значэнне будзе пераўтворана ў радок для параўнання). Глядзіце {0} для атрымання дакументацыі па мове запытаў. А трэніравацца вы можаце {1}.",
"backupDescription": "Вы можаце захаваць рэзервовую копію ўсіх манітораў і апавешчанняў у выглядзе JSON-файла.",
"backupDescription2": "Важна: гісторыя і падзеі захаваныя не будуць.",
"backupDescription3": "Важныя даныя, такія як токены апавешчанняў, дадаюцца пры экспарце, таму захоўвайце файлы ў бяспечным месцы.",
"endpoint": "канчатковая кропка",
"octopushAPIKey": "\"{API key}\" з даных уліковых запісаў HTTP API ў панэлі кіравання",
"octopushLogin": "\"Login\" з даных уліковых запісаў HTTP API ў панэлі кіравання",
"promosmsLogin": "Лагін API",
"promosmsPassword": "Пароль API",
"pushoversounds pushover": "Pushover (па змаўчанні)",
"pushoversounds bike": "Веласіпед",
"pushoversounds bugle": "Горн",
"pushoversounds cashregister": "Касавы апарат",
"pushoversounds classical": "Класічны",
"pushoversounds cosmic": "Касмічны",
"pushoversounds falling": "Падаючы",
"pushoversounds intermission": "Антракт",
"pushoversounds magic": "Магія",
"pushoversounds mechanical": "Механічны",
"pushoversounds pianobar": "Піяна-бар",
"pushoversounds siren": "Сірэна",
"pushoversounds spacealarm": "Касмічная сігналізацыя",
"pushoversounds tugboat": "Буксір",
"pushoversounds alien": "Іншаплянетная трывога (доўгая)",
"pushoversounds persistent": "Настойлівы (доўгі)",
"pushoversounds echo": "Pushover Эха (доўгае)",
"pushoversounds updown": "Уверх уніз (доўгае)",
"pushoversounds vibrate": "Толькі вібрацыя",
"pushoversounds none": "Няма (ціха)",
"pushyAPIKey": "Сакрэтны ключ API",
"pushyToken": "Токен прылады",
"apprise": "Apprise (Падтрымка 50+ сэрвісаў абвестак)",
"GoogleChat": "Google Chat (толькі Google Workspace)",
"wayToGetKookGuildID": "Уключыце \"Рэжым распрацошчыка\" у наладах Kook, а затым націсніце правай кнопкай на гільдыю, каб скапіраваць яе ID",
"Guild ID": "Ідэнтыфікатар гільдыі",
"User Key": "Ключ карыстальніка",
"Message Title": "Загаловак паведамлення",
"Notification Sound": "Гук абвесткі",
"More info on:": "Больш інфармацыі на: {0}",
"pushoverDesc1": "Экстрэмальны прыярытэт (2) мае таймаўт паўтору па змаўчанні 30 секунд і сканчаецца праз 1 гадзіну.",
"pushoverDesc2": "Калі вы хочаце адпраўляць абвесткі розным прыладам, неабходна запоўніць поле Прылада.",
"pushoverMessageTtl": "TTL паведамлення (у секундах)",
"SMS Type": "Тып SMS",
"octopushTypePremium": "Преміум (Хуткі - рэкамендуецца для аляртаў)",
"octopushTypeLowCost": "Танны (Павольны - часам блакуецца аператарамі)",
"apiCredentials": "API рэквізіты",
"checkPrice": "Тарыфы {0}:",
"octopushLegacyHint": "Вы выкарыстоўваеце старую версію Octopush (2011-2020) ці новую?",
"Check octopush prices": "Тарыфы Octopush {0}.",
"octopushPhoneNumber": "Нумар тэлефона (міжнародны фармат напр. +48123456789) ",
"octopushSMSSender": "Імя адпраўніка SMS: 3-11 сімвалаў алфавіта, лічбаў і прабелаў (a-zA-Z0-9)",
"LunaSea Device ID": "ID прылады LunaSea",
"Apprise URL": "URL апавяшчэння",
"Example:": "Прыклад: {0}",
"Read more:": "Падрабязней: {0}",
"Status:": "Статус: {0}",
"Strategy": "Стратэгія",
"Free Mobile User Identifier": "Бясплатны мабільны ідэнтыфікатар карыстальніка",
"Free Mobile API Key": "API ключ Free Mobile",
"Enable TLS": "Уключыць TLS",
"Proto Service Name": "Назва службы Proto",
"Proto Method": "Метад Proto",
"Proto Content": "Змест Proto",
"Economy": "Эканомія",
"Lowcost": "Бюджэтны",
"high": "высокі",
"SendKey": "SendKey",
"SMSManager API Docs": "Дакументацыя да API SMSManager ",
"Gateway Type": "Тып шлюза",
"You can divide numbers with": "Вы можаце дзяліць лічбы з",
"Base URL": "Базавы URL",
"goAlertInfo": "GoAlert — гэта праграма з адкрытым зыходным кодам для складання раскладу выклікаў, аўтаматычнай эскаляцыі і абвестак (напрыклад, SMS або галасавых выклікаў). Аўтаматычна прывабляйце патрэбнага чалавека, патрэбным спосабам і ў патрэбны час! {0}",
"goAlertIntegrationKeyInfo": "Атрымаць агульны ключ інтэграцыі API для службы ў гэтым фармаце \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" звычайна значэнне параметра токена скапіяванага URL.",
"AccessKeyId": "ID ключа доступу",
"SecretAccessKey": "Сакрэтны ключ доступу",
"PhoneNumbers": "Нумары тэлефонаў",
"TemplateCode": "Код шаблону",
"SignName": "SignName",
"Sms template must contain parameters: ": "Шаблон SMS павінен змяшчаць параметры: ",
"Bark API Version": "Версія Bark API",
"Bark Endpoint": "Канчатковая кропка Bark",
"Bark Group": "Bark Group",
"Bark Sound": "Bark Sound",
"WebHookUrl": "WebHookUrl",
"SecretKey": "Сакрэтны Ключ",
"For safety, must use secret key": "Для бяспекі, неабходна выкарыстоўваць сакрэтны ключ",
"Mentioning": "Згадванне",
"Don't mention people": "Не згадваць людзей",
"Mention group": "Згадаць {group}",
"Device Token": "Токен прылады",
"Platform": "Платформа",
"High": "Высокі",
"Retry": "Паўторыць",
"Topic": "Тэма",
"WeCom Bot Key": "WeCom Bot Key",
"Setup Proxy": "Налада Проксі",
"Proxy Protocol": "Пратакол Проксі",
"Proxy Server": "Проксі",
"Proxy server has authentication": "Проксі мае аўтэнтыфікацыю",
"promosmsTypeEco": "SMS ECO - танкі і павольны, часта перагружаны. Толькі для атрымальнікаў з Польшчы.",
"promosmsTypeFlash": "SMS FLASH - паведамленні аўтаматычна з'яўляюцца на прыладзе атрымальніка. Толькі для атрымальнікаў з Польшчы.",
"promosmsTypeFull": "SMS FULL - прэміум-узровень SMS, можна выкарыстоўваць сваё імя адпраўніка (папярэдне зарэгістраваў яго). Надзейна для аляртаў.",
"promosmsTypeSpeed": "SMS SPEED - найвышэйшы прыярытэт у сістэме. Вельмі хутка і надзейна, але вельмі дорага (у два разы дорага, чым SMS FULL).",
"promosmsPhoneNumber": "Нумар тэлефона (для атрымальнікаў з Польшчы можна прапусціць код рэгіёна)",
"promosmsSMSSender": "Імя адпраўніка SMS: Зарэгістраванае або адно з імён па змаўчанні: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"promosmsAllowLongSMS": "Дазволіць доўгія SMS",
"Feishu WebHookUrl": "Feishu WebHookURL",
"matrixHomeserverURL": "URL сервера (разам з http(s):// і па жаданні порт)",
"Internal Room Id": "Унутраны ID пакою",
"matrixDesc1": "Унутраны ID пакою можна знайсці ў Падрабязнасцях у параметрах канала вашага кліента Matrix. Ён павінен выглядаць прыблізна так !QMdRCpUIfLwsfjxye6:home.server.",
"matrixDesc2": "Рэкамендуецца стварыць новага карыстальніка і не выкарыстоўваць токен доступу асабістага карыстальніка Matrix, т.к. гэта ўяўляе за сабой поўны доступ да акаўнта і да пакояў, у якіх вы знаходзіцеся. Замест гэтага стварыце новага карыстальніка і запрасіце яго толькі ў той пакой, у якім вы хочаце атрымліваць абвесткі. Токен доступу можна атрымаць, выканаўшы каманду {0}",
"Channel Name": "Назва канала",
"Notify Channel": "Канал апавешчанняў",
"aboutNotifyChannel": "Апавяшчэнне аб канале выкліча настольнае або мабільнае апавяшчэнне для ўсіх удзельнікаў канала, незалежна ад таго, ці ўстаноўлена іх даступнасць як актыўная або адсутная.",
"Uptime Kuma URL": "Uptime Kuma URL",
"setup a new monitor group": "наладзіць новую групу манітораў",
"openModalTo": "адкрыць мадальнае акно {0}",
"Add a domain": "Дадаць дамен",
"Remove domain": "Выдаліць дамен '{0}'",
"Icon Emoji": "Emoji",
"signalImportant": "ВАЖНА: Нельга змешваць у Атрымальніках групы і нумары!",
"aboutWebhooks": "Больш інфармацыі аб вэбхуках: {0}",
"aboutChannelName": "Увядзіце назву канала ў поле {0} Назва канала, калі вы хочаце абысці канал вэбхука. Напрыклад: #other-channel",
"aboutKumaURL": "Калі поле Uptime Kuma URL у наладах застанецца пустым, па змаўчанні будзе выкарыстоўвацца спасылка на праект на GitHub.",
"smtpDkimSettings": "DKIM Налады",
"smtpDkimDesc": "Калі ласка, азнаёмцеся з {0} Nodemailer DKIM для выкарыстання.",
"documentation": "дакументацыяй",
"smtpDkimDomain": "Назва дамена",
"smtpDkimKeySelector": "Ключ",
"smtpDkimPrivateKey": "Прыватны ключ",
"smtpDkimHashAlgo": "Алгарытм хэша (неабавязкова)",
"smtpDkimheaderFieldNames": "Загаловак ключоў для подпісу (неабавязкова)",
"smtpDkimskipFields": "Загаловак ключоў не для подпісу (опцыянальна)",
"wayToGetPagerDutyKey": "Вы можаце гэта атрымаць, перайшоўшы ў Сервіс -> Каталог сервісаў -> (Выберыце сервіс) -> Інтэграцыі -> Дадаць інтэграцыю. Тут вы можаце шукаць «Events API V2». Падрабязней {0}",
"Integration Key": "Ключ інтэграцыі",
"Integration URL": "URL інтэграцыі",
"Auto resolve or acknowledged": "Аўтаматычнае развязванне або пацверджанне",
"do nothing": "нічога не рабіць",
"auto acknowledged": "аўтаматычна пацверджана",
"auto resolve": "аўтаматычна развязана",
"alertaApiEndpoint": "Канчатковая кропка API",
"alertaEnvironment": "Асяроддзе",
"alertaApiKey": "Ключ API",
"alertaAlertState": "Стан алярта",
"alertaRecoverState": "Стан аднаўлення",
"serwersmsAPIUser": "API Карыстальнік (уключаючы прэфікс webapi_)",
"serwersmsAPIPassword": "API Пароль",
"serwersmsPhoneNumber": "Нумар тэлефона",
"serwersmsSenderName": "SMS Імя адпраўніка (зарэгістравана праз карыстальніцкі партал)",
"smseagleTo": "Нумар(ы) тэлефона",
"smseagleGroup": "Назва(ы) групы тэлефоннай кнігі",
"smseagleContact": "Імёны кантактаў тэлефоннай кнігі",
"smseagleRecipientType": "Тып атрымальніка",
"smseagleRecipient": "Атрымальнік(і) (калі множнасць, павінны быць раздзеленыя коскай)",
"smseagleToken": "Токен доступу API",
"smseagleUrl": "URL вашага прылады SMSEagle",
"smseagleEncoding": "Адправіць у Unicode",
"smseaglePriority": "Прыярытэт паведамлення (0-9, па змаўчанні = 0)",
"Recipient Number": "Нумар атрымальніка",
"From Name/Number": "Імя/нумар адпраўніка",
"Leave blank to use a shared sender number.": "Пакіньце пустым, каб выкарыстоўваць агульны нумар адпраўніка.",
"Octopush API Version": "Версія API Octopush",
"Legacy Octopush-DM": "Састарэлы Octopush-DM",
"ntfy Topic": "Тэма ntfy",
"Server URL should not contain the nfty topic": "URL сервера не павінен утрымліваць тэму nfty",
"onebotHttpAddress": "HTTP-адрас OneBot",
"onebotMessageType": "Тып паведамлення OneBot",
"onebotGroupMessage": "Група",
"onebotPrivateMessage": "Асабістае",
"onebotUserOrGroupId": "ID групы/карыстальніка",
"onebotSafetyTips": "Для бяспекі неабходна ўсталяваць токен доступу",
"PushDeer Server": "Сервер PushDeer",
"pushDeerServerDescription": "Пакіньце пустым для выкарыстання афіцыйнага сервера",
"PushDeer Key": "Ключ PushDeer",
"wayToGetClickSendSMSToken": "Вы можаце атрымаць імя карыстальніка API і ключ API з {0} .",
"Custom Monitor Type": "Сваёродны тып манітора",
"Google Analytics ID": "ID Google Аналітыкі",
"Edit Tag": "Рэдагаваць тэг",
"Server Address": "Адрас сервера",
"Learn More": "Даведацца больш",
"Body Encoding": "Тып зместу запыту.(JSON або XML)",
"API Keys": "API Ключы",
"Expiry": "Сканчэнне",
"Continue": "Працягнуць",
"Add Another": "Дадаць яшчэ",
"Key Added": "Ключ дададзены",
"apiKeyAddedMsg": "Ваш ключ API дададзены. Звярніце ўвагу на гэтае паведамленне, так як яно адлюстроўваецца адзін раз.",
"Add API Key": "Дадаць API ключ",
"No API Keys": "Няма ключоў API",
"apiKey-active": "Актыўны",
"apiKey-expired": "Скончыўся",
"apiKey-inactive": "Неактыўны",
"Expires": "Сканчаецца",
"disableAPIKeyMsg": "Вы ўпэўнены, што хочаце адключыць гэты API ключ?",
"deleteAPIKeyMsg": "Вы ўпэўнены, што хочаце выдаліць гэты ключ API?",
"Generate": "Згенераваць",
"pagertreeIntegrationUrl": "URL-адрас інтэграцыі",
"pagertreeUrgency": "Тэрміновасць",
"pagertreeSilent": "Ціхі",
"pagertreeLow": "Нізкі",
"pagertreeMedium": "Сярэдні",
"pagertreeHigh": "Высокі",
"pagertreeCritical": "Крытычны",
"pagertreeResolve": "Аўтаматычнае развязванне",
"pagertreeDoNothing": "Нічога не рабіць",
"wayToGetPagerTreeIntegrationURL": "Пасля стварэння інтэграцыі Uptime Kuma ў PagerTree скапіруйце файл {Endpoint}. Гл. поўную інфармацыю {0}",
"lunaseaTarget": "Мэта",
"lunaseaDeviceID": "Ідэнтыфікатар прылады",
"lunaseaUserID": "Ідэнтыфікатар карыстальніка",
"ntfyAuthenticationMethod": "Метад уваходу",
"ntfyPriorityHelptextAllEvents": "Усе падзеі адпраўляюцца з максімальным прыярытэтам",
"ntfyPriorityHelptextAllExceptDown": "Усе падзеі адпраўляюцца з гэтым прыярытэтам, акрамя {0}-падзеяў, якія маюць прыярытэт {1}",
"ntfyUsernameAndPassword": "Лагін і пароль",
"twilioAccountSID": "SID уліковага запісу",
"twilioApiKey": "Ключ API (неабавязкова)",
"twilioAuthToken": "Токен аўтарызацыі / Сакрэтны API ключ",
"twilioFromNumber": "З нумара",
"twilioToNumber": "На нумар",
"Monitor Setting": "Налада манітора {0}",
"Show Clickable Link": "Паказаць націскальную спасылку",
"Show Clickable Link Description": "Калі пазначаны флажок, усе, хто мае доступ да гэтай старонкі стану, могуць мець доступ да URL-адрасу манітора.",
"Open Badge Generator": "Адкрыць генератар значкаў",
"Badge Generator": "Генератар значкоў для {0}",
"Badge Type": "Тып значка",
"Badge Duration (in hours)": "Тэрмін дзеяння значка (у гадзінах)",
"Badge Label": "Надпіс для значка",
"Badge Prefix": "Значэнне прэфікса значка",
"Badge Suffix": "Значэнне суфікса значка",
"Badge Label Color": "Колер надпісу значка",
"Badge Color": "Колер значка",
"Badge Label Prefix": "Прэфікс надпісу для значка",
"Badge Preview": "Папярэдні прагляд значка",
"Badge Label Suffix": "Суфікс надпісу для значка",
"Badge Up Color": "Колер значка для статусу \"Даступны\"",
"Badge Down Color": "Колер значка для статусу \"Недаступны\"",
"Badge Pending Color": "Колер значка для статусу \"Чаканне\"",
"Badge Maintenance Color": "Колер значка для статусу \"Тэхабслугоўванне\"",
"Badge Warn Color": "Колер значка для папярэджання",
"Badge Warn Days": "Значок для \"дзён папярэджання\"",
"Badge Down Days": "Значок для \"дзён недаступнасці\"",
"Badge Style": "Стыль значка",
"Badge value (For Testing only.)": "Значэнне значка (толькі для тэставання)",
"Group": "Група",
"Monitor Group": "Група манітораў",
"monitorToastMessagesLabel": "Апавяшчэнні",
"monitorToastMessagesDescription": "Паведамленні для манітораў знікаюць праз зададзены час у секундах. Значэнне -1 адключае тайм-аўт. Значэнне 0 адключае апавяшчэнні.",
"toastErrorTimeout": "Таймаут для апавешчанняў пра памылкі",
"toastSuccessTimeout": "Таймаут для апавешчанняў пра паспяховасьць",
"Enter the list of brokers": "Увядзіце спіс брокераў",
"Kafka Brokers": "Kafka Brokers",
"Press Enter to add broker": "Націсніце Enter, каб дадаць брокера",
"Kafka Topic Name": "Назва тэмы Kafka",
"Kafka Producer Message": "Паведамленне продюсера Kafka",
"Enable Kafka SSL": "Уключэнне пратаколу Kafka SSL",
"Enable Kafka Producer Auto Topic Creation": "Уключэнне аўтаматычнага стварэння тэм у Kafka Producer",
"Kafka SASL Options": "Параметры SASL у Kafka",
"Mechanism": "Механізм",
"Pick a SASL Mechanism...": "Выберыце механізм SASL…",
"Authorization Identity": "Аўтарызацыйная ідэнтычнасць",
"AccessKey Id": "AccessKey Id",
"Secret AccessKey": "Сакрэтны ключ доступу",
"Session Token": "Токен сеансу",
"noGroupMonitorMsg": "Не даступна. Спачатку стварыце групу манітораў.",
"Close": "Закрыць",
"Request Body": "Цела запыту",
"wayToGetFlashDutyKey": "Вы можаце перайсці на старонку \"Канал\" -> (Выберыце канал) -> \"Інтэграцыі\" -> \"Дадаць новую старонку інтэграцыі\", дадаць \"Карыстацкую падзею\", каб атрымаць push-адрас, скапіяваць ключ інтэграцыі ў адрас. Для атрымання дадатковай інфармацыі, калі ласка, наведайце",
"FlashDuty Severity": "Сур'ёзнасць",
"nostrRecipients": "Адкрытыя ключы атрымальнікаў (npub)",
"nostrRelaysHelp": "Адзін URL-адрас рэтрансляцыі ў кожным радку",
"nostrSender": "Закрыты ключ адпраўшчыка (nsec)",
"nostrRecipientsHelp": "фармат npub, па адным у радку",
"showCertificateExpiry": "Паказваць пратэрмінаваны сертыфікат",
"noOrBadCertificate": "Адсутнасць сертыфіката",
"gamedigGuessPortDescription": "Порт, які выкарыстоўваецца пратаколам Valve Server Query Protocol, можа адрознівацца ад порта кліента. Паспрабуйце гэта, калі манітор не можа падключыцца да сервера.",
"authUserInactiveOrDeleted": "Карыстальнік неактыўны або выдалены.",
"authInvalidToken": "Няправільны токен.",
"authIncorrectCreds": "Няправільнае імя карыстальніка або пароль.",
"2faAlreadyEnabled": "2FA ўжо ўключана.",
"2faEnabled": "2FA ўключана.",
"2faDisabled": "2FA адключана.",
"successAdded": "Паспяхова дададзена.",
"successResumed": "Паспяхова прадоўжана.",
"successPaused": "Паспяхова спынена.",
"successDeleted": "Паспяхова выдалена.",
"successEdited": "Паспяхова зменена.",
"successAuthChangePassword": "Пароль паспяхова абноўлены.",
"successBackupRestored": "Рэзервовая копія паспяхова адноўлена.",
"successDisabled": "Паспяхова адключана.",
"successEnabled": "Паспяхова ўключана.",
"tagNotFound": "Тэг не знойдзены.",
"foundChromiumVersion": "Выяўлены Chromium/Chrome. Версіі: {0}",
"Remote Browsers": "Аддаленыя браўзеры",
"Remote Browser": "Аддалены браўзер",
"Add a Remote Browser": "Дадаць аддалены браўзер",
"Remote Browser not found!": "Аддалены браўзер не знойдзены!",
"remoteBrowsersDescription": "Аддаленыя браўзеры — альтэрнатыва лакальнаму запуску Chromium. Усталюйце такі сервіс, як browserless.io, або падлучыцеся да свайго ўласнага",
"self-hosted container": "кантэйнер, які хостыцца самастойна",
"remoteBrowserToggle": "Па змаўчаньні Chromium працуе ўнутры кантэйнера Uptime Kuma. Вы можаце выкарыстоўваць аддалены браўзер, пераключыўшы гэты пераключальнік.",
"useRemoteBrowser": "Выкарыстоўваць удалены браўзер",
"deleteRemoteBrowserMessage": "Вы ўпэўнены, што хочаце выдаліць гэты удалены браўзер для ўсіх манітораў?",
"Browser Screenshot": "Скрыншот браўзера",
"wayToGetSevenIOApiKey": "Зайдзіце на панэль кіравання па адрасе app.seven.io > распрацоўшчык > {api key} > зялёная кнопка дадаць",
"senderSevenIO": "Адпраўляе нумар або імя",
"receiverSevenIO": "Нумар атрымання",
"apiKeySevenIO": "SevenIO {API Key}",
"receiverInfoSevenIO": "Калі нумар атрымальніка не знаходзіцца ў Германіі, то перад нумарам неабходна дадаць код краіны (напрыклад, для ЗША код краіны 1, тады выкарыстоўвайце 117612121212, замест 017612121212)",
"wayToGetWhapiUrlAndToken": "Вы можаце атрымаць {API URL} і токен, зайдзяўшы ў патрэбны вам канал з {0}",
"whapiRecipient": "Нумар тэлефона / ID кантакта / ID групы",
"documentationOf": "{0} Дакументацыя",
"What is a Remote Browser?": "Што такое аддалены браўзер?",
"wayToGetHeiiOnCallDetails": "Як атрымаць ID трыгера і {API Keys}, напісана ў {documentation}",
"callMeBotGet": "Тут вы можаце стварыць {endpoint} для {0}, {1} і {2}. Майце на ўвазе, што вы можаце атрымаць абмежаванне па хуткасці. Абмежаванні па хуткасці выглядаюць наступным чынам: {3}",
"gtxMessagingApiKeyHint": "Вы можаце знайсці свой {API key} на старонцы: Мае уліковыя запісы маршрутызацыі > Паказаць інфармацыю аб уліковым запісе > Уліковыя даныя API > REST API (v2.x)",
"From Phone Number / Transmission Path Originating Address (TPOA)": "Нумар тэлефона / Адрас крыніцы шляху перадачы (АІПП)",
"To Phone Number": "На нумар тэлефона",
"gtxMessagingToHint": "Міжнародны фармат, з «+» ({e164}, {e212} або {e214})",
"Alphanumeric (recommended)": "Літарна-лічбавая (рэкамендуецца)",
"Telephone number": "Нумар тэлефона",
"cellsyntOriginatortypeAlphanumeric": "Літарна-лічбавы радок (не больш за 11 літарна-лічбавых сімвалаў). Атрымальнікі не могуць адказаць на гэта паведамленне.",
"cellsyntOriginatortypeNumeric": "Лічбавае значэнне (не больш за 15 лічбаў) з нумарам тэлефона ў міжнародным фармаце без 00 ў пачатку (напрыклад, нумар Вялікабрытаніі 07920 110 000 павінен быць заданы, як 447920110000). Атрымальнікі могуць адказаць на паведамленне.",
"Originator": "Крыніца",
"cellsyntOriginator": "Бачны на мабільным тэлефоне атрымальніка як адпраўшчыка паведамлення. Дапушчальныя значэнні і функцыя залежаць ад параметра {originatortype}.",
"cellsyntDestination": "Нумар тэлефона атрымальніка ў міжнародным фармаце з 00 ў пачатку, за якім следуе код краіны, напрыклад, 00447920110000 для нумара Вялікабрытаніі 07920 110 000 (не больш за 17 лічбаў у суме). Не больш за 25000 атрымальнікаў, раздзеленых коскамі, на адзін HTTP-запыт.",
"Allow Long SMS": "Дазволіць доўгія SMS",
"cellsyntSplitLongMessages": "Раздзеляць доўгія паведамленні на 6 частак. 153 x 6 = 918 сімвалаў.",
"max 15 digits": "макс. 15 лічбаў",
"max 11 alphanumeric characters": "максімум 11 літарна-лічбавых сімвалаў",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Увядзіце {Hostname} сервера, да якога вы хочаце падключыцца, або {localhost}, калі вы хочаце выкарыстоўваць {local_mta}",
"Saved.": "Захавана.",
"wayToWriteWhapiRecipient": "Нумар тэлефона з міжнародным прэфіксам, але без знака плюс у пачатку ({0}), ідэнтыфікатара кантакта ({1}) або ідэнтыфікатара групы ({2}).",
"gtxMessagingFromHint": "На мабільных тэлефонах атрымальнікі бачаць АІПП як адпраўшчыка паведамлення. Дапускаецца выкарыстаньне да 11 літарна-лічбавых сімвалаў, шорткода, мясцовага доўгага кода або міжнародных нумароў ({e164}, {e212} або {e214})",
"Send to channel": "Адправіць у канал",
"threadForumPostID": "Трэд / ID паста",
"whatHappensAtForumPost": "Стварыць новы пост на форуме. Гэта НЕ размяшчае паведамленні ў існуючым пасце. Для публікацыі ў існуючай публікацыі выкарыстоўвайце \"{option}\"",
"now": "зараз",
"-year": "-год"
}

@ -77,7 +77,7 @@
"Save": "Запази",
"Notifications": "Известия",
"Not available, please setup.": "Не са налични. Моля, настройте.",
"Setup Notification": "Настрой известие",
"Setup Notification": "Настройка на известие",
"Light": "Светла",
"Dark": "Тъмна",
"Auto": "Автоматично",
@ -146,7 +146,7 @@
"Options": "Опции",
"Keep both": "Запази двете",
"Verify Token": "Провери токен код",
"Setup 2FA": "Настройка 2FA",
"Setup 2FA": "Настройка на 2FA",
"Enable 2FA": "Активирай 2FA",
"Disable 2FA": "Деактивирай 2FA",
"2FA Settings": "Настройка за 2FA",
@ -312,7 +312,6 @@
"PasswordsDoNotMatch": "Паролите не съвпадат.",
"Current User": "Текущ потребител",
"recent": "Скорошни",
"shrinkDatabaseDescription": "Инициира \"VACUUM\" за \"SQLite\" база данни. Ако Вашата база данни е създадена след версия 1.10.0, \"AUTO_VACUUM\" функцията е активна и това действие не е нужно.",
"Done": "Готово",
"Info": "Информация",
"Security": "Сигурност",
@ -403,7 +402,7 @@
"Retry": "Повтори",
"Topic": "Тема",
"WeCom Bot Key": "WeCom бот ключ",
"Setup Proxy": "Настрой прокси",
"Setup Proxy": "Настройка на прокси",
"Proxy Protocol": "Прокси протокол",
"Proxy Server": "Прокси сървър",
"Proxy server has authentication": "Прокси сървърът е с удостоверяване",
@ -802,7 +801,6 @@
"twilioApiKey": "API ключ (по избор)",
"Expected Value": "Очаквана стойност",
"Json Query": "Заявка тип JSON",
"jsonQueryDescription": "Прави JSON заявка срещу отговора и проверява за очаквана стойност (Върнатата стойност ще бъде преобразувана в низ за сравнение). Разгледайте {0} за документация относно езика на заявката. Имате възможност да тествате {1}.",
"Badge Duration (in hours)": "Времетраене на баджа (в часове)",
"Badge Preview": "Преглед на баджа",
"Notify Channel": "Канал за известяване",
@ -897,10 +895,10 @@
"DockerHostRequired": "Моля, задайте \"Docker\" хоста за този монитор.",
"Browser Screenshot": "Екранна снимка на браузър",
"remoteBrowserToggle": "По подразбиране Chromium работи в контейнера Uptime Kuma. Можете да използвате отдалечен браузър, като превключите този ключ.",
"remoteBrowsersDescription": "Отдалечените браузъри са алтернатива на локалното стартиране на Chromium. Настройте с услуга като browserless.io или свържете с вашата собствена",
"remoteBrowsersDescription": "Отдалечените браузъри са алтернатива на локалното стартиране на Chromium. Настройте с услуга като \"browserless.io\" или свържете с Вашата собствена",
"Remove the expiry notification": "Премахни деня за известяване при изтичане",
"Add a new expiry notification day": "Добави нов ден за известяване при изтичане",
"setup a new monitor group": "настройване на нова група от монитори",
"setup a new monitor group": "настройка на нова група от монитори",
"openModalTo": "отвори модален прозорец към {0}",
"Add a domain": "Добави домейн",
"Remove domain": "Премахни домейн '{0}'",
@ -973,5 +971,84 @@
"Create new forum post": "Създай нова публикация във форум",
"postToExistingThread": "Публикувай в съществуваща тема/публикация във форум",
"forumPostName": "Име на публикацията във форума",
"threadForumPostID": "ID на публикация в темата/форум"
"threadForumPostID": "ID на публикация в темата/форум",
"smspartnerApiurl": "Можете да намерите вашия API ключ в таблото на {0}",
"smspartnerPhoneNumber": "Телефон номер(а)",
"smspartnerPhoneNumberHelptext": "Номерът задължително е в международен формат {0}, {1}. Отделните номера трябва да бъдат разделени посредством {2}",
"smspartnerSenderName": "Име на изпращащия на SMS",
"smspartnerSenderNameInfo": "Трябва да е между 3..=11 стандартни знака",
"wayToGetThreemaGateway": "Можете да се регистрирате за Threema Gateway {0}.",
"threemaRecipient": "Получател",
"threemaRecipientType": "Тип получател",
"threemaRecipientTypeIdentity": "Threema-ID",
"threemaRecipientTypePhone": "Телефонен номер",
"threemaRecipientTypeEmail": "Имейл адрес",
"threemaSenderIdentity": "Gateway-ID",
"threemaSenderIdentityFormat": "8 знака, обикновено започва с *",
"threemaApiAuthenticationSecret": "Gateway-ID Тайна фраза",
"threemaRecipientTypeIdentityFormat": "8 знака",
"threemaRecipientTypePhoneFormat": "E.164, без водещ +",
"threemaBasicModeInfo": "Забележка: Тази интеграция използва Threema Gateway в основен режим (сървърно базирано криптиране). Допълнителни подробности можете да намерите {0}.",
"apiKeysDisabledMsg": "API ключовете са деактивирани, защото удостоверяването е деактивирано.",
"jsonQueryDescription": "Анализира и извлича конкретни данни от JSON отговора на сървъра, използвайки JSON заявка или чрез \"$\" за необработения отговор, ако не очаква JSON. След това резултатът се сравнява с очакваната стойност като низове. Вижте {0} за документация и използвайте {1}, за да експериментирате със заявки.",
"starts with": "започва с",
"less than or equal to": "по-малко или равно на",
"now": "сега",
"time ago": "преди {0}",
"-year": "-година",
"Json Query Expression": "Json израз на заявка",
"and": "и",
"cacheBusterParam": "Добави параметъра {0}",
"cacheBusterParamDescription": "Произволно генериран параметър за пропускане на кешове.",
"Community String": "Общностен низ",
"snmpCommunityStringHelptext": "Този низ функционира като парола за удостоверяване и контрол на достъпа до устройства с активиран SNMP. Сравнете го с конфигурацията на вашето SNMP устройство.",
"OID (Object Identifier)": "OID (Идентификатор на обект)",
"snmpOIDHelptext": "Въведете OID за сензора или състоянието, които искате да мониторирате. Използвайте инструменти за управление на мрежата като MIB браузъри или SNMP софтуер, ако не сте сигурни за OID.",
"SNMP Version": "SNMP Версия",
"Please enter a valid OID.": "Моля, въведете валиден OID.",
"Host Onesender": "Onesender хост",
"Token Onesender": "Onesender токен",
"Recipient Type": "Тип получател",
"Private Number": "Частен номер",
"privateOnesenderDesc": "Уверете се, че телефонният номер е валиден. За да изпратите съобщение на личен телефонен номер, напр.: 628123456789",
"groupOnesenderDesc": "Уверете се, че GroupID е валиден. За да изпратите съобщение в група, напр.: 628123456789-342345",
"Group ID": "ID на групата",
"wayToGetOnesenderUrlandToken": "Можете да получите URL адреса и токена, като посетите уебсайта на Onesender. Повече информация {0}",
"Add Remote Browser": "Добави отдалечен браузър",
"New Group": "Нова група",
"Group Name": "Име на групата",
"OAuth2: Client Credentials": "OAuth2: Идентификационни данни на клиента",
"Condition": "Условие",
"Authentication Method": "Метод за удостоверяване",
"Authorization Header": "Хедър за оторизация",
"Form Data Body": "Тяло на формата за данни",
"OAuth Token URL": "URL адрес на OAuth токена",
"Client ID": "ID на клиента",
"Client Secret": "Тайна на клиента",
"OAuth Scope": "Обхват на OAuth",
"Optional: Space separated list of scopes": "По избор: разделен с интервал списък с обхвати",
"Go back to home page.": "Обратно към началната страница.",
"No tags found.": "Няма намерени етикети.",
"Lost connection to the socket server.": "Изгубена връзка със сокет сървъра.",
"Cannot connect to the socket server.": "Не може да се свърже със сокет сървъра.",
"SIGNL4": "SIGNL4",
"SIGNL4 Webhook URL": "SIGNL4 URL адрес на уеб кука",
"signl4Docs": "Повече информация относно конфигуриране на SIGNL4 и получаване на URL адрес за уеб кука SIGNL4 в {0}.",
"Conditions": "Условия",
"conditionAdd": "Добави условие",
"conditionDelete": "Изтрий условие",
"conditionAddGroup": "Добави група",
"conditionDeleteGroup": "Изтрий група",
"conditionValuePlaceholder": "Стойност",
"contains": "съдържа",
"equals": "равно на",
"not equals": "не е равно на",
"not contains": "не съдържа",
"not starts with": "не започва с",
"ends with": "завършва с",
"not ends with": "не завършва с",
"less than": "по-малко от",
"greater than": "по-голямо от",
"greater than or equal to": "по-голямо или равно на",
"record": "запис"
}

@ -1 +1,18 @@
{}
{
"setupDatabaseChooseDatabase": "আপনি কোন ডাটাবেজটি ব্যবহার করতে চান?",
"setupDatabaseEmbeddedMariaDB": "আপনাকে কিছু নিযুক্ত করতে হবে না। এই ডকার ইমেজটি (Docker image) স্বয়ংক্রিয়ভাবে আপনার জন্য মারিয়া ডিবি (MariaDB) বসিয়েছে এবং প্রস্তুত করেছে।Uptime Kuma ইউনিক্স সকেটের (Unix Socket) মাধ্যমে এই ডাটাবেসের সাথে সংযুক্ত হবে।",
"setupDatabaseMariaDB": "একটি বহিরাগত মারিয়া ডিবি (MariaDB) ডাটাবেসের সাথে সংযোগ করুন। আপনাকে ডাটাবেস সংযোগ তথ্য নিযুক্ত করতে হবে।",
"Add": "সংযোগ করুন",
"dbName": "ডাটাবেজের নাম",
"languageName": "ইংরেজি",
"Settings": "সেটিংস",
"Dashboard": "ড্যাশবোর্ড",
"Help": "সাহায্য",
"New Update": "নতুন আপডেট",
"Language": "ভাষা",
"Version": "সংস্করণ",
"Check Update On GitHub": "GitHub-এ আপডেট চেক করুন",
"List": "তালিকা",
"General": "সাধারণ",
"Game": "খেলা"
}

@ -70,5 +70,148 @@
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Introduïu el nom del servidor al qual voleu connectar-vos o {localhost} si voleu utilitzar un {local_mta}",
"Host URL": "URL del servidor",
"Friendly Name": "Nom senzill",
"markdownSupported": "Sintaxi de Markdown suportada"
"markdownSupported": "Sintaxi de Markdown suportada",
"Retries": "Reintents",
"Advanced": "Avançat",
"ignoreTLSErrorGeneral": "Ignora errors TLS/SSL per connexió",
"maxRedirectDescription": "Nombre màxim de redireccions a seguir. Establiu a 0 per a desactivar les redireccions.",
"Upside Down Mode": "Mode al revés",
"Max. Redirects": "Redireccions Màx",
"Accepted Status Codes": "Codis d'Estat Acceptats",
"needPushEvery": "Hauries de cridar a aquesta URL cada {0} segons.",
"Heartbeat Retry Interval": "Reintent de l'interval de \"heartbeat\"",
"Resend Notification if Down X times consecutively": "Reenvia notificacions si Down X vegades consecutives",
"resendEveryXTimes": "Reenvia cada {0} vegades",
"retryCheckEverySecond": "Reintenta cada {0} segons",
"checkEverySecond": "Comprova cada {0} segons",
"resendDisabled": "Reenvia deshabilitat",
"retriesDescription": "Màxim d'intents abans de que el servei sigui marcat com a caigut i la notificació sigui enviada",
"ignoreTLSError": "Ignora errors de TLS/SSL per a pàgines web HTTPS",
"upsideDownModeDescription": "Canvia l'estat al revés. Si el servei és accessible, està CAIGUT.",
"Setup Notification": "Configurar Notificació",
"Allow indexing": "Permetre indexat",
"Discourage search engines from indexing site": "Desencoratjar als motors de cerca que indexin la pàgina",
"Current Password": "Contrasenya actual",
"Please use this option carefully!": "Per favor, empra aquesta opció amb cura !",
"disable authentication": "deshabilita autenticació",
"Partially Degraded Service": "Servei Parcialment Degradat",
"Degraded Service": "Servei Degradat",
"Add Group": "Afegir Grup",
"Add a monitor": "Afegir monitor",
"pushViewCode": "Com emprar el monitor de Push? (Veure Codi)",
"Notifications": "Notificacions",
"pushOthers": "Altres",
"programmingLanguages": "Llenguatges de programació",
"Dark": "Fosc",
"Remember me": "Recordar-me",
"Login": "Iniciar sessió",
"No Monitors, please": "Sense Monitors, per favor",
"notAvailableShort": "N/A",
"Two Factor Authentication": "Segon Factor d'Autenticació",
"Custom": "Personalitzat",
"Search...": "Cercar…",
"Search monitored sites": "Cercar llocs monitoritzats",
"Avg. Response": "Resp. Promig",
"Add New below or Select...": "Afegir nova o Seleccionar…",
"Tag with this name already exist.": "Aquesta etiqueta ja existeix.",
"color": "Color",
"Gray": "Gris",
"value (optional)": "valor (opcional)",
"Active": "Actiu",
"Push URL": "URL push",
"pushOptionalParams": "Paràmetres opcionals: {0}",
"Save": "Desa",
"Not available, please setup.": "No disponible, per favor configura-ho.",
"Light": "Clar",
"Auto": "Auto",
"Theme - Heartbeat Bar": "Tema - Heartbet Bar",
"styleElapsedTime": "Temps transcorregut a la barra",
"styleElapsedTimeShowNoLine": "Mostrar (Fora Línia)",
"styleElapsedTimeShowWithLine": "Mostrar (Amb línia)",
"Normal": "Normal",
"Bottom": "Inferior",
"None": "Cap",
"Timezone": "Zona Horària",
"Search Engine Visibility": "Visibilitat motor de cerca",
"Change Password": "Canviar contrasenya",
"New Password": "Nova Contrasenya",
"Repeat New Password": "Repeteix Nova Contrasenya",
"Update Password": "Actualitzar Contrasenya",
"Disable Auth": "Deshabilita autenticació",
"Enable Auth": "Habilita Autenticació",
"disableauth.message1": "Estau segur que voleu {disableAuth}?",
"disableauth.message2": "Està dissenyat per a escenaris {intendThirdPartyAuth} davant Uptime Kuma, com ara Cloudflare Access, Authelia o altres mecanismes d'autenticació.",
"where you intend to implement third-party authentication": "on es vol implementar l'autenticació de tercers",
"Logout": "Tancar sessió",
"Leave": "Marxar",
"I understand, please disable": "Ho entenc, per favor deshabilita-ho",
"Confirm": "Confirma",
"Yes": "Si",
"No": "No",
"Username": "Nom d'usuari",
"Password": "Contrasenya",
"add one": "afegir un",
"Notification Type": "Tipus de notificació",
"Test": "Test",
"Certificate Info": "Informació del certificat",
"Resolver Server": "Servidor DNS",
"Resource Record Type": "Tipus de registre",
"Create your admin account": "Crear compte d'administració",
"Repeat Password": "Repeteix Contrasenya",
"Export Backup": "Exportar Còpia",
"Import Backup": "Importar Còpia",
"Export": "Exporta",
"Import": "Importa",
"respTime": "Temps Resp. (ms)",
"Default enabled": "Habilitat per defecte",
"Apply on all existing monitors": "Aplicar a tots els monitors existents",
"Create": "Crear",
"Clear Data": "Esborra dades",
"Events": "Events",
"Heartbeats": "Heartbeat",
"Auto Get": "Obtenir automàticament",
"Schedule maintenance": "Programar manteniment",
"Affected Monitors": "Monitors Afectats",
"Pick Affected Monitors...": "Seleccionar Monitors Afectats…",
"Start of maintenance": "Inici del manteniment",
"All Status Pages": "Totes les pàgines d'estat",
"Select status pages...": "Selecciona pàgines d'estat…",
"alertNoFile": "Per favor, selecciona fitxer a importar.",
"alertWrongFileType": "Selecciona un fitxer JSON.",
"Clear all statistics": "Esborra totes les Estadístiques",
"Skip existing": "Ometre existent",
"Overwrite": "Sobreescriu",
"Options": "Opcions",
"Keep both": "Manté ambdos",
"Verify Token": "Verificar token",
"Setup 2FA": "Configurar 2FA",
"Enable 2FA": "Habilitar 2FA",
"Disable 2FA": "Deshabilitar 2FA",
"2FA Settings": "Ajustaments 2FA",
"filterActive": "Actiu",
"filterActivePaused": "Pausat",
"Inactive": "Inactiu",
"Token": "Token",
"Show URI": "Mostrar URI",
"Tags": "Etiquetes",
"Red": "Vermell",
"Orange": "Taronja",
"Green": "Verd",
"Blue": "Blau",
"Indigo": "Morat",
"Purple": "Porpra",
"Pink": "Rosa",
"Avg. Ping": "Ping promig",
"Entry Page": "Pàgina d'entrada",
"statusPageNothing": "Res per aquí, per favor afegeix un grup o un monitor.",
"statusPageRefreshIn": "Refrescat en: {0]",
"No Services": "Sense Servei",
"All Systems Operational": "Tots els sistemes operatius",
"Edit Status Page": "Editar pàgina Estat",
"Go to Dashboard": "Anar al Panell",
"Status Page": "Pàgina d'Estat",
"Email": "Correu",
"Last Result": "Darrer Resultat",
"Add New Tag": "Afegir nova etiqueta",
"Tag with this value already exist.": "Ja existeix una etiqueta amb aquest valor."
}

@ -387,7 +387,6 @@
"Discard": "Zahodit",
"Cancel": "Zrušit",
"Powered by": "Poskytuje",
"shrinkDatabaseDescription": "Pomocí této možnosti provedete příkaz VACUUM nad SQLite databází. Pokud byla databáze vytvořena po vydání verze 1.10.0, AUTO_VACUUM je již povolena a tato akce není vyžadována.",
"serwersms": "SerwerSMS.pl",
"serwersmsAPIUser": "API uživatelské jméno (včetně předpony webapi_)",
"serwersmsAPIPassword": "API heslo",
@ -823,7 +822,6 @@
"Enable Kafka Producer Auto Topic Creation": "Povolit Kafka zprostředkovateli automatické vytváření vláken",
"Kafka Producer Message": "Zpráva Kafka zprostředkovatele",
"tailscalePingWarning": "Abyste mohli používat Tailscale Ping monitor, je nutné Uptime Kuma nainstalovat mimo Docker, a dále na váš server nainstalovat Tailscale klienta.",
"jsonQueryDescription": "Proveďte JSON dotaz vůči odpovědi a zkontrolujte očekávaný výstup (za účelem porovnání bude návratová hodnota převedena na řetězec). Dokumentaci k dotazovacímu jazyku naleznete na {0}, a využít můžete též {1}.",
"Select": "Vybrat",
"selectedMonitorCount": "Vybráno: {0}",
"Check/Uncheck": "Vybrat/Zrušit výběr",
@ -833,16 +831,16 @@
"nostrRelays": "Relé Nostr",
"FlashDuty Severity": "Závažnost",
"PushDeer Server": "Server PushDeer",
"wayToGetFlashDutyKey": "Můžete přejít na stránku Kanál -> (Vyberte kanál) -> Integrace -> Přidat novou integraci, přidat \"Vlastní událost\" a získat adresu pro odeslání, zkopírovat klíč integrace do adresy. Další informace naleznete na adrese",
"wayToGetFlashDutyKey": "Můžete přejít na stránku \"Kanál -> (Vyberte kanál) -> Integrace -> Přidat novou integraci\", přidat \"Uptime Kuma\" a získat push adresu, zkopírovat integrační klíč v adrese. Další informace naleznete na adrese",
"Request Timeout": "Časový limit požadavku",
"timeoutAfter": "Vypršení časového limitu po {0} sekundách",
"styleElapsedTime": "Čas uplynulý pod heartbeat ukazatelem",
"styleElapsedTimeShowWithLine": "Zobrazit (s linkou)",
"gamedigGuessPortDescription": "Port používaný protokolem Valve Server Query Protocol se může lišit od portu klienta. Pokud se monitor nemůže připojit k serveru, zkuste to.",
"styleElapsedTimeShowNoLine": "Zobrazit (bez linky)",
"gamedigGuessPort": "Gamedig: Guess Port",
"gamedigGuessPort": "Gamedig: Port",
"Saved.": "Uloženo.",
"setupDatabaseChooseDatabase": "Kterou databázi chcete použít?",
"setupDatabaseChooseDatabase": "Kterou databázi byste chtěli používat?",
"setupDatabaseEmbeddedMariaDB": "Nemusíte nic nastavovat. Tento Docker obraz pro vás automaticky vložil a nakonfiguroval databázi MariaDB. Uptime Kuma se k této databázi připojí prostřednictvím unixového socketu.",
"setupDatabaseMariaDB": "Připojení k externí databázi MariaDB. Je třeba nastavit informace o připojení k databázi.",
"setupDatabaseSQLite": "Jednoduchý databázový soubor, doporučený pro malé instalace. Před verzí 2.0.0 používal Uptime Kuma jako výchozí databázi SQLite.",
@ -895,9 +893,9 @@
"emailTemplateLimitedToUpDownNotification": "dostupné pouze pro heatbeaty BĚŽÍ/NEBĚŽÍ, jinak null",
"templateMonitorJSON": "objekt popisující dohled",
"templateLimitedToUpDownNotifications": "dostupné pouze pro oznámení BĚŽÍ/NEBĚŽÍ",
"successKeyword": "Nalezení klíčového slova",
"Search monitored sites": "Vyhledávání dohledů",
"settingUpDatabaseMSG": "Vytvářím strukturu databáze. Může to chvíli trvat, buďte trpěliví.",
"successKeyword": "Úspěch Klíčové slovo",
"Search monitored sites": "Hledat v monitorovaných umístěních",
"settingUpDatabaseMSG": "Nastavuje se databáze. Prosím buďte trpělivý, může to chvíli trvat.",
"successKeywordExplanation": "Klíčové slovo MQTT, které bude považováno za úspěch",
"Browser Screenshot": "Snímek obrazovky prohlížeče",
"setup a new monitor group": "nastavení nové skupiny dohledů",
@ -944,7 +942,7 @@
"remoteBrowserToggle": "Ve výchozím nastavení běží Chromium uvnitř kontejneru Uptime Kuma. Přepnutím tohoto přepínače můžete použít vzdálený prohlížeč.",
"wayToGetWhapiUrlAndToken": "URL rozhraní API a token získáte tak, že přejdete do požadovaného kanálu z {0}",
"wayToWriteWhapiRecipient": "Telefonní číslo s mezinárodní předvolbou, ale bez znaménka plus na začátku ({0}), ID kontaktu ({1}) nebo ID skupiny ({2}).",
"remoteBrowsersDescription": "Vzdálené prohlížeče jsou alternativou k lokálně spuštěnému Chromu. Nastavte tuto možnost pomocí služby jako je browserless.io, nebo se připojte k vlastnímu vzdálenému prohlížeči",
"remoteBrowsersDescription": "Vzdálené prohlížeče jsou alternativou k místnímu spuštění Chromu. Nastavte se pomocí služby, jako je browserless.io, nebo se připojte k vlastnímu",
"statusPageSpecialSlugDesc": "Speciální slug {0}: tato stránka se zobrazí, pokud není definován žádný slug",
"Mentioning": "Zmínky",
"wayToGetSevenIOApiKey": "Navštivte ovládací panel v části app.seven.io > developer > api key > zelené tlačítko přidat",
@ -953,5 +951,65 @@
"apiKeySevenIO": "API klíč SevenIO",
"locally configured mail transfer agent": "místně nakonfigurovaný agent pro přenos pošty",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Zadejte název hostitele serveru, ke kterému se chcete připojit, nebo {localhost}, pokud hodláte použít {local_mta}",
"receiverInfoSevenIO": "Pokud se přijímající číslo nenachází v Německu, musíte před číslo přidat kód země (např. pro kód země 420 z ČR použijte 420603603603 místo 603603603)"
"receiverInfoSevenIO": "Pokud se přijímající číslo nenachází v Německu, musíte před číslo přidat kód země (např. pro kód země 420 z ČR použijte 420603603603 místo 603603603)",
"Command": "Příkaz",
"mongodbCommandDescription": "Spusťte příkaz MongoDB proti databázi. Informace o dostupných příkazech najdete v {dokumentaci}",
"ignoreTLSErrorGeneral": "Ignorování chyby TLS/SSL u připojení",
"smspartnerApiurl": "Svůj klíč API najdete v ovládacím panelu na adrese {0}",
"smspartnerPhoneNumber": "Telefonní číslo (čísla)",
"smspartnerPhoneNumberHelptext": "Číslo musí být v mezinárodním formátu {0}, {1}. Více čísel musí být odděleno znakem {2}",
"smspartnerSenderName": "Název odesílatele SMS",
"smspartnerSenderNameInfo": "Musí obsahovat 3..=11 běžných znaků",
"Bitrix24 Webhook URL": "Adresa URL webhooku služby Bitrix24",
"wayToGetBitrix24Webhook": "Webhook můžete vytvořit podle pokynů na adrese {0}",
"bitrix24SupportUserID": "Zadejte své uživatelské ID v Bitrix24. ID zjistíte z odkazu, který najdete v profilu uživatele.",
"Refresh Interval": "Interval obnovení",
"Refresh Interval Description": "Stavová stránka provede úplnou obnovu webu každých {0} sekund",
"Select message type": "Zvolte typ zprávy",
"Send to channel": "Poslat na kanál",
"Create new forum post": "Vytvořit nový příspěvek na fóru",
"postToExistingThread": "Příspěvek do existujícího vlákna / příspěvek na fóru",
"forumPostName": "Název příspěvku ve fóru",
"threadForumPostID": "Vlákno / ID příspěvku ve fóru",
"e.g. {discordThreadID}": "např. {discordThreadID}",
"whatHappensAtForumPost": "Vytvořte nový příspěvek ve fóru. Tímto způsobem NENÍ možné odesílat zprávy v existujících příspěvcích. Pro vložení příspěvku do existujícího příspěvku použijte \"{option}\"",
"wayToGetDiscordThreadId": "Získání ID vlákna / příspěvku ve fóru je podobné získání ID kanálu. Přečtěte si více o tom, jak získat id {0}",
"Don't mention people": "Nezmiňujte se o osobách",
"Mention group": "Zmínka o {skupině}",
"Host URL": "URL hosta",
"threemaRecipientType": "Typ příjemce",
"threemaRecipient": "Příjemce",
"threemaSenderIdentityFormat": "8 znaků, obvykle začíná na *",
"threemaRecipientTypeEmail": "Emailová adresa",
"threemaSenderIdentity": "ID brány",
"threemaApiAuthenticationSecret": "Tajný klíč ID brány",
"apiKeysDisabledMsg": "Klíče API jsou zakázány, protože je zakázáno ověřování.",
"threemaBasicModeInfo": "Poznámka: Tato integrace využívá bránu Threema v základním režimu (šifrování pomocí serveru). Další podrobnosti naleznete {0}.",
"threemaRecipientTypeIdentityFormat": "8 znaků",
"threemaRecipientTypeIdentity": "Threema-ID",
"threemaRecipientTypePhone": "Telefonní číslo",
"wayToGetThreemaGateway": "Můžete se zaregistrovat do služby brány Threema {0}.",
"privateOnesenderDesc": "Zkontrolujte, zda je telefonní číslo platné. Odeslání zprávy na soukromé telefonní číslo, např.: 628123456789",
"wayToGetOnesenderUrlandToken": "Adresu URL a token získáte na webových stránkách společnosti Onesender. Více informací {0}",
"now": "nyní",
"time ago": "před {0}",
"-year": "-rok",
"Json Query Expression": "Výraz dotazu JSON",
"and": "a",
"cacheBusterParam": "Přidání parametru {0}",
"cacheBusterParamDescription": "Náhodně generovaný parametr pro vynechání mezipaměti.",
"OID (Object Identifier)": "OID (identifikátor objektu)",
"Condition": "Stav",
"SNMP Version": "Verze SNMP",
"Please enter a valid OID.": "Zadejte prosím platný identifikátor OID.",
"Recipient Type": "Typ příjemce",
"Private Number": "Soukromé číslo",
"Group ID": "ID skupiny",
"Add Remote Browser": "Přidat vzdálený prohlížeč",
"New Group": "Nová skupina",
"Group Name": "Název skupiny",
"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"
}

@ -345,7 +345,6 @@
"Discard": "Kassér",
"Cancel": "Annullér",
"Powered by": "Drevet af",
"shrinkDatabaseDescription": "Udfør database VACUUM for SQLite. Hvis din database er oprettet efter 1.10.0, er AUTO_VACUUM allerede aktiveret, og denne handling er ikke nødvendig.",
"serwersms": "SerwerSMS.pl",
"serwersmsAPIUser": "API Brugernavn (inkl. webapi_ prefix)",
"serwersmsAPIPassword": "API Adgangskode",

@ -355,7 +355,6 @@
"Discard": "Verwerfen",
"Cancel": "Abbrechen",
"Powered by": "Erstellt mit",
"shrinkDatabaseDescription": "Löse VACUUM für die SQLite Datenbank aus. Wenn die Datenbank nach 1.10.0 erstellt wurde, ist AUTO_VACUUM bereits aktiviert und diese Aktion ist nicht erforderlich.",
"serwersms": "SerwerSMS.pl",
"serwersmsAPIUser": "API Benutzername (inkl. webapi_ prefix)",
"serwersmsAPIPassword": "API Passwort",
@ -812,7 +811,6 @@
"Json Query": "Json-Abfrage",
"filterActive": "Aktiv",
"filterActivePaused": "Pausiert",
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf {0} findest du die Dokumentation zur Abfragesprache. {1} kannst du Abfragen üben.",
"Badge Duration (in hours)": "Abzeichen Dauer (in Stunden)",
"Badge Preview": "Abzeichen Vorschau",
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
@ -970,5 +968,84 @@
"ignoreTLSErrorGeneral": "TLS/SSL-Fehler für Verbindung ignorieren",
"threadForumPostID": "Themen-/Forumbeitrags-ID",
"e.g. {discordThreadID}": "z.B. {discordThreadID}",
"wayToGetDiscordThreadId": "Das Abrufen einer Thread-/Forumspost-ID ist ähnlich wie das Abrufen einer Channel-ID. Lese mehr darüber, wie man IDs abruft {0}"
"wayToGetDiscordThreadId": "Das Abrufen einer Thread-/Forumspost-ID ist ähnlich wie das Abrufen einer Channel-ID. Lese mehr darüber, wie man IDs abruft {0}",
"smspartnerApiurl": "Den API-Schlüssel findest du im Dashboard unter {0}",
"smspartnerPhoneNumber": "Telefonnummer(n)",
"smspartnerSenderName": "SMS Absender Name",
"smspartnerSenderNameInfo": "Muss zwischen 3..=11 regulären Zeichen sein",
"smspartnerPhoneNumberHelptext": "Die Nummer muss das internationale Format {0}, {1} haben. Mehrere Nummern müssen durch {2} getrennt werden",
"threemaRecipient": "Empfänger",
"threemaRecipientType": "Empfänger Typ",
"threemaRecipientTypeIdentity": "Threema-ID",
"threemaRecipientTypePhone": "Telefonnummer",
"threemaRecipientTypePhoneFormat": "E.164, ohne führendes +",
"threemaRecipientTypeEmail": "E-Mail Adresse",
"threemaSenderIdentity": "Gateway-ID",
"threemaApiAuthenticationSecret": "Gateway-ID Schlüssel",
"wayToGetThreemaGateway": "Du kannst dich für Threema Gateway {0} registrieren.",
"threemaRecipientTypeIdentityFormat": "8 Zeichen",
"threemaSenderIdentityFormat": "8 Zeichen, beginnt normalerweise mit *",
"threemaBasicModeInfo": "Hinweis: Diese Integration verwendet Threema Gateway im Basismodus (serverbasierte Verschlüsselung). Weitere Details siehe {0}.",
"apiKeysDisabledMsg": "API-Schlüssel sind deaktiviert, da die Authentifizierung deaktiviert ist.",
"Json Query Expression": "Json Query Ausdrck",
"Cannot connect to the socket server.": "Es kann keine Verbindung zum Socket-Server hergestellt werden.",
"not ends with": "endet nicht mit",
"signl4Docs": "Weitere Informationen zur Konfiguration von SIGNL4 und zum Abrufen der SIGNL4-Webhook-URL siehe {0}.",
"now": "jetzt",
"time ago": "vor {0}",
"-year": "-Jahr",
"and": "und",
"jsonQueryDescription": "Parsen und Extrahieren spezifischer Daten aus der JSON-Antwort des Servers mittels JSON-Abfrage oder Verwendung von \"$\" für die rohe Antwort, wenn kein JSON erwartet wird. Das Ergebnis wird dann mit dem erwarteten Wert in Form von Strings verglichen. Siehe {0} für die Dokumentation und verwende {1}, um mit Abfragen zu experimentieren.",
"cacheBusterParamDescription": "Zufällig generierter Parameter um den Cache zu umgehen.",
"cacheBusterParam": "Den Parameter {0} hinzufügen",
"Community String": "Gemeinschaftliche Zeichenkette",
"snmpCommunityStringHelptext": "Diese Zeichenfolge dient als Passwort zur Authentifizierung und Kontrolle des Zugriffs auf SNMP-fähigen Geräten. Pass sie an die Konfiguration des SNMP-Geräts an.",
"OID (Object Identifier)": "OID (Objekt-Identifikator)",
"Condition": "Bedingung",
"SNMP Version": "SNMP Version",
"Please enter a valid OID.": "Gib eine gültige OID ein.",
"Host Onesender": "Host Onesender",
"Token Onesender": "Token Onesender",
"Recipient Type": "Empfänger Typ",
"Private Number": "Private Nummer",
"Group ID": "Gruppen ID",
"wayToGetOnesenderUrlandToken": "Du kannst die URL und den Token auf der Onesender-Website erhalten. Weitere Infos {0}",
"Add Remote Browser": "Remote-Browser hinzufügen",
"New Group": "Neue Gruppe",
"Group Name": "Gruppenname",
"OAuth2: Client Credentials": "OAuth2: Client-Anmeldeinformationen",
"snmpOIDHelptext": "Gib die OID für den zu überwachenden Sensor oder Status ein. Verwende Netzwerkverwaltungstools wie MIB-Browser oder SNMP-Software, wenn du bezüglich OID unsicher bist.",
"privateOnesenderDesc": "Stell sicher, dass die Telefonnummer gültig ist. Um Nachrichten an private Telefonnummer zu senden, z. B.: 628123456789",
"groupOnesenderDesc": "Stell sicher, dass die GroupID gültig ist. Um Nachricht an die Gruppe zu senden, z.B.: 628123456789-342345",
"Authentication Method": "Authentifizierungsmethode",
"Authorization Header": "Autorisierungs-Header",
"Form Data Body": "Formular Data Body",
"OAuth Token URL": "OAuth Token URL",
"Client ID": "Client ID",
"Client Secret": "Client Secret",
"OAuth Scope": "OAuth Scope",
"Optional: Space separated list of scopes": "Optional: Durch Leerzeichen getrennte Liste der Scopes",
"Go back to home page.": "Zurück zur Startseite.",
"No tags found.": "Keine Tags gefunden.",
"Lost connection to the socket server.": "Verbindung zum Socket-Server verloren.",
"SIGNL4": "SIGNL4",
"SIGNL4 Webhook URL": "SIGNL4 Webhook URL",
"Conditions": "Bedingungen",
"conditionAdd": "Bedingung hinzufügen",
"conditionDelete": "Bedingung löschen",
"conditionAddGroup": "Gruppe hinzufügen",
"conditionDeleteGroup": "Gruppe löschen",
"conditionValuePlaceholder": "Wert",
"equals": "ist gleich",
"not equals": "ist nicht gleich",
"contains": "enthält",
"not contains": "enthält nicht",
"starts with": "beginnt mit",
"not starts with": "beginnt nicht mit",
"ends with": "endet mit",
"less than": "weniger als",
"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"
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save