mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-11 13:36:55 +08:00
Compare commits
3 Commits
escaped-de
...
extracted-
Author | SHA1 | Date | |
---|---|---|---|
|
2bd51a3145 | ||
|
6c49b53d6a | ||
|
334e37eaa0 |
28
.devcontainer/README.md
Normal file
28
.devcontainer/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Codespaces
|
||||
|
||||
You can modifiy Uptime Kuma in your browser without setting up a local development.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
## 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`
|
||||
|
||||

|
23
.devcontainer/devcontainer.json
Normal file
23
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"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]
|
||||
}
|
@@ -17,6 +17,7 @@ README.md
|
||||
.vscode
|
||||
.eslint*
|
||||
.stylelint*
|
||||
/.devcontainer
|
||||
/.github
|
||||
yarn.lock
|
||||
app.json
|
||||
|
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
ignorePatterns: [
|
||||
"test/*.js",
|
||||
"server/modules/*",
|
||||
"server/modules/apicache/*",
|
||||
"src/util.js"
|
||||
],
|
||||
root: true,
|
||||
|
8
.github/workflows/auto-test.yml
vendored
8
.github/workflows/auto-test.yml
vendored
@@ -15,14 +15,14 @@ on:
|
||||
|
||||
jobs:
|
||||
auto-test:
|
||||
needs: [ check-linters ]
|
||||
needs: [ check-linters, e2e-test ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
|
||||
node: [ 18, 20 ]
|
||||
node: [ 18, 20.5 ]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
|
||||
armv7-simple-test:
|
||||
needs: [ ]
|
||||
needs: [ check-linters ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
if: ${{ github.repository == 'louislam/uptime-kuma' }}
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
- run: npm run lint:prod
|
||||
|
||||
e2e-test:
|
||||
needs: [ ]
|
||||
needs: [ check-linters ]
|
||||
runs-on: ARM64
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
|
4
.github/workflows/stale-bot.yml
vendored
4
.github/workflows/stale-bot.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v8
|
||||
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@v9
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
stale-issue-message: |-
|
||||
This issue was marked as `cannot-reproduce` by a maintainer.
|
||||
|
@@ -127,7 +127,7 @@ Different guidelines exist for different types of pull requests (PRs):
|
||||
- `server/monitor-types/MONITORING_TYPE.js` is the core of each monitor.
|
||||
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 successful message and set `heartbeat.status = UP`
|
||||
- in the happy-path, you should set `heartbeat.msg` to a successfull 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.
|
||||
@@ -236,6 +236,12 @@ 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.
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const port = 30001;
|
||||
export const url = `http://localhost:${port}`;
|
||||
const url = `http://localhost:${port}`;
|
||||
|
||||
export default defineConfig({
|
||||
// Look for test files in the "tests" directory, relative to this configuration file.
|
||||
testDir: "../test/e2e/specs",
|
||||
testDir: "../test/e2e",
|
||||
outputDir: "../private/playwright-test-results",
|
||||
fullyParallel: false,
|
||||
locale: "en-US",
|
||||
@@ -40,15 +40,9 @@ export default defineConfig({
|
||||
// Configure projects for major browsers.
|
||||
projects: [
|
||||
{
|
||||
name: "run-once setup",
|
||||
testMatch: /setup-process\.once\.js/,
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
{
|
||||
name: "specs",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
dependencies: [ "run-once setup" ],
|
||||
},
|
||||
/*
|
||||
{
|
||||
name: "firefox",
|
||||
@@ -58,7 +52,7 @@ export default defineConfig({
|
||||
|
||||
// Run your local dev server before starting the tests.
|
||||
webServer: {
|
||||
command: `node extra/remove-playwright-test-data.js && cross-env NODE_ENV=development node server/server.js --port=${port} --data-dir=./data/playwright-test`,
|
||||
command: `node extra/remove-playwright-test-data.js && node server/server.js --port=${port} --data-dir=./data/playwright-test`,
|
||||
url,
|
||||
reuseExistingServer: false,
|
||||
cwd: "../",
|
||||
|
@@ -16,7 +16,9 @@ export default defineConfig({
|
||||
},
|
||||
define: {
|
||||
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
|
||||
"process.env": {},
|
||||
"DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER),
|
||||
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),
|
||||
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
|
@@ -1,16 +0,0 @@
|
||||
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");
|
||||
});
|
||||
};
|
@@ -1,13 +0,0 @@
|
||||
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");
|
||||
});
|
||||
};
|
@@ -1,12 +0,0 @@
|
||||
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,6 +3,7 @@ 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?)
|
||||
@@ -11,10 +12,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 apt update && \
|
||||
apt --yes --no-install-recommends install \
|
||||
sqlite3 \
|
||||
ca-certificates \
|
||||
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 \
|
||||
iputils-ping \
|
||||
util-linux \
|
||||
dumb-init \
|
||||
@@ -24,15 +25,6 @@ RUN apt update && \
|
||||
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 && \
|
||||
@@ -50,9 +42,7 @@ COPY ./docker/etc/sudoers /etc/sudoers
|
||||
|
||||
# Full Base Image
|
||||
# MariaDB, Chromium and fonts
|
||||
# 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
|
||||
FROM 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,6 +4,7 @@ const tar = require("tar");
|
||||
|
||||
const packageJSON = require("../package.json");
|
||||
const fs = require("fs");
|
||||
const rmSync = require("./fs-rmSync.js");
|
||||
const version = packageJSON.version;
|
||||
|
||||
const filename = "dist.tar.gz";
|
||||
@@ -28,9 +29,8 @@ function download(url) {
|
||||
if (fs.existsSync("./dist")) {
|
||||
|
||||
if (fs.existsSync("./dist-backup")) {
|
||||
fs.rmSync("./dist-backup", {
|
||||
recursive: true,
|
||||
force: true,
|
||||
rmSync("./dist-backup", {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,9 +43,8 @@ function download(url) {
|
||||
|
||||
tarStream.on("close", () => {
|
||||
if (fs.existsSync("./dist-backup")) {
|
||||
fs.rmSync("./dist-backup", {
|
||||
recursive: true,
|
||||
force: true,
|
||||
rmSync("./dist-backup", {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
console.log("Done");
|
||||
|
23
extra/fs-rmSync.js
Normal file
23
extra/fs-rmSync.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const fs = require("fs");
|
||||
/**
|
||||
* Detect if `fs.rmSync` is available
|
||||
* to avoid the runtime deprecation warning triggered for using `fs.rmdirSync` with `{ recursive: true }` in Node.js v16,
|
||||
* or the `recursive` property removing completely in the future Node.js version.
|
||||
* See the link below.
|
||||
* @todo Once we drop the support for Node.js v14 (or at least versions before v14.14.0), we can safely replace this function with `fs.rmSync`, since `fs.rmSync` was add in Node.js v14.14.0 and currently we supports all the Node.js v14 versions that include the versions before the v14.14.0, and this function have almost the same signature with `fs.rmSync`.
|
||||
* @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true- the deprecation information of `fs.rmdirSync`
|
||||
* @link https://nodejs.org/docs/latest-v16.x/api/fs.html#fsrmsyncpath-options the document of `fs.rmSync`
|
||||
* @param {fs.PathLike} path Valid types for path values in "fs".
|
||||
* @param {fs.RmDirOptions} options options for `fs.rmdirSync`, if `fs.rmSync` is available and property `recursive` is true, it will automatically have property `force` with value `true`.
|
||||
* @returns {void}
|
||||
*/
|
||||
const rmSync = (path, options) => {
|
||||
if (typeof fs.rmSync === "function") {
|
||||
if (options.recursive) {
|
||||
options.force = true;
|
||||
}
|
||||
return fs.rmSync(path, options);
|
||||
}
|
||||
return fs.rmdirSync(path, options);
|
||||
};
|
||||
module.exports = rmSync;
|
@@ -2,6 +2,7 @@
|
||||
|
||||
import fs from "fs";
|
||||
import util from "util";
|
||||
import rmSync from "../fs-rmSync.js";
|
||||
|
||||
/**
|
||||
* Copy across the required language files
|
||||
@@ -15,10 +16,7 @@ import util from "util";
|
||||
*/
|
||||
function copyFiles(langCode, baseLang) {
|
||||
if (fs.existsSync("./languages")) {
|
||||
fs.rmSync("./languages", {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
rmSync("./languages", { recursive: true });
|
||||
}
|
||||
fs.mkdirSync("./languages");
|
||||
|
||||
@@ -95,9 +93,6 @@ console.log("Updating: " + langCode);
|
||||
|
||||
copyFiles(langCode, baseLangCode);
|
||||
await updateLanguage(langCode, baseLangCode);
|
||||
fs.rmSync("./languages", {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
rmSync("./languages", { recursive: true });
|
||||
|
||||
console.log("Done. Fixing formatting by ESLint...");
|
||||
|
12450
package-lock.json
generated
12450
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -27,7 +27,9 @@
|
||||
"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": "cross-env TEST_BACKEND=1 node --test test/backend-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-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",
|
||||
@@ -47,7 +49,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.14 && npm ci --production && npm run download-dist",
|
||||
"setup": "git checkout 1.23.13 && 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",
|
||||
@@ -68,15 +70,16 @@
|
||||
"sort-contributors": "node extra/sort-contributors.js",
|
||||
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
|
||||
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
|
||||
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X"
|
||||
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
|
||||
"start-server-node14-win": "private\\node14\\node.exe server/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "~1.8.22",
|
||||
"@grpc/grpc-js": "~1.7.3",
|
||||
"@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",
|
||||
@@ -86,14 +89,12 @@
|
||||
"command-exists": "~1.2.9",
|
||||
"compare-versions": "~3.6.0",
|
||||
"compression": "~1.7.4",
|
||||
"croner": "~8.1.0",
|
||||
"croner": "~6.0.5",
|
||||
"dayjs": "~1.11.5",
|
||||
"dev-null": "^0.1.1",
|
||||
"dotenv": "~16.0.3",
|
||||
"express": "~4.21.0",
|
||||
"express": "~4.19.2",
|
||||
"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",
|
||||
@@ -111,14 +112,12 @@
|
||||
"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": "~11.0.0",
|
||||
"mssql": "~8.1.4",
|
||||
"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",
|
||||
@@ -137,9 +136,10 @@
|
||||
"redbean-node": "~0.3.0",
|
||||
"redis": "~4.5.1",
|
||||
"semver": "~7.5.4",
|
||||
"socket.io": "~4.8.0",
|
||||
"socket.io-client": "~4.8.0",
|
||||
"socket.io": "~4.6.1",
|
||||
"socket.io-client": "~4.6.1",
|
||||
"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,12 +171,13 @@
|
||||
"cross-env": "~7.0.3",
|
||||
"delay": "^5.0.0",
|
||||
"dns2": "~2.0.1",
|
||||
"dompurify": "~3.1.7",
|
||||
"dompurify": "~3.0.11",
|
||||
"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,32 +213,6 @@ 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,
|
||||
@@ -248,5 +222,4 @@ module.exports = {
|
||||
sendInfo,
|
||||
sendDockerHostList,
|
||||
sendRemoteBrowserList,
|
||||
sendMonitorTypeList,
|
||||
};
|
||||
|
@@ -223,11 +223,8 @@ 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: Dialect,
|
||||
client: "sqlite3",
|
||||
connection: {
|
||||
filename: Database.sqlitePath,
|
||||
acquireConnectionTimeout: acquireConnectionTimeout,
|
||||
|
@@ -239,7 +239,19 @@ class Maintenance extends BeanModel {
|
||||
this.beanMeta.status = "under-maintenance";
|
||||
clearTimeout(this.beanMeta.durationTimeout);
|
||||
|
||||
let duration = this.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
|
||||
let duration;
|
||||
|
||||
if (customDuration > 0) {
|
||||
duration = customDuration;
|
||||
} else if (this.end_date) {
|
||||
let d = dayjs(this.end_date).diff(dayjs(), "second");
|
||||
if (d < this.duration) {
|
||||
duration = d * 1000;
|
||||
}
|
||||
} else {
|
||||
duration = this.duration * 1000;
|
||||
}
|
||||
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
|
||||
@@ -251,21 +263,9 @@ class Maintenance extends BeanModel {
|
||||
};
|
||||
|
||||
// Create Cron
|
||||
if (this.strategy === "recurring-interval") {
|
||||
// For recurring-interval, Croner needs to have interval and startAt
|
||||
const startDate = dayjs(this.startDate);
|
||||
const [ hour, minute ] = this.startTime.split(":");
|
||||
const startDateTime = startDate.hour(hour).minute(minute);
|
||||
this.beanMeta.job = new Cron(this.cron, {
|
||||
timezone: await this.getTimezone(),
|
||||
interval: this.interval_day * 24 * 60 * 60,
|
||||
startAt: startDateTime.toISOString(),
|
||||
}, startEvent);
|
||||
} else {
|
||||
this.beanMeta.job = new Cron(this.cron, {
|
||||
timezone: await this.getTimezone(),
|
||||
}, startEvent);
|
||||
}
|
||||
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,24 +311,6 @@ 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}
|
||||
@@ -413,8 +395,10 @@ class Maintenance extends BeanModel {
|
||||
} else if (!this.strategy.startsWith("recurring-")) {
|
||||
this.cron = "";
|
||||
} else if (this.strategy === "recurring-interval") {
|
||||
// For intervals, the pattern is calculated in the run function as the interval-option is set
|
||||
this.cron = "* * * * *";
|
||||
let array = this.start_time.split(":");
|
||||
let hour = parseInt(array[0]);
|
||||
let minute = parseInt(array[1]);
|
||||
this.cron = minute + " " + hour + " */" + this.interval_day + " * *";
|
||||
this.duration = this.calcDuration();
|
||||
log.debug("maintenance", "Cron: " + this.cron);
|
||||
log.debug("maintenance", "Duration: " + this.duration);
|
||||
|
@@ -2,9 +2,9 @@ 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, evaluateJsonQuery
|
||||
SQL_DATETIME_FORMAT
|
||||
} = require("../../src/util");
|
||||
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
|
||||
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius,
|
||||
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
||||
} = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
@@ -17,6 +17,7 @@ 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");
|
||||
@@ -71,12 +72,23 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @param {object} preloadData to prevent n+1 problems, we query the data in a batch outside of this function
|
||||
* @param {boolean} includeSensitiveData Include sensitive data in
|
||||
* JSON
|
||||
* @returns {object} Object ready to parse
|
||||
* @returns {Promise<object>} Object ready to parse
|
||||
*/
|
||||
toJSON(preloadData = {}, includeSensitiveData = true) {
|
||||
async toJSON(includeSensitiveData = true) {
|
||||
|
||||
let notificationIDList = {};
|
||||
|
||||
let list = await R.find("monitor_notification", " monitor_id = ? ", [
|
||||
this.id,
|
||||
]);
|
||||
|
||||
for (let bean of list) {
|
||||
notificationIDList[bean.notification_id] = true;
|
||||
}
|
||||
|
||||
const tags = await this.getTags();
|
||||
|
||||
let screenshot = null;
|
||||
|
||||
@@ -84,7 +96,7 @@ class Monitor extends BeanModel {
|
||||
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
|
||||
}
|
||||
|
||||
const path = preloadData.paths.get(this.id) || [];
|
||||
const path = await this.getPath();
|
||||
const pathName = path.join(" / ");
|
||||
|
||||
let data = {
|
||||
@@ -94,15 +106,15 @@ class Monitor extends BeanModel {
|
||||
path,
|
||||
pathName,
|
||||
parent: this.parent,
|
||||
childrenIDs: preloadData.childrenIDs.get(this.id) || [],
|
||||
childrenIDs: await Monitor.getAllChildrenIDs(this.id),
|
||||
url: this.url,
|
||||
method: this.method,
|
||||
hostname: this.hostname,
|
||||
port: this.port,
|
||||
maxretries: this.maxretries,
|
||||
weight: this.weight,
|
||||
active: preloadData.activeStatus.get(this.id),
|
||||
forceInactive: preloadData.forceInactive.get(this.id),
|
||||
active: await this.isActive(),
|
||||
forceInactive: !await Monitor.isParentActive(this.id),
|
||||
type: this.type,
|
||||
timeout: this.timeout,
|
||||
interval: this.interval,
|
||||
@@ -122,9 +134,9 @@ class Monitor extends BeanModel {
|
||||
docker_container: this.docker_container,
|
||||
docker_host: this.docker_host,
|
||||
proxyId: this.proxy_id,
|
||||
notificationIDList: preloadData.notifications.get(this.id) || {},
|
||||
tags: preloadData.tags.get(this.id) || [],
|
||||
maintenance: preloadData.maintenanceStatus.get(this.id),
|
||||
notificationIDList,
|
||||
tags: tags,
|
||||
maintenance: await Monitor.isUnderMaintenance(this.id),
|
||||
mqttTopic: this.mqttTopic,
|
||||
mqttSuccessMessage: this.mqttSuccessMessage,
|
||||
mqttCheckType: this.mqttCheckType,
|
||||
@@ -148,12 +160,7 @@ 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) {
|
||||
@@ -190,6 +197,16 @@ class Monitor extends BeanModel {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the monitor is active based on itself and its parents
|
||||
* @returns {Promise<boolean>} Is the monitor active?
|
||||
*/
|
||||
async isActive() {
|
||||
const parentActive = await Monitor.isParentActive(this.id);
|
||||
|
||||
return (this.active === 1) && parentActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags applied to this monitor
|
||||
* @returns {Promise<LooseObject<any>[]>} List of tags on the
|
||||
@@ -276,14 +293,6 @@ 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
|
||||
@@ -489,14 +498,6 @@ 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);
|
||||
|
||||
@@ -597,15 +598,25 @@ class Monitor extends BeanModel {
|
||||
} else if (this.type === "json-query") {
|
||||
let data = res.data;
|
||||
|
||||
const { status, response } = await evaluateJsonQuery(data, this.jsonPath, this.jsonPathOperator, this.expectedValue);
|
||||
|
||||
if (status) {
|
||||
bean.status = UP;
|
||||
bean.msg = `JSON query passes (comparing ${response} ${this.jsonPathOperator} ${this.expectedValue})`;
|
||||
} else {
|
||||
throw new Error(`JSON query does not pass (comparing ${response} ${this.jsonPathOperator} ${this.expectedValue})`);
|
||||
// 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);
|
||||
|
||||
let result = await expression.evaluate(data);
|
||||
|
||||
if (result.toString() === this.expectedValue) {
|
||||
bean.msg += ", expected value is found";
|
||||
bean.status = UP;
|
||||
} else {
|
||||
throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
|
||||
}
|
||||
}
|
||||
|
||||
} else if (this.type === "port") {
|
||||
@@ -762,37 +773,6 @@ class Monitor extends BeanModel {
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "grpc-keyword") {
|
||||
let startTime = dayjs().valueOf();
|
||||
const options = {
|
||||
grpcUrl: this.grpcUrl,
|
||||
grpcProtobufData: this.grpcProtobuf,
|
||||
grpcServiceName: this.grpcServiceName,
|
||||
grpcEnableTls: this.grpcEnableTls,
|
||||
grpcMethod: this.grpcMethod,
|
||||
grpcBody: this.grpcBody,
|
||||
};
|
||||
const response = await grpcQuery(options);
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
log.debug("monitor:", `gRPC response: ${JSON.stringify(response)}`);
|
||||
let responseData = response.data;
|
||||
if (responseData.length > 50) {
|
||||
responseData = responseData.toString().substring(0, 47) + "...";
|
||||
}
|
||||
if (response.code !== 1) {
|
||||
bean.status = DOWN;
|
||||
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
|
||||
} else {
|
||||
let keywordFound = response.data.toString().includes(this.keyword);
|
||||
if (keywordFound === !this.isInvertKeyword()) {
|
||||
bean.status = UP;
|
||||
bean.msg = `${responseData}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`;
|
||||
} else {
|
||||
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response.data} + "]"`);
|
||||
bean.status = DOWN;
|
||||
bean.msg = `, but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${responseData} + "]`;
|
||||
}
|
||||
}
|
||||
} else if (this.type === "postgres") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
@@ -886,7 +866,6 @@ class Monitor extends BeanModel {
|
||||
retries = 0;
|
||||
|
||||
} catch (error) {
|
||||
|
||||
if (error?.name === "CanceledError") {
|
||||
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
|
||||
} else {
|
||||
@@ -959,6 +938,7 @@ class Monitor extends BeanModel {
|
||||
} else if (bean.status === MAINTENANCE) {
|
||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
|
||||
} else {
|
||||
beatInterval = this.retryInterval;
|
||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||
}
|
||||
|
||||
@@ -1175,18 +1155,6 @@ class Monitor extends BeanModel {
|
||||
return checkCertificateResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the monitor is active based on itself and its parents
|
||||
* @param {number} monitorID ID of monitor to send
|
||||
* @param {boolean} active is active
|
||||
* @returns {Promise<boolean>} Is the monitor active?
|
||||
*/
|
||||
static async isActive(monitorID, active) {
|
||||
const parentActive = await Monitor.isParentActive(monitorID);
|
||||
|
||||
return (active === 1) && parentActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send statistics to clients
|
||||
* @param {Server} io Socket server instance
|
||||
@@ -1323,10 +1291,7 @@ class Monitor extends BeanModel {
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
const heartbeatJSON = bean.toJSON();
|
||||
const monitorData = [{ id: monitor.id,
|
||||
active: monitor.active
|
||||
}];
|
||||
const preloadData = await Monitor.preparePreloadData(monitorData);
|
||||
|
||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||
if (!heartbeatJSON["msg"]) {
|
||||
heartbeatJSON["msg"] = "N/A";
|
||||
@@ -1337,7 +1302,7 @@ class Monitor extends BeanModel {
|
||||
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
|
||||
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
|
||||
|
||||
await Notification.send(JSON.parse(notification.config), msg, monitor.toJSON(preloadData, false), heartbeatJSON);
|
||||
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
|
||||
} catch (e) {
|
||||
log.error("monitor", "Cannot send notification to " + notification.name);
|
||||
log.error("monitor", e);
|
||||
@@ -1499,111 +1464,6 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets monitor notification of multiple monitor
|
||||
* @param {Array} monitorIDs IDs of monitor to get
|
||||
* @returns {Promise<LooseObject<any>>} object
|
||||
*/
|
||||
static async getMonitorNotification(monitorIDs) {
|
||||
return await R.getAll(`
|
||||
SELECT monitor_notification.monitor_id, monitor_notification.notification_id
|
||||
FROM monitor_notification
|
||||
WHERE monitor_notification.monitor_id IN (?)
|
||||
`, [
|
||||
monitorIDs,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1636,18 +1496,16 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Gets the full path
|
||||
* @param {number} monitorID ID of the monitor to get
|
||||
* @param {string} name of the monitor to get
|
||||
* @returns {Promise<string[]>} Full path (includes groups and the name) of the monitor
|
||||
*/
|
||||
static async getAllPath(monitorID, name) {
|
||||
const path = [ name ];
|
||||
async getPath() {
|
||||
const path = [ this.name ];
|
||||
|
||||
if (this.parent === null) {
|
||||
return path;
|
||||
}
|
||||
|
||||
let parent = await Monitor.getParent(monitorID);
|
||||
let parent = await Monitor.getParent(this.id);
|
||||
while (parent !== null) {
|
||||
path.unshift(parent.name);
|
||||
parent = await Monitor.getParent(parent.id);
|
||||
|
@@ -4,11 +4,6 @@ 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 {
|
||||
|
||||
@@ -18,24 +13,6 @@ 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
|
||||
@@ -61,38 +38,6 @@ 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
|
||||
@@ -101,11 +46,7 @@ class StatusPage extends BeanModel {
|
||||
*/
|
||||
static async renderHTML(indexHTML, statusPage) {
|
||||
const $ = cheerio.load(indexHTML);
|
||||
|
||||
const description155 = marked(statusPage.description ?? "")
|
||||
.replace(/<[^>]+>/gm, "")
|
||||
.trim()
|
||||
.substring(0, 155);
|
||||
const description155 = statusPage.description?.substring(0, 155) ?? "";
|
||||
|
||||
$("title").text(statusPage.title);
|
||||
$("meta[name=description]").attr("content", description155);
|
||||
@@ -152,109 +93,6 @@ 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
|
||||
|
@@ -1,21 +0,0 @@
|
||||
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.
|
@@ -1,77 +0,0 @@
|
||||
'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
|
@@ -1,122 +0,0 @@
|
||||
'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
|
@@ -1,220 +0,0 @@
|
||||
'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
|
@@ -1,127 +0,0 @@
|
||||
"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
|
@@ -1,71 +0,0 @@
|
||||
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,
|
||||
};
|
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* @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,
|
||||
};
|
@@ -1,318 +0,0 @@
|
||||
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,
|
||||
};
|
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* 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,22 +1,13 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP, DOWN } = require("../../src/util");
|
||||
const { UP } = 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
|
||||
*/
|
||||
@@ -27,48 +18,28 @@ 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;
|
||||
|
||||
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_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);
|
||||
}
|
||||
|
||||
if (monitor.dns_last_result !== dnsMessage && dnsMessage !== undefined) {
|
||||
@@ -76,7 +47,7 @@ class DnsMonitorType extends MonitorType {
|
||||
}
|
||||
|
||||
heartbeat.msg = dnsMessage;
|
||||
heartbeat.status = conditionsResult ? UP : DOWN;
|
||||
heartbeat.status = UP;
|
||||
}
|
||||
}
|
||||
|
||||
|
90
server/monitor-types/grpc.js
Normal file
90
server/monitor-types/grpc.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP, log } = require("../../src/util");
|
||||
const dayjs = require("dayjs");
|
||||
const grpc = require("@grpc/grpc-js");
|
||||
const protojs = require("protobufjs");
|
||||
|
||||
class GrpcKeywordMonitorType extends MonitorType {
|
||||
name = "grpc-keyword";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
const startTime = dayjs().valueOf();
|
||||
const service = this.constructGrpcService(this.grpcUrl, this.grpcProtobuf, this.grpcServiceName, this.grpcEnableTls);
|
||||
let response = await this.grpcQuery(service, this.grpcMethod, this.grpcBody);
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
log.debug(this.name, `gRPC response: ${response}`);
|
||||
if (response.length > 50) {
|
||||
response = response.toString().substring(0, 47) + "...";
|
||||
}
|
||||
let keywordFound = response.toString().includes(this.keyword);
|
||||
if (keywordFound !== !this.isInvertKeyword()) {
|
||||
log.debug(this.name, `GRPC response [${response}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response} + "]"`);
|
||||
throw new Error(`keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response} + "]`);
|
||||
}
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = `${response}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create gRPC client
|
||||
* @param {string} url grpc Url
|
||||
* @param {string} protobufData grpc ProtobufData
|
||||
* @param {string} serviceName grpc ServiceName
|
||||
* @param {string} enableTls grpc EnableTls
|
||||
* @returns {grpc.Service} grpc Service
|
||||
*/
|
||||
constructGrpcService(url, protobufData, serviceName, enableTls) {
|
||||
const protocObject = protojs.parse(protobufData);
|
||||
const protoServiceObject = protocObject.root.lookupService(serviceName);
|
||||
const Client = grpc.makeGenericClientConstructor({});
|
||||
const credentials = enableTls ? grpc.credentials.createSsl() : grpc.credentials.createInsecure();
|
||||
const client = new Client(url, credentials);
|
||||
return protoServiceObject.create((method, requestData, cb) => {
|
||||
const fullServiceName = method.fullName;
|
||||
const serviceFQDN = fullServiceName.split(".");
|
||||
const serviceMethod = serviceFQDN.pop();
|
||||
const serviceMethodClientImpl = `/${serviceFQDN.slice(1).join(".")}/${serviceMethod}`;
|
||||
log.debug(this.name, `gRPC method ${serviceMethodClientImpl}`);
|
||||
client.makeUnaryRequest(
|
||||
serviceMethodClientImpl,
|
||||
arg => arg,
|
||||
arg => arg,
|
||||
requestData,
|
||||
cb);
|
||||
}, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create gRPC client stib
|
||||
* @param {grpc.Service} service grpc Url
|
||||
* @param {string} method grpc Method
|
||||
* @param {string} body grpc Body
|
||||
* @returns {Promise<string>} Result of gRPC query
|
||||
*/
|
||||
async grpcQuery(service, method, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
service[`${method}`](JSON.parse(body), (err, response) => {
|
||||
if (err) {
|
||||
if (err.code !== 1) {
|
||||
reject(`Error in send gRPC ${err.code} ${err.details}`);
|
||||
}
|
||||
log.debug(this.name, `ignoring ${err.code} ${err.details}, as code=1 is considered OK`);
|
||||
resolve(`${err.code} is considered OK because ${err.details}`);
|
||||
}
|
||||
resolve(JSON.stringify(response));
|
||||
});
|
||||
} catch (err) {
|
||||
reject(`Error ${err}. Please review your gRPC configuration option. The service name must not include package name value, and the method name must follow camelCase format`);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GrpcKeywordMonitorType,
|
||||
};
|
@@ -4,6 +4,7 @@ const { MongoClient } = require("mongodb");
|
||||
const jsonata = require("jsonata");
|
||||
|
||||
class MongodbMonitorType extends MonitorType {
|
||||
|
||||
name = "mongodb";
|
||||
|
||||
/**
|
||||
@@ -48,7 +49,8 @@ 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,19 +1,6 @@
|
||||
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
|
||||
@@ -24,6 +11,7 @@ class MonitorType {
|
||||
async check(monitor, heartbeat, server) {
|
||||
throw new Error("You need to override check()");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@@ -4,10 +4,15 @@ const mqtt = require("mqtt");
|
||||
const jsonata = require("jsonata");
|
||||
|
||||
class MqttMonitorType extends MonitorType {
|
||||
|
||||
name = "mqtt";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* 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>}
|
||||
*/
|
||||
async check(monitor, heartbeat, server) {
|
||||
const receivedMessage = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {
|
||||
|
@@ -1,63 +0,0 @@
|
||||
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,13 +2,23 @@ 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";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* 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
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
async check(monitor, heartbeat) {
|
||||
try {
|
||||
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
|
||||
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
|
||||
|
@@ -33,6 +33,26 @@ 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 = {
|
||||
@@ -48,7 +68,7 @@ class Discord extends NotificationProvider {
|
||||
},
|
||||
{
|
||||
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||
value: this.extractAdress(monitorJSON),
|
||||
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||
},
|
||||
{
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
@@ -85,7 +105,7 @@ class Discord extends NotificationProvider {
|
||||
},
|
||||
{
|
||||
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||
value: this.extractAdress(monitorJSON),
|
||||
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||
},
|
||||
{
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
|
@@ -1,3 +1,4 @@
|
||||
const { log } = require("../../src/util");
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const {
|
||||
relayInit,
|
||||
@@ -11,7 +12,16 @@ const {
|
||||
// polyfills for node versions
|
||||
const semver = require("semver");
|
||||
const nodeVersion = process.version;
|
||||
if (semver.lt(nodeVersion, "20.0.0")) {
|
||||
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")) {
|
||||
// polyfills for node 18
|
||||
global.crypto = require("crypto");
|
||||
global.WebSocket = require("isomorphic-ws");
|
||||
|
@@ -19,36 +19,6 @@ 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.
|
||||
*/
|
||||
extractAdress(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
|
||||
|
@@ -1,47 +0,0 @@
|
||||
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,6 +1,3 @@
|
||||
const { getMonitorRelativeURL } = require("../../src/util");
|
||||
const { setting } = require("../util-server");
|
||||
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
@@ -26,12 +23,6 @@ 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,13 +11,8 @@ 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(url, {
|
||||
await axios.post(`https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`, {
|
||||
"title": this.checkStatus(heartbeatJSON, monitorJSON),
|
||||
"desp": msg,
|
||||
});
|
||||
|
@@ -32,7 +32,28 @@ class SevenIO extends NotificationProvider {
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let address = this.extractAdress(monitorJSON);
|
||||
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;
|
||||
}
|
||||
|
||||
if (address !== "") {
|
||||
address = `(${address}) `;
|
||||
}
|
||||
|
@@ -1,52 +0,0 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { UP, DOWN } = require("../../src/util");
|
||||
|
||||
class SIGNL4 extends NotificationProvider {
|
||||
name = "SIGNL4";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON,
|
||||
msg,
|
||||
// Source system
|
||||
"X-S4-SourceSystem": "UptimeKuma",
|
||||
monitorUrl: this.extractAdress(monitorJSON),
|
||||
};
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
if (heartbeatJSON == null) {
|
||||
// Test alert
|
||||
data.title = "Uptime Kuma Alert";
|
||||
data.message = msg;
|
||||
} else if (heartbeatJSON.status === UP) {
|
||||
data.title = "Uptime Kuma Monitor ✅ Up";
|
||||
data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID;
|
||||
data["X-S4-Status"] = "resolved";
|
||||
} else if (heartbeatJSON.status === DOWN) {
|
||||
data.title = "Uptime Kuma Monitor 🔴 Down";
|
||||
data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID;
|
||||
data["X-S4-Status"] = "new";
|
||||
}
|
||||
|
||||
await axios.post(notification.webhookURL, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SIGNL4;
|
@@ -48,8 +48,7 @@ class Slack extends NotificationProvider {
|
||||
|
||||
}
|
||||
|
||||
const address = this.extractAdress(monitorJSON);
|
||||
if (address) {
|
||||
if (monitorJSON.url) {
|
||||
actions.push({
|
||||
"type": "button",
|
||||
"text": {
|
||||
@@ -57,7 +56,7 @@ class Slack extends NotificationProvider {
|
||||
"text": "Visit site",
|
||||
},
|
||||
"value": "Site",
|
||||
"url": address,
|
||||
"url": monitorJSON.url,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,22 +139,17 @@ 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": [],
|
||||
};
|
||||
|
||||
if (notification.slackrichmessage) {
|
||||
data.attachments.push(
|
||||
"attachments": [
|
||||
{
|
||||
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
|
||||
"blocks": Slack.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
|
||||
}
|
||||
);
|
||||
} else {
|
||||
data.text = `${title}\n${msg}`;
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (notification.slackbutton) {
|
||||
await Slack.deprecateURL(notification.slackbutton);
|
||||
|
@@ -93,7 +93,12 @@ class SMTP extends NotificationProvider {
|
||||
|
||||
if (monitorJSON !== null) {
|
||||
monitorName = monitorJSON["name"];
|
||||
monitorHostnameOrURL = this.extractAdress(monitorJSON);
|
||||
|
||||
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
|
||||
monitorHostnameOrURL = monitorJSON["url"];
|
||||
} else {
|
||||
monitorHostnameOrURL = monitorJSON["hostname"];
|
||||
}
|
||||
}
|
||||
|
||||
let serviceStatus = "⚠️ Test";
|
||||
|
@@ -34,7 +34,25 @@ class Squadcast extends NotificationProvider {
|
||||
data.status = "resolve";
|
||||
}
|
||||
|
||||
data.tags["AlertAddress"] = this.extractAdress(monitorJSON);
|
||||
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;
|
||||
|
||||
monitorJSON["tags"].forEach(tag => {
|
||||
data.tags[tag["name"]] = {
|
||||
|
@@ -216,6 +216,21 @@ 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) {
|
||||
@@ -225,7 +240,7 @@ class Teams extends NotificationProvider {
|
||||
const payload = this._notificationPayloadFactory({
|
||||
heartbeatJSON: heartbeatJSON,
|
||||
monitorName: monitorJSON.name,
|
||||
monitorUrl: this.extractAdress(monitorJSON),
|
||||
monitorUrl: monitorUrl,
|
||||
dashboardUrl: dashboardUrl,
|
||||
});
|
||||
|
||||
|
@@ -1,77 +0,0 @@
|
||||
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,17 +32,20 @@ class WeCom extends NotificationProvider {
|
||||
* @returns {object} Message
|
||||
*/
|
||||
composeMessage(heartbeatJSON, msg) {
|
||||
let title = "UptimeKuma Message";
|
||||
let title;
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||
title = "UptimeKuma Monitor Up";
|
||||
}
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
||||
title = "UptimeKuma Monitor Down";
|
||||
}
|
||||
if (msg != null) {
|
||||
title = "UptimeKuma Message";
|
||||
}
|
||||
return {
|
||||
msgtype: "text",
|
||||
text: {
|
||||
content: title + "\n" + msg
|
||||
content: title + msg
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -1,51 +0,0 @@
|
||||
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 `🔴 [${monitorName}] went down\n`;
|
||||
return `🔴 Application [${monitorName}] went down\n`;
|
||||
} else if (status === UP) {
|
||||
return `### ✅ [${monitorName}] is back online\n`;
|
||||
return `✅ Application [${monitorName}] is back online\n`;
|
||||
}
|
||||
return "Notification\n";
|
||||
};
|
||||
@@ -46,11 +46,16 @@ 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}`);
|
||||
payload.push(`*URL:* [${monitorUrl}](${monitorUrl})`);
|
||||
}
|
||||
|
||||
return payload;
|
||||
@@ -82,10 +87,24 @@ 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: this.extractAdress(monitorJSON),
|
||||
monitorUrl: url,
|
||||
status: heartbeatJSON.status
|
||||
});
|
||||
|
||||
|
@@ -42,7 +42,6 @@ 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");
|
||||
@@ -52,7 +51,6 @@ 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");
|
||||
@@ -65,8 +63,6 @@ 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 {
|
||||
|
||||
@@ -114,7 +110,6 @@ class Notification {
|
||||
new Ntfy(),
|
||||
new Octopush(),
|
||||
new OneBot(),
|
||||
new Onesender(),
|
||||
new Opsgenie(),
|
||||
new PagerDuty(),
|
||||
new FlashDuty(),
|
||||
@@ -128,7 +123,6 @@ class Notification {
|
||||
new ServerChan(),
|
||||
new SerwerSMS(),
|
||||
new Signal(),
|
||||
new SIGNL4(),
|
||||
new SMSManager(),
|
||||
new SMSPartner(),
|
||||
new Slack(),
|
||||
@@ -139,7 +133,6 @@ class Notification {
|
||||
new Teams(),
|
||||
new TechulusPush(),
|
||||
new Telegram(),
|
||||
new Threema(),
|
||||
new Twilio(),
|
||||
new Splunk(),
|
||||
new Webhook(),
|
||||
@@ -150,7 +143,6 @@ 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 (/^[0-9]+$/.test(requestedDuration)) {
|
||||
requestedDuration = `${requestedDuration}h`;
|
||||
if (requestedDuration === "24") {
|
||||
requestedDuration = "24h";
|
||||
}
|
||||
|
||||
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.slice(0, -1)}${labelSuffix})`,
|
||||
label ?? `Uptime (${requestedDuration}${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 (/^[0-9]+$/.test(requestedDuration)) {
|
||||
requestedDuration = `${requestedDuration}h`;
|
||||
if (requestedDuration === "24") {
|
||||
requestedDuration = "24h";
|
||||
}
|
||||
|
||||
// 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.slice(0, -1)}${labelSuffix})` ]);
|
||||
badgeValues.label = filterAndJoin([ labelPrefix, label ?? `Avg. Ping (${requestedDuration}${labelSuffix})` ]);
|
||||
badgeValues.message = filterAndJoin([ prefix, avgPing, suffix ]);
|
||||
}
|
||||
|
||||
|
@@ -18,11 +18,6 @@ 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 = " < 18 || 20.0.* || 20.1.* || 20.2.* || 20.3.* ";
|
||||
const bannedNodeVersions = " < 14 || 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, sendMonitorTypeList } = require("./client");
|
||||
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList, sendRemoteBrowserList } = 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,36 +246,6 @@ 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
|
||||
@@ -716,8 +686,6 @@ 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;
|
||||
|
||||
@@ -727,13 +695,13 @@ let needSetup = false;
|
||||
|
||||
await updateMonitorNotification(bean.id, notificationIDList);
|
||||
|
||||
await server.sendUpdateMonitorIntoList(socket, bean.id);
|
||||
await server.sendMonitorList(socket);
|
||||
|
||||
if (monitor.active !== false) {
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
}
|
||||
|
||||
log.info("monitor", `Added Monitor: ${bean.id} User ID: ${socket.userID}`);
|
||||
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -858,17 +826,11 @@ 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();
|
||||
|
||||
@@ -880,11 +842,11 @@ let needSetup = false;
|
||||
|
||||
await updateMonitorNotification(bean.id, monitor.notificationIDList);
|
||||
|
||||
if (await Monitor.isActive(bean.id, bean.active)) {
|
||||
if (await bean.isActive()) {
|
||||
await restartMonitor(socket.userID, bean.id);
|
||||
}
|
||||
|
||||
await server.sendUpdateMonitorIntoList(socket, bean.id);
|
||||
await server.sendMonitorList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -924,17 +886,14 @@ let needSetup = false;
|
||||
|
||||
log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||
|
||||
let monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [
|
||||
let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
socket.userID,
|
||||
]);
|
||||
const monitorData = [{ id: monitor.id,
|
||||
active: monitor.active
|
||||
}];
|
||||
const preloadData = await Monitor.preparePreloadData(monitorData);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
monitor: monitor.toJSON(preloadData),
|
||||
monitor: await bean.toJSON(),
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
@@ -985,7 +944,7 @@ let needSetup = false;
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await startMonitor(socket.userID, monitorID);
|
||||
await server.sendUpdateMonitorIntoList(socket, monitorID);
|
||||
await server.sendMonitorList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -1005,7 +964,7 @@ let needSetup = false;
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await pauseMonitor(socket.userID, monitorID);
|
||||
await server.sendUpdateMonitorIntoList(socket, monitorID);
|
||||
await server.sendMonitorList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -1051,7 +1010,8 @@ let needSetup = false;
|
||||
msg: "successDeleted",
|
||||
msgi18n: true,
|
||||
});
|
||||
await server.sendDeleteMonitorFromList(socket, monitorID);
|
||||
|
||||
await server.sendMonitorList(socket);
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
@@ -1676,18 +1636,17 @@ 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) {
|
||||
monitorPromises.push(sendHeartbeatList(socket, monitorID));
|
||||
monitorPromises.push(Monitor.sendStats(io, monitorID, user.id));
|
||||
await sendHeartbeatList(socket, monitorID);
|
||||
}
|
||||
|
||||
await Promise.all(monitorPromises);
|
||||
for (let monitorID in monitorList) {
|
||||
await Monitor.sendStats(io, monitorID, user.id);
|
||||
}
|
||||
|
||||
// Set server timezone from client browser if not set
|
||||
// It should be run once only
|
||||
|
@@ -6,7 +6,7 @@ const Database = require("../database");
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @returns {void}
|
||||
*/
|
||||
module.exports.databaseSocketHandler = (socket) => {
|
||||
module.exports = (socket) => {
|
||||
|
||||
// Post or edit incident
|
||||
socket.on("getDatabaseSize", async (callback) => {
|
||||
|
@@ -29,13 +29,8 @@ 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,9 +543,7 @@ 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);
|
||||
|
||||
@@ -743,36 +741,20 @@ class UptimeCalculator {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Get the uptime data by duration
|
||||
* @param {'24h'|'30d'|'1y'} duration Only accept 24h, 30d, 1y
|
||||
* @returns {UptimeDataResult} UptimeDataResult
|
||||
* @throws {Error} Invalid duration / Unsupported unit
|
||||
* @throws {Error} Invalid duration
|
||||
*/
|
||||
getDataByDuration(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}`
|
||||
);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -113,7 +113,7 @@ class UptimeKumaServer {
|
||||
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
||||
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["grpc-keyword"] = new GrpcKeywordMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
|
||||
|
||||
// Allow all CORS origins (polling) in development
|
||||
@@ -205,56 +205,24 @@ class UptimeKumaServer {
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Monitor into list
|
||||
* @param {Socket} socket Socket to send list on
|
||||
* @param {number} monitorID update or deleted monitor id
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async sendUpdateMonitorIntoList(socket, monitorID) {
|
||||
let list = await this.getMonitorJSONList(socket.userID, monitorID);
|
||||
this.io.to(socket.userID).emit("updateMonitorIntoList", list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Monitor from list
|
||||
* @param {Socket} socket Socket to send list on
|
||||
* @param {number} monitorID update or deleted monitor id
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async sendDeleteMonitorFromList(socket, monitorID) {
|
||||
this.io.to(socket.userID).emit("deleteMonitorFromList", monitorID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of monitors for the given user.
|
||||
* @param {string} userID - The ID of the user to get monitors for.
|
||||
* @param {number} monitorID - The ID of monitor for.
|
||||
* @returns {Promise<object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async getMonitorJSONList(userID, monitorID = null) {
|
||||
async getMonitorJSONList(userID) {
|
||||
let result = {};
|
||||
|
||||
let query = " user_id = ? ";
|
||||
let queryParams = [ userID ];
|
||||
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
|
||||
userID,
|
||||
]);
|
||||
|
||||
if (monitorID) {
|
||||
query += "AND id = ? ";
|
||||
queryParams.push(monitorID);
|
||||
for (let monitor of monitorList) {
|
||||
result[monitor.id] = await monitor.toJSON();
|
||||
}
|
||||
|
||||
let monitorList = await R.find("monitor", query + "ORDER BY weight DESC, name", queryParams);
|
||||
|
||||
const monitorData = monitorList.map(monitor => ({
|
||||
id: monitor.id,
|
||||
active: monitor.active,
|
||||
name: monitor.name,
|
||||
}));
|
||||
const preloadData = await Monitor.preparePreloadData(monitorData);
|
||||
|
||||
const result = {};
|
||||
monitorList.forEach(monitor => result[monitor.id] = monitor.toJSON(preloadData));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -550,6 +518,5 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor
|
||||
const { TailscalePing } = require("./monitor-types/tailscale-ping");
|
||||
const { DnsMonitorType } = require("./monitor-types/dns");
|
||||
const { MqttMonitorType } = require("./monitor-types/mqtt");
|
||||
const { SNMPMonitorType } = require("./monitor-types/snmp");
|
||||
const { GrpcKeywordMonitorType } = require("./monitor-types/grpc");
|
||||
const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
||||
const Monitor = require("./model/monitor");
|
||||
|
@@ -11,10 +11,8 @@ const mssql = require("mssql");
|
||||
const { Client } = require("pg");
|
||||
const postgresConParse = require("pg-connection-string").parse;
|
||||
const mysql = require("mysql2");
|
||||
const { NtlmClient } = require("./modules/axios-ntlm/lib/ntlmClient.js");
|
||||
const { NtlmClient } = require("axios-ntlm");
|
||||
const { Settings } = require("./settings");
|
||||
const grpc = require("@grpc/grpc-js");
|
||||
const protojs = require("protobufjs");
|
||||
const radiusClient = require("node-radius-client");
|
||||
const redis = require("redis");
|
||||
const oidc = require("openid-client");
|
||||
@@ -919,64 +917,6 @@ module.exports.timeObjectToLocal = (obj, timezone = undefined) => {
|
||||
return timeObjectConvertTimezone(obj, timezone, false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create gRPC client stib
|
||||
* @param {object} options from gRPC client
|
||||
* @returns {Promise<object>} Result of gRPC query
|
||||
*/
|
||||
module.exports.grpcQuery = async (options) => {
|
||||
const { grpcUrl, grpcProtobufData, grpcServiceName, grpcEnableTls, grpcMethod, grpcBody } = options;
|
||||
const protocObject = protojs.parse(grpcProtobufData);
|
||||
const protoServiceObject = protocObject.root.lookupService(grpcServiceName);
|
||||
const Client = grpc.makeGenericClientConstructor({});
|
||||
const credentials = grpcEnableTls ? grpc.credentials.createSsl() : grpc.credentials.createInsecure();
|
||||
const client = new Client(
|
||||
grpcUrl,
|
||||
credentials
|
||||
);
|
||||
const grpcService = protoServiceObject.create(function (method, requestData, cb) {
|
||||
const fullServiceName = method.fullName;
|
||||
const serviceFQDN = fullServiceName.split(".");
|
||||
const serviceMethod = serviceFQDN.pop();
|
||||
const serviceMethodClientImpl = `/${serviceFQDN.slice(1).join(".")}/${serviceMethod}`;
|
||||
log.debug("monitor", `gRPC method ${serviceMethodClientImpl}`);
|
||||
client.makeUnaryRequest(
|
||||
serviceMethodClientImpl,
|
||||
arg => arg,
|
||||
arg => arg,
|
||||
requestData,
|
||||
cb);
|
||||
}, false, false);
|
||||
return new Promise((resolve, _) => {
|
||||
try {
|
||||
return grpcService[`${grpcMethod}`](JSON.parse(grpcBody), function (err, response) {
|
||||
const responseData = JSON.stringify(response);
|
||||
if (err) {
|
||||
return resolve({
|
||||
code: err.code,
|
||||
errorMessage: err.details,
|
||||
data: ""
|
||||
});
|
||||
} else {
|
||||
log.debug("monitor:", `gRPC response: ${JSON.stringify(response)}`);
|
||||
return resolve({
|
||||
code: 1,
|
||||
errorMessage: "",
|
||||
data: responseData
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
return resolve({
|
||||
code: -1,
|
||||
errorMessage: `Error ${err}. Please review your gRPC configuration option. The service name must not include package name value, and the method name must follow camelCase format`,
|
||||
data: ""
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of SHA256 fingerprints for all known root certificates.
|
||||
* @returns {Set} A set of SHA256 fingerprints.
|
||||
|
@@ -576,12 +576,6 @@ optgroup {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.prism-editor__container {
|
||||
.important {
|
||||
font-weight: var(--bs-body-font-weight) !important;
|
||||
}
|
||||
}
|
||||
|
||||
h5.settings-subheading::after {
|
||||
content: "";
|
||||
display: block;
|
||||
|
@@ -1,201 +0,0 @@
|
||||
<template>
|
||||
<div ref="modal" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div v-if="monitor?.type === 'http'">
|
||||
<textarea id="curl-debug" v-model="curlCommand" class="form-control mb-3" readonly wrap="off"></textarea>
|
||||
<button
|
||||
id="debug-copy-btn" class="btn btn-outline-primary position-absolute top-0 end-0 mt-3 me-3 border-0"
|
||||
type="button" @click.stop="copyToClipboard"
|
||||
>
|
||||
<font-awesome-icon icon="copy" />
|
||||
</button>
|
||||
<i18n-t keypath="CurlDebugInfo" tag="p" class="form-text">
|
||||
<template #newiline>
|
||||
<br>
|
||||
</template>
|
||||
<template #firewalls>
|
||||
<a href="https://xkcd.com/2259/" target="_blank">{{ $t('firewalls') }}</a>
|
||||
</template>
|
||||
<template #dns_resolvers>
|
||||
<a
|
||||
href="https://www.reddit.com/r/sysadmin/comments/rxho93/thank_you_for_the_running_its_always_dns_joke_its/"
|
||||
target="_blank"
|
||||
>{{ $t('dns resolvers') }}</a>
|
||||
</template>
|
||||
<template #docker_networks>
|
||||
<a href="https://youtu.be/bKFMS5C4CG0" target="_blank">{{ $t('docker networks') }}</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<div
|
||||
v-if="monitor.authMethod === 'oauth2-cc'" class="alert alert-warning d-flex align-items-center gap-2"
|
||||
role="alert"
|
||||
>
|
||||
<div role="img" aria-label="Warning:">⚠️</div>
|
||||
<i18n-t keypath="CurlDebugInfoOAuth2CCUnsupported" tag="div">
|
||||
<template #curl>
|
||||
<code>curl</code>
|
||||
</template>
|
||||
<template #newline>
|
||||
<br>
|
||||
</template>
|
||||
<template #oauth2_bearer>
|
||||
<code>--oauth2-bearer TOKEN</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div v-if="monitor.proxyId" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
|
||||
<div role="img" aria-label="Warning:">⚠️</div>
|
||||
<i18n-t keypath="CurlDebugInfoProxiesUnsupported" tag="div">
|
||||
<template #curl>
|
||||
<code>curl</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { Modal } from "bootstrap";
|
||||
import { version } from "../../package.json";
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
export default {
|
||||
name: "DebugMonitor",
|
||||
props: {
|
||||
/** Monitor this represents */
|
||||
monitor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modal: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
curlCommand() {
|
||||
if (this.monitor === null) {
|
||||
return "";
|
||||
}
|
||||
let method = this.monitor.method;
|
||||
if ([ "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS" ].indexOf(method) === -1) {
|
||||
// set to a custom value => could lead to injections
|
||||
method = this.escapeShell(method);
|
||||
}
|
||||
const command = [ "curl", "--verbose", "--head", "--request", method, "\\\n" ];
|
||||
command.push("--user-agent", `'Uptime-Kuma/${version}'`, "\\\n");
|
||||
if (this.monitor.ignoreTls) {
|
||||
command.push("--insecure", "\\\n");
|
||||
}
|
||||
if (this.monitor.headers) {
|
||||
try {
|
||||
for (const [ key, value ] of Object.entries(JSON.parse(this.monitor.headers))) {
|
||||
command.push("--header", `'${this.escapeShellNoQuotes(key)}: ${this.escapeShellNoQuotes(value)}'`, "\\\n");
|
||||
}
|
||||
} catch (e) {
|
||||
command.push("--header", this.escapeShell(this.monitor.headers), "\\\n");
|
||||
}
|
||||
}
|
||||
if (this.monitor.authMethod === "basic") {
|
||||
command.push("--basic", "--user", `'${this.escapeShellNoQuotes(this.monitor.basic_auth_user)}:${this.escapeShellNoQuotes(this.monitor.basic_auth_pass)}'`, "\\\n");
|
||||
} else if (this.monitor.authMethod === "mtls") {
|
||||
command.push("--cacert", this.escapeShell(this.monitor.tlsCa), "\\\n");
|
||||
command.push("--key", this.escapeShell(this.monitor.tlsKey), "\\\n");
|
||||
command.push( "--cert", this.escapeShell(this.monitor.tlsCert), "\\\n");
|
||||
} else if (this.monitor.authMethod === "ntlm") {
|
||||
let domain = "";
|
||||
if (this.monitor.authDomain) {
|
||||
domain = `${this.monitor.authDomain}/`;
|
||||
}
|
||||
command.push("--ntlm", "--user", `'${this.escapeShellNoQuotes(domain)}${this.escapeShellNoQuotes(this.monitor.basic_auth_user)}:${this.escapeShellNoQuotes(this.monitor.basic_auth_pass)}'`, "\\\n");
|
||||
}
|
||||
if (this.monitor.body && this.monitor.httpBodyEncoding === "json") {
|
||||
let json = "";
|
||||
try {
|
||||
// trying to parse the supplied data as json to trim whitespace
|
||||
json = JSON.stringify(JSON.parse(this.monitor.body));
|
||||
} catch (e) {
|
||||
json = this.monitor.body;
|
||||
}
|
||||
command.push("--header", "'Content-Type: application/json'", "\\\n");
|
||||
command.push("--data", this.escapeShell(json), "\\\n");
|
||||
} else if (this.monitor.body && this.monitor.httpBodyEncoding === "xml") {
|
||||
command.push("--headers", "'Content-Type: application/xml'", "\\\n");
|
||||
command.push("--data", this.escapeShell(this.monitor.body), "\\\n");
|
||||
}
|
||||
if (this.monitor.maxredirects) {
|
||||
command.push("--location", "--max-redirs", this.escapeShell(this.monitor.maxredirects), "\\\n");
|
||||
}
|
||||
if (this.monitor.timeout) {
|
||||
command.push("--max-time", this.escapeShell(this.monitor.timeout), "\\\n");
|
||||
}
|
||||
if (this.monitor.maxretries) {
|
||||
command.push("--retry", this.escapeShell(this.monitor.maxretries), "\\\n");
|
||||
}
|
||||
command.push("--url", this.escapeShell(this.monitor.url));
|
||||
return command.join(" ");
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal);
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Show the dialog
|
||||
* @returns {void}
|
||||
*/
|
||||
show() {
|
||||
this.modal.show();
|
||||
},
|
||||
/**
|
||||
* Escape a string for use in a shell
|
||||
* @param {string|number} s string to escape
|
||||
* @returns {string} escaped, quoted string
|
||||
*/
|
||||
escapeShell(s) {
|
||||
if (typeof s == "number") {
|
||||
return s.toString();
|
||||
}
|
||||
return "'" + this.escapeShellNoQuotes(s) + "'";
|
||||
},
|
||||
/**
|
||||
* Escape a string for use in a shell
|
||||
* @param {string} s string to escape
|
||||
* @returns {string} escaped string
|
||||
*/
|
||||
escapeShellNoQuotes(s) {
|
||||
return s.replace(/(['"$`\\])/g, "\\$1");
|
||||
},
|
||||
/**
|
||||
* Copies a value to the clipboard and shows toasts with the success/error
|
||||
* @returns {void}
|
||||
*/
|
||||
async copyToClipboard() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.curlCommand);
|
||||
toast.success(this.$t("CopyToClipboardSuccess"));
|
||||
} catch (err) {
|
||||
toast.error(this.$t("CopyToClipboardError", { error: err.message }));
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars";
|
||||
|
||||
textarea {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
#curl-debug {
|
||||
font-family: monospace;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
@@ -1,152 +0,0 @@
|
||||
<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>
|
@@ -1,189 +0,0 @@
|
||||
<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>
|
@@ -1,149 +0,0 @@
|
||||
<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 }}</div>
|
||||
<div>{{ timeSinceFirstBeat }} ago</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 this.$t("now");
|
||||
return "now";
|
||||
} else if (seconds < 60 * 60) {
|
||||
return this.$t("time ago", [ (seconds / 60).toFixed(0) + "m" ]);
|
||||
return (seconds / 60).toFixed(0) + "m ago";
|
||||
} else {
|
||||
return this.$t("time ago", [ (seconds / 60 / 60).toFixed(0) + "h" ]);
|
||||
return (seconds / 60 / 60).toFixed(0) + "h ago";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -45,7 +45,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle" data-testid="monitor-list">
|
||||
<div ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle">
|
||||
<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,15 +43,12 @@
|
||||
<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>
|
||||
|
@@ -135,7 +135,6 @@ export default {
|
||||
"ntfy": "Ntfy",
|
||||
"octopush": "Octopush",
|
||||
"OneBot": "OneBot",
|
||||
"Onesender": "Onesender",
|
||||
"Opsgenie": "Opsgenie",
|
||||
"PagerDuty": "PagerDuty",
|
||||
"PagerTree": "PagerTree",
|
||||
@@ -145,7 +144,6 @@ export default {
|
||||
"pushy": "Pushy",
|
||||
"rocket.chat": "Rocket.Chat",
|
||||
"signal": "Signal",
|
||||
"SIGNL4": "SIGNL4",
|
||||
"slack": "Slack",
|
||||
"squadcast": "SquadCast",
|
||||
"SMSEagle": "SMSEagle",
|
||||
@@ -154,7 +152,6 @@ export default {
|
||||
"stackfield": "Stackfield",
|
||||
"teams": "Microsoft Teams",
|
||||
"telegram": "Telegram",
|
||||
"threema": "Threema",
|
||||
"twilio": "Twilio",
|
||||
"Splunk": "Splunk",
|
||||
"webhook": "Webhook",
|
||||
@@ -180,7 +177,6 @@ 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" data-testid="group">
|
||||
<div class="mb-5 ">
|
||||
<!-- 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" data-testid="group-name" />
|
||||
<Editable v-model="group.element.name" :contenteditable="editMode" tag="span" />
|
||||
</h2>
|
||||
|
||||
<div class="shadow-box monitor-list mt-4 position-relative">
|
||||
@@ -31,9 +31,9 @@
|
||||
item-key="id"
|
||||
>
|
||||
<template #item="monitor">
|
||||
<div class="item" data-testid="monitor">
|
||||
<div class="item">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-4 small-padding">
|
||||
<div class="col-9 col-md-8 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,11 +45,10 @@
|
||||
class="item-name"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid="monitor-name"
|
||||
>
|
||||
{{ monitor.element.name }}
|
||||
</a>
|
||||
<p v-else class="item-name" data-testid="monitor-name"> {{ monitor.element.name }} </p>
|
||||
<p v-else class="item-name"> {{ monitor.element.name }} </p>
|
||||
|
||||
<span
|
||||
title="Setting"
|
||||
@@ -67,11 +66,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'" data-testid="monitor-tag" />
|
||||
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :key="$root.userHeartbeatBar" class="col-6 col-md-8">
|
||||
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -14,7 +14,6 @@
|
||||
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") }}
|
||||
@@ -60,7 +59,6 @@
|
||||
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">
|
||||
@@ -78,7 +76,6 @@
|
||||
label="name"
|
||||
select-label=""
|
||||
deselect-label=""
|
||||
data-testid="tag-color-select"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<div
|
||||
@@ -106,7 +103,6 @@
|
||||
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">
|
||||
@@ -118,7 +114,6 @@
|
||||
type="button"
|
||||
class="btn btn-secondary float-end"
|
||||
:disabled="processing || validateDraftTag.invalid"
|
||||
data-testid="tag-submit-button"
|
||||
@click.stop="addDraftTag"
|
||||
>
|
||||
{{ $t("Add") }}
|
||||
|
@@ -1,81 +0,0 @@
|
||||
<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>
|
@@ -1,16 +0,0 @@
|
||||
<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>
|
@@ -9,12 +9,6 @@
|
||||
<label for="slack-channel" class="form-label">{{ $t("Channel Name") }}</label>
|
||||
<input id="slack-channel-name" v-model="$parent.notification.slackchannel" type="text" class="form-control">
|
||||
|
||||
<label class="form-label">{{ $t("Message format") }}</label>
|
||||
<div class="form-check form-switch">
|
||||
<input id="slack-text-message" v-model="$parent.notification.slackrichmessage" type="checkbox" class="form-check-input">
|
||||
<label for="slack-text-message" class="form-label">{{ $t("Send rich messages") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||
|
@@ -1,87 +0,0 @@
|
||||
<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>
|
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="wpush-apikey" class="form-label">WPush {{ $t("API Key") }}</label>
|
||||
<HiddenInput id="wpush-apikey" v-model="$parent.notification.wpushAPIkey" :required="true" autocomplete="new-password" placeholder="WPushxxxxx"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="wpush-channel" class="form-label">发送通道</label>
|
||||
<select id="wpush-channel" v-model="$parent.notification.wpushChannel" class="form-select" required>
|
||||
<option value="wechat">微信</option>
|
||||
<option value="sms">短信</option>
|
||||
<option value="mail">邮件</option>
|
||||
<option value="feishu">飞书</option>
|
||||
<option value="dingtalk">钉钉</option>
|
||||
<option value="wechat_work">企业微信</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<i18n-t tag="p" keypath="More info on:">
|
||||
<a href="https://wpush.cn/" rel="noopener noreferrer" target="_blank">https://wpush.cn/</a>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
@@ -29,7 +29,6 @@ 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";
|
||||
@@ -53,7 +52,6 @@ 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";
|
||||
@@ -63,8 +61,6 @@ 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.
|
||||
@@ -101,7 +97,6 @@ const NotificationFormList = {
|
||||
"ntfy": Ntfy,
|
||||
"octopush": Octopush,
|
||||
"OneBot": OneBot,
|
||||
"Onesender": Onesender,
|
||||
"Opsgenie": Opsgenie,
|
||||
"PagerDuty": PagerDuty,
|
||||
"FlashDuty": FlashDuty,
|
||||
@@ -115,7 +110,6 @@ const NotificationFormList = {
|
||||
"rocket.chat": RocketChat,
|
||||
"serwersms": SerwerSMS,
|
||||
"signal": Signal,
|
||||
"SIGNL4": SIGNL4,
|
||||
"SMSManager": SMSManager,
|
||||
"SMSPartner": SMSPartner,
|
||||
"slack": Slack,
|
||||
@@ -125,7 +119,6 @@ const NotificationFormList = {
|
||||
"stackfield": Stackfield,
|
||||
"teams": Teams,
|
||||
"telegram": Telegram,
|
||||
"threema": Threema,
|
||||
"twilio": Twilio,
|
||||
"Splunk": Splunk,
|
||||
"webhook": Webhook,
|
||||
@@ -137,7 +130,6 @@ const NotificationFormList = {
|
||||
"whapi": Whapi,
|
||||
"gtxmessaging": GtxMessaging,
|
||||
"Cellsynt": Cellsynt,
|
||||
"WPush": WPush
|
||||
};
|
||||
|
||||
export default NotificationFormList;
|
||||
|
@@ -1,63 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="settings.disableAuth"
|
||||
class="mt-5 d-flex align-items-center justify-content-center my-3"
|
||||
>
|
||||
{{ $t("apiKeysDisabledMsg") }}
|
||||
<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 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>
|
||||
<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
|
||||
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 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>
|
||||
</div>
|
||||
<button class="btn btn-danger" @click="deleteDialog(item.id)">
|
||||
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,9 +88,6 @@ export default {
|
||||
let result = Object.values(this.$root.apiKeyList);
|
||||
return result;
|
||||
},
|
||||
settings() {
|
||||
return this.$parent.$parent.$parent.settings;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -139,11 +126,9 @@ 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);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -161,113 +146,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;
|
||||
}
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-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;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.circle {
|
||||
background-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
.circle {
|
||||
background-color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
&.expired {
|
||||
.left-part {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.circle {
|
||||
background-color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
.left-part {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
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;
|
||||
|
||||
.circle {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
.title {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.circle {
|
||||
background-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
.circle {
|
||||
background-color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
&.expired {
|
||||
.left-part {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.circle {
|
||||
background-color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -32,14 +32,7 @@
|
||||
<button class="btn btn-outline-info me-2" @click="shrinkDatabase">
|
||||
{{ $t("Shrink Database") }} ({{ databaseSizeDisplay }})
|
||||
</button>
|
||||
<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 class="form-text mt-2 mb-4 ms-2">{{ $t("shrinkDatabaseDescription") }}</div>
|
||||
</div>
|
||||
<button
|
||||
id="clearAllStats-btn"
|
||||
|
@@ -6,7 +6,6 @@ const languageList = {
|
||||
"cs-CZ": "Čeština",
|
||||
"zh-HK": "繁體中文 (香港)",
|
||||
"bg-BG": "Български",
|
||||
"be": "Беларуская",
|
||||
"de-DE": "Deutsch (Deutschland)",
|
||||
"de-CH": "Deutsch (Schweiz)",
|
||||
"nl-NL": "Nederlands",
|
||||
|
@@ -388,6 +388,7 @@
|
||||
"Discard": "تجاهل",
|
||||
"Cancel": "يلغي",
|
||||
"Powered by": "مشغل بواسطة",
|
||||
"shrinkDatabaseDescription": "تشغيل فراغ قاعدة البيانات لـ SQLite. إذا تم إنشاء قاعدة البيانات الخاصة بك بعد تمكين 1.10.0 AUTO_VACUUM بالفعل ولا يلزم هذا الإجراء.",
|
||||
"serwersms": "Serwersms.pl",
|
||||
"serwersmsAPIUser": "اسم مستخدم API (بما في ذلك بادئة WebAPI_)",
|
||||
"serwersmsAPIPassword": "كلمة مرور API",
|
||||
|
@@ -278,6 +278,7 @@
|
||||
"Discard": "تجاهل",
|
||||
"Cancel": "يلغي",
|
||||
"Powered by": "مشغل بواسطة",
|
||||
"shrinkDatabaseDescription": "تشغيل فراغ قاعدة البيانات لـ SQLite. إذا تم إنشاء قاعدة البيانات الخاصة بك بعد تمكين 1.10.0 AUTO_VACUUM بالفعل ولا يلزم هذا الإجراء.",
|
||||
"Customize": "يعدل أو يكيف",
|
||||
"Custom Footer": "تذييل مخصص",
|
||||
"Custom CSS": "لغة تنسيق ويب حسب الطلب",
|
||||
|
937
src/lang/be.json
937
src/lang/be.json
@@ -1,937 +0,0 @@
|
||||
{
|
||||
"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,6 +312,7 @@
|
||||
"PasswordsDoNotMatch": "Паролите не съвпадат.",
|
||||
"Current User": "Текущ потребител",
|
||||
"recent": "Скорошни",
|
||||
"shrinkDatabaseDescription": "Инициира \"VACUUM\" за \"SQLite\" база данни. Ако Вашата база данни е създадена след версия 1.10.0, \"AUTO_VACUUM\" функцията е активна и това действие не е нужно.",
|
||||
"Done": "Готово",
|
||||
"Info": "Информация",
|
||||
"Security": "Сигурност",
|
||||
@@ -402,7 +403,7 @@
|
||||
"Retry": "Повтори",
|
||||
"Topic": "Тема",
|
||||
"WeCom Bot Key": "WeCom бот ключ",
|
||||
"Setup Proxy": "Настройка на прокси",
|
||||
"Setup Proxy": "Настрой прокси",
|
||||
"Proxy Protocol": "Прокси протокол",
|
||||
"Proxy Server": "Прокси сървър",
|
||||
"Proxy server has authentication": "Прокси сървърът е с удостоверяване",
|
||||
@@ -801,6 +802,7 @@
|
||||
"twilioApiKey": "API ключ (по избор)",
|
||||
"Expected Value": "Очаквана стойност",
|
||||
"Json Query": "Заявка тип JSON",
|
||||
"jsonQueryDescription": "Прави JSON заявка срещу отговора и проверява за очаквана стойност (Върнатата стойност ще бъде преобразувана в низ за сравнение). Разгледайте {0} за документация относно езика на заявката. Имате възможност да тествате {1}.",
|
||||
"Badge Duration (in hours)": "Времетраене на баджа (в часове)",
|
||||
"Badge Preview": "Преглед на баджа",
|
||||
"Notify Channel": "Канал за известяване",
|
||||
@@ -895,10 +897,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}'",
|
||||
@@ -971,84 +973,5 @@
|
||||
"Create new forum post": "Създай нова публикация във форум",
|
||||
"postToExistingThread": "Публикувай в съществуваща тема/публикация във форум",
|
||||
"forumPostName": "Име на публикацията във форума",
|
||||
"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": "запис"
|
||||
"threadForumPostID": "ID на публикация в темата/форум"
|
||||
}
|
||||
|
@@ -1,18 +1 @@
|
||||
{
|
||||
"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": "খেলা"
|
||||
}
|
||||
{}
|
||||
|
145
src/lang/ca.json
145
src/lang/ca.json
@@ -70,148 +70,5 @@
|
||||
"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",
|
||||
"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."
|
||||
"markdownSupported": "Sintaxi de Markdown suportada"
|
||||
}
|
||||
|
@@ -387,6 +387,7 @@
|
||||
"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",
|
||||
@@ -822,6 +823,7 @@
|
||||
"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",
|
||||
@@ -831,16 +833,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 \"Uptime Kuma\" a získat push adresu, zkopírovat integrační klíč v adrese. 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 \"Vlastní událost\" a získat adresu pro odeslání, zkopírovat klíč integrace do adresy. 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: Port",
|
||||
"gamedigGuessPort": "Gamedig: Guess Port",
|
||||
"Saved.": "Uloženo.",
|
||||
"setupDatabaseChooseDatabase": "Kterou databázi byste chtěli používat?",
|
||||
"setupDatabaseChooseDatabase": "Kterou databázi chcete použít?",
|
||||
"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.",
|
||||
@@ -893,9 +895,9 @@
|
||||
"emailTemplateLimitedToUpDownNotification": "dostupné pouze pro heatbeaty BĚŽÍ/NEBĚŽÍ, jinak null",
|
||||
"templateMonitorJSON": "objekt popisující dohled",
|
||||
"templateLimitedToUpDownNotifications": "dostupné pouze pro oznámení BĚŽÍ/NEBĚŽÍ",
|
||||
"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.",
|
||||
"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í.",
|
||||
"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ů",
|
||||
@@ -942,7 +944,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 místnímu spuštění Chromu. Nastavte se pomocí služby, jako je browserless.io, nebo se připojte k vlastnímu",
|
||||
"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",
|
||||
"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",
|
||||
@@ -951,65 +953,5 @@
|
||||
"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)",
|
||||
"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"
|
||||
"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)"
|
||||
}
|
||||
|
@@ -345,6 +345,7 @@
|
||||
"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,6 +355,7 @@
|
||||
"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",
|
||||
@@ -811,6 +812,7 @@
|
||||
"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.",
|
||||
@@ -968,84 +970,5 @@
|
||||
"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}",
|
||||
"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"
|
||||
"wayToGetDiscordThreadId": "Das Abrufen einer Thread-/Forumspost-ID ist ähnlich wie das Abrufen einer Channel-ID. Lese mehr darüber, wie man IDs abruft {0}"
|
||||
}
|
||||
|
@@ -355,6 +355,7 @@
|
||||
"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",
|
||||
@@ -816,6 +817,7 @@
|
||||
"filterActivePaused": "Pausiert",
|
||||
"Expected Value": "Erwarteter Wert",
|
||||
"Json Query": "Json-Abfrage",
|
||||
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf {0} findest du die Dokumentation zur Abfragesprache. {1} kannst du Abfragen üben.",
|
||||
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
|
||||
"Server URL should not contain the nfty topic": "Die Server-URL sollte das nfty-Thema nicht enthalten",
|
||||
"pushDeerServerDescription": "Leer lassen um den offiziellen Server zu verwenden",
|
||||
@@ -971,84 +973,5 @@
|
||||
"forumPostName": "Name des Forumsbeitrags",
|
||||
"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}",
|
||||
"smspartnerPhoneNumber": "Telefonnummer(n)",
|
||||
"smspartnerSenderName": "SMS Absender Name",
|
||||
"smspartnerSenderNameInfo": "Muss zwischen 3..=11 regulären Zeichen sein",
|
||||
"smspartnerApiurl": "Den API-Schlüssel findest du im Dashboard unter {0}",
|
||||
"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.",
|
||||
"wayToGetOnesenderUrlandToken": "Du kannst die URL und den Token auf der Onesender-Website erhalten. Weitere Infos {0}",
|
||||
"Lost connection to the socket server.": "Verbindung zum Socket-Server verloren.",
|
||||
"conditionDeleteGroup": "Gruppe löschen",
|
||||
"greater than": "mehr als",
|
||||
"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.",
|
||||
"signl4Docs": "Weitere Informationen zur Konfiguration von SIGNL4 und zum Abrufen der SIGNL4-Webhook-URL siehe {0}.",
|
||||
"now": "jetzt",
|
||||
"time ago": "vor {0}",
|
||||
"Json Query Expression": "Json Query Ausdrck",
|
||||
"-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",
|
||||
"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",
|
||||
"Add Remote Browser": "Remote-Browser hinzufügen",
|
||||
"New Group": "Neue Gruppe",
|
||||
"Group Name": "Gruppenname",
|
||||
"OAuth2: Client Credentials": "OAuth2: Client-Anmeldeinformationen",
|
||||
"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.",
|
||||
"Cannot connect to the socket server.": "Es kann keine Verbindung zum Socket-Server hergestellt werden.",
|
||||
"SIGNL4": "SIGNL4",
|
||||
"SIGNL4 Webhook URL": "SIGNL4 Webhook URL",
|
||||
"Conditions": "Bedingungen",
|
||||
"conditionAdd": "Bedingung hinzufügen",
|
||||
"conditionDelete": "Bedingung löschen",
|
||||
"conditionAddGroup": "Gruppe hinzufügen",
|
||||
"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",
|
||||
"not ends with": "endet nicht mit",
|
||||
"less than": "weniger als",
|
||||
"less than or equal to": "kleiner als oder gleich",
|
||||
"greater than or equal to": "größer als oder gleich",
|
||||
"record": "Eintrag"
|
||||
"wayToGetDiscordThreadId": "Das Abrufen einer Thread-/Forumspost-ID ist ähnlich wie das Abrufen einer Channel-ID. Lese mehr darüber, wie man IDs abruft {0}"
|
||||
}
|
||||
|
@@ -31,13 +31,13 @@
|
||||
"confirmDisableTwoFAMsg": "Είστε βέβαιοι ότι θέλετε να απενεργοποιήσετε το 2FA;",
|
||||
"Settings": "Ρυθμίσεις",
|
||||
"Dashboard": "Πίνακας",
|
||||
"New Update": "Νέα ενημέρωση",
|
||||
"New Update": "Νέα αναβάθμιση",
|
||||
"Language": "Γλώσσα",
|
||||
"Appearance": "Εμφάνιση",
|
||||
"Theme": "Θέμα",
|
||||
"General": "Γενικά",
|
||||
"Primary Base URL": "Κύρια βασική διεύθυνση URL",
|
||||
"Version": "Έκδοση",
|
||||
"Version": "Εκδοχή",
|
||||
"Check Update On GitHub": "Ελέγξτε για Ενημέρωση στο GitHub",
|
||||
"List": "Λίστα",
|
||||
"Add": "Προσθήκη",
|
||||
@@ -56,7 +56,7 @@
|
||||
"Resume": "Συνέχιση",
|
||||
"Edit": "Επεξεργασία",
|
||||
"Delete": "Διαγράφη",
|
||||
"Current": "Τωρινό",
|
||||
"Current": "Τρέχον",
|
||||
"Uptime": "Χρόνος λειτουργίας",
|
||||
"Cert Exp.": "Λήξη Πιστοπ.",
|
||||
"day": "ημέρα | ημέρες",
|
||||
@@ -363,6 +363,7 @@
|
||||
"Discard": "Απορρίψει",
|
||||
"Cancel": "Ακυρο",
|
||||
"Powered by": "Με την υποστήριξη του",
|
||||
"shrinkDatabaseDescription": "Ενεργοποίηση βάσης δεδομένων VACUUM για SQLite. Εάν η βάση δεδομένων σας έχει δημιουργηθεί μετά την έκδοση 1.10.0, το AUTO_VACUUM είναι ήδη ενεργοποιημένο και αυτή η ενέργεια δεν χρειάζεται.",
|
||||
"serwersms": "SerwerSMS.pl",
|
||||
"serwersmsAPIUser": "API Username (incl. webapi_ prefix)",
|
||||
"serwersmsAPIPassword": "API κωδικός πρόσβασης",
|
||||
@@ -699,13 +700,13 @@
|
||||
"Add New Tag": "Πρόσθεσε νέα ετικέτα",
|
||||
"setupDatabaseMariaDB": "Συνδεθείτε με εξωτερική βάση δεδομένων MariaDB. Θα πρέπει να ορίσετε τα στοιχεία σύνδεσης της βάσης δεδομένων.",
|
||||
"setupDatabaseChooseDatabase": "Ποια βάση δεδομένων θέλετε να χρησιμοποιήσετε;",
|
||||
"setupDatabaseEmbeddedMariaDB": "Δεν χρειάζεται να ορίσετε τίποτε. Το Docker Image έχει ενθέσειι και διαμορφώσει μια βάση δεδομένων MariaDB για εσάς αυτόματα. Το Uptime Kuma θα συνδεθεί με αυτήν την βάση δεδομένων μέσω unix socket.",
|
||||
"setupDatabaseEmbeddedMariaDB": "Δεν χρειάζεται να ρυθμίσετε τίποτε. Η εικόνα Docker έχει ενσωματώσει και ρυθμίσει μια βάση δεδομένων MariaDB για εσάς αυτόματα. Το Uptime Kuma θα συνδεθεί με την βάση δεδομένων μέσω unix socket.",
|
||||
"setupDatabaseSQLite": "Ένα απλό αρχείο βάσης δεδομένων, προτεινόμενο για εγκαταστάσεις μικρής κλίμακας. Πρίν από την έκδοση 2.0.0, το Uptime Kuma χρησιμοποιούσε το SQLite ως την προεπιλεγμένη βάση δεδομένων.",
|
||||
"Cannot connect to the socket server": "Δεν είναι δυνατή η σύνδεση με τον διακομιστή socket",
|
||||
"Reconnecting...": "Επανασύνδεση...",
|
||||
"Reconnecting...": "Επανασύνδεση",
|
||||
"Home": "Αρχική",
|
||||
"settingUpDatabaseMSG": "Ρύθμιση της βάσης δεδομένων. Μπορεί να διαρκέσει λίγη ώρα, παρακαλώ κάνετε υπομονετικοί.",
|
||||
"dbName": "Όνομα βάσης δεδομένων",
|
||||
"settingUpDatabaseMSG": "Ρύθμιση της βάσης δεδομένων. Μπορεί να διαρκέσει λίγο, παρακαλώ να είστε υπομονετικοί.",
|
||||
"dbName": "Όνομα Βάσης Δεδομένων",
|
||||
"Invert Keyword": "Αντιστροφή Λέξης-Κλειδιού",
|
||||
"Expected Value": "Αναμενόμενη Τιμή",
|
||||
"Json Query": "Ερώτημα Json"
|
||||
|
@@ -49,20 +49,17 @@
|
||||
"Uptime": "Uptime",
|
||||
"Cert Exp.": "Cert Exp.",
|
||||
"Monitor": "Monitor | Monitors",
|
||||
"now": "now",
|
||||
"time ago": "{0} ago",
|
||||
"day": "day | days",
|
||||
"-day": "-day",
|
||||
"hour": "hour",
|
||||
"-hour": "-hour",
|
||||
"-year": "-year",
|
||||
"Response": "Response",
|
||||
"Ping": "Ping",
|
||||
"Monitor Type": "Monitor Type",
|
||||
"Keyword": "Keyword",
|
||||
"Invert Keyword": "Invert Keyword",
|
||||
"Expected Value": "Expected Value",
|
||||
"Json Query Expression": "Json Query Expression",
|
||||
"Json Query": "Json Query",
|
||||
"Friendly Name": "Friendly Name",
|
||||
"URL": "URL",
|
||||
"Hostname": "Hostname",
|
||||
@@ -82,9 +79,8 @@
|
||||
"resendEveryXTimes": "Resend every {0} times",
|
||||
"resendDisabled": "Resend disabled",
|
||||
"retriesDescription": "Maximum retries before the service is marked as down and a notification is sent",
|
||||
"ignoredTLSError": "TLS/SSL errors have been ignored",
|
||||
"ignoreTLSError": "Ignore TLS/SSL errors for HTTPS websites",
|
||||
"ignoreTLSErrorGeneral": "Ignore TLS/SSL error for connection",
|
||||
"ignoreTLSErrorGeneral": "Ignore TLS/SSL error for connection",
|
||||
"upsideDownModeDescription": "Flip the status upside down. If the service is reachable, it is DOWN.",
|
||||
"maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
||||
"Upside Down Mode": "Upside Down Mode",
|
||||
@@ -97,8 +93,6 @@
|
||||
"pushOthers": "Others",
|
||||
"programmingLanguages": "Programming Languages",
|
||||
"Save": "Save",
|
||||
"Debug": "Debug",
|
||||
"Copy": "Copy",
|
||||
"Notifications": "Notifications",
|
||||
"Not available, please setup.": "Not available, please set up.",
|
||||
"Setup Notification": "Set Up Notification",
|
||||
@@ -251,14 +245,6 @@
|
||||
"PushUrl": "Push URL",
|
||||
"HeadersInvalidFormat": "The request headers are not valid JSON: ",
|
||||
"BodyInvalidFormat": "The request body is not valid JSON: ",
|
||||
"CopyToClipboardError": "Couldn't copy to clipbard: {error}",
|
||||
"CopyToClipboardSuccess": "Copied!",
|
||||
"CurlDebugInfo": "To debug the monitor, you can either paste this into your own machines terminal or into the machines terminal which uptime kuma is running on and see what you are requesting.{newiline}Please be aware of networking differences like {firewalls}, {dns_resolvers} or {docker_networks}.",
|
||||
"firewalls": "firewalls",
|
||||
"dns resolvers": "dns resolvers",
|
||||
"docker networks": "docker networks",
|
||||
"CurlDebugInfoOAuth2CCUnsupported": "Full oauth client credential flow is not supported in {curl}.{newline}Please get a bearer token and pass it via the {oauth2_bearer} option.",
|
||||
"CurlDebugInfoProxiesUnsupported": "Proxy support in the above {curl} command is currently not implemented.",
|
||||
"Monitor History": "Monitor History",
|
||||
"clearDataOlderThan": "Keep monitor history data for {0} days.",
|
||||
"PasswordsDoNotMatch": "Passwords do not match.",
|
||||
@@ -277,7 +263,6 @@
|
||||
"Security": "Security",
|
||||
"Steam API Key": "Steam API Key",
|
||||
"Shrink Database": "Shrink Database",
|
||||
"shrinkDatabaseDescriptionSqlite": "Trigger database {vacuum} for SQLite. {auto_vacuum} is already enabled but this does not defragment the database nor repack individual database pages the way that the {vacuum} command does.",
|
||||
"Pick a RR-Type...": "Pick a RR-Type…",
|
||||
"Pick Accepted Status Codes...": "Pick Accepted Status Codes…",
|
||||
"Default": "Default",
|
||||
@@ -314,6 +299,7 @@
|
||||
"selectedMonitorCount": "Selected: {0}",
|
||||
"Check/Uncheck": "Check/Uncheck",
|
||||
"Powered by": "Powered by",
|
||||
"shrinkDatabaseDescription": "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
|
||||
"Customize": "Customize",
|
||||
"Custom Footer": "Custom Footer",
|
||||
"Custom CSS": "Custom CSS",
|
||||
@@ -455,7 +441,6 @@
|
||||
"backupOutdatedWarning": "Deprecated: Since a lot of features were added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.",
|
||||
"backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.",
|
||||
"Optional": "Optional",
|
||||
"and": "and",
|
||||
"or": "or",
|
||||
"sameAsServerTimezone": "Same as Server Timezone",
|
||||
"startDateTime": "Start Date/Time",
|
||||
@@ -603,7 +588,7 @@
|
||||
"notificationDescription": "Notifications must be assigned to a monitor to function.",
|
||||
"keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
|
||||
"invertKeywordDescription": "Look for the keyword to be absent rather than present.",
|
||||
"jsonQueryDescription": "Parse and extract specific data from the server's JSON response using JSON query or use \"$\" for the raw response, if not expecting JSON. The result is then compared to the expected value, as strings. See {0} for documentation and use {1} to experiment with queries.",
|
||||
"jsonQueryDescription": "Do a json Query against the response and check for expected value (Return value will get converted into string for comparison). Check out {0} for the documentation about the query language. A playground can be found {1}.",
|
||||
"backupDescription": "You can backup all monitors and notifications into a JSON file.",
|
||||
"backupDescription2": "Note: history and event data is not included.",
|
||||
"backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.",
|
||||
@@ -891,12 +876,8 @@
|
||||
"nostrRecipientsHelp": "npub format, one per line",
|
||||
"showCertificateExpiry": "Show Certificate Expiry",
|
||||
"noOrBadCertificate": "No/Bad Certificate",
|
||||
"cacheBusterParam": "Add the {0} parameter",
|
||||
"cacheBusterParamDescription": "Randomly generated parameter to skip caches.",
|
||||
"gamedigGuessPort": "Gamedig: Guess Port",
|
||||
"gamedigGuessPortDescription": "The port used by Valve Server Query Protocol may be different from the client port. Try this if the monitor cannot connect to your server.",
|
||||
"Message format": "Message format",
|
||||
"Send rich messages": "Send rich messages",
|
||||
"Bitrix24 Webhook URL": "Bitrix24 Webhook URL",
|
||||
"wayToGetBitrix24Webhook": "You can create a webhook by following the steps at {0}",
|
||||
"bitrix24SupportUserID": "Enter your user ID in Bitrix24. You can find out the ID from the link by going to the user's profile.",
|
||||
@@ -961,71 +942,5 @@
|
||||
"Allow Long SMS": "Allow Long SMS",
|
||||
"cellsyntSplitLongMessages": "Split long messages into up to 6 parts. 153 x 6 = 918 characters.",
|
||||
"max 15 digits": "max 15 digits",
|
||||
"max 11 alphanumeric characters": "max 11 alphanumeric characters",
|
||||
"Community String": "Community String",
|
||||
"snmpCommunityStringHelptext": "This string functions as a password to authenticate and control access to SNMP-enabled devices. Match it with your SNMP device's configuration.",
|
||||
"OID (Object Identifier)": "OID (Object Identifier)",
|
||||
"snmpOIDHelptext": "Enter the OID for the sensor or status you want to monitor. Use network management tools like MIB browsers or SNMP software if you're unsure about the OID.",
|
||||
"Condition": "Condition",
|
||||
"SNMP Version": "SNMP Version",
|
||||
"Please enter a valid OID.": "Please enter a valid OID.",
|
||||
"wayToGetThreemaGateway": "You can register for Threema Gateway {0}.",
|
||||
"threemaRecipient": "Recipient",
|
||||
"threemaRecipientType": "Recipient Type",
|
||||
"threemaRecipientTypeIdentity": "Threema-ID",
|
||||
"threemaRecipientTypeIdentityFormat": "8 characters",
|
||||
"threemaRecipientTypePhone": "Phone Number",
|
||||
"threemaRecipientTypePhoneFormat": "E.164, without leading +",
|
||||
"threemaRecipientTypeEmail": "Email Address",
|
||||
"threemaSenderIdentity": "Gateway-ID",
|
||||
"threemaSenderIdentityFormat": "8 characters, usually starts with *",
|
||||
"threemaApiAuthenticationSecret": "Gateway-ID Secret",
|
||||
"threemaBasicModeInfo": "Note: This integration uses Threema Gateway in basic mode (server-based encryption). Further details can be found {0}.",
|
||||
"apiKeysDisabledMsg": "API keys are disabled because authentication is disabled.",
|
||||
"Host Onesender": "Host Onesender",
|
||||
"Token Onesender": "Token Onesender",
|
||||
"Recipient Type": "Recipient Type",
|
||||
"Private Number": "Private Number",
|
||||
"privateOnesenderDesc": "Make sure the number phone is valid. To send message into private number phone, ex: 628123456789",
|
||||
"groupOnesenderDesc": "Make sure the GroupID is valid. To send message into Group, ex: 628123456789-342345",
|
||||
"Group ID": "Group ID",
|
||||
"wayToGetOnesenderUrlandToken": "You can get the URL and Token by going to the Onesender website. More info {0}",
|
||||
"Add Remote Browser": "Add Remote Browser",
|
||||
"New Group": "New Group",
|
||||
"Group Name": "Group Name",
|
||||
"OAuth2: Client Credentials": "OAuth2: Client Credentials",
|
||||
"Authentication Method": "Authentication Method",
|
||||
"Authorization Header": "Authorization Header",
|
||||
"Form Data Body": "Form Data Body",
|
||||
"OAuth Token URL": "OAuth Token URL",
|
||||
"Client ID": "Client ID",
|
||||
"Client Secret": "Client Secret",
|
||||
"OAuth Scope": "OAuth Scope",
|
||||
"Optional: Space separated list of scopes": "Optional: Space separated list of scopes",
|
||||
"Go back to home page.": "Go back to home page.",
|
||||
"No tags found.": "No tags found.",
|
||||
"Lost connection to the socket server.": "Lost connection to the socket server.",
|
||||
"Cannot connect to the socket server.": "Cannot connect to the socket server.",
|
||||
"SIGNL4": "SIGNL4",
|
||||
"SIGNL4 Webhook URL": "SIGNL4 Webhook URL",
|
||||
"signl4Docs": "You can find more information about how to configure SIGNL4 and how to obtain the SIGNL4 webhook URL in the {0}.",
|
||||
"Conditions": "Conditions",
|
||||
"conditionAdd": "Add Condition",
|
||||
"conditionDelete": "Delete Condition",
|
||||
"conditionAddGroup": "Add Group",
|
||||
"conditionDeleteGroup": "Delete Group",
|
||||
"conditionValuePlaceholder": "Value",
|
||||
"equals": "equals",
|
||||
"not equals": "not equals",
|
||||
"contains": "contains",
|
||||
"not contains": "not contains",
|
||||
"starts with": "starts with",
|
||||
"not starts with": "not starts with",
|
||||
"ends with": "ends with",
|
||||
"not ends with": "not ends with",
|
||||
"less than": "less than",
|
||||
"greater than": "greater than",
|
||||
"less than or equal to": "less than or equal to",
|
||||
"greater than or equal to": "greater than or equal to",
|
||||
"record": "record"
|
||||
"max 11 alphanumeric characters": "max 11 alphanumeric characters"
|
||||
}
|
||||
|
@@ -396,6 +396,7 @@
|
||||
"successMessage": "Mensaje de éxito",
|
||||
"Pick Accepted Status Codes...": "Seleccione Códigos de Estado Aceptados…",
|
||||
"Post": "Post",
|
||||
"shrinkDatabaseDescription": "Activar VACUUM para SQLite. Si tu base de datos fue creada después 1.10.0, AUTO_VACUUM ya está habilitada y esta acción no es necesaria.",
|
||||
"deleteStatusPageMsg": "¿Estas seguro que quieres eliminar esta página de estado?",
|
||||
"default": "Predeterminado",
|
||||
"enabled": "Habilitado",
|
||||
@@ -557,7 +558,7 @@
|
||||
"affectedMonitorsDescription": "Selecciona los monitores que se ven afectados por el mantenimiento actual",
|
||||
"affectedStatusPages": "Muestra este mensaje de mantenimiento en las páginas de estado seleccionadas",
|
||||
"atLeastOneMonitor": "Selecciona al menos un monitor afectado",
|
||||
"endpoint": "punto final",
|
||||
"endpoint": "endpoint",
|
||||
"promosmsPassword": "Contraseña API",
|
||||
"pushoversounds pushover": "Pushover (predeterminado)",
|
||||
"pushoversounds bike": "Bicicleta",
|
||||
@@ -654,7 +655,7 @@
|
||||
"gorush": "Gorush",
|
||||
"squadcast": "Squadcast",
|
||||
"Maintenance Time Window of a Day": "Ventana de tiempo de mantenimiento de un día",
|
||||
"Effective Date Range": "Rango de Fechas Efectivas (Opcional)",
|
||||
"Effective Date Range": "Rango de fecha efectivo (opcional)",
|
||||
"Free Mobile User Identifier": "Identificador de Usuario de Free Mobile",
|
||||
"Gateway Type": "Tipo de puerta de enlace",
|
||||
"SMSManager": "SMSManager",
|
||||
@@ -770,6 +771,7 @@
|
||||
"Json Query": "Consulta Json",
|
||||
"invertKeywordDescription": "Comprobar si la palabra clave está ausente en vez de presente.",
|
||||
"enableNSCD": "Habilitar NSCD (Demonio de Caché de Servicio de Nombres) para almacenar en caché todas las solicitudes DNS",
|
||||
"jsonQueryDescription": "Realiza una consulta JSON contra la respuesta y verifica el valor esperado (el valor de retorno se convertirá a una cadena para la comparación). Consulta {0} para obtener documentación sobre el lenguaje de consulta. Puede encontrar un espacio de prueba {1}.",
|
||||
"Request Timeout": "Tiempo de espera máximo de petición",
|
||||
"timeoutAfter": "Expirar después de {0} segundos",
|
||||
"chromeExecutableDescription": "Para usuarios de Docker, si Chromium no está instalado, puede que tarde unos minutos en ser instalado y mostrar el resultado de la prueba. Usa 1GB de espacio.",
|
||||
@@ -829,7 +831,7 @@
|
||||
"Badge value (For Testing only.)": "Valor de la insignia (Solo para pruebas.)",
|
||||
"Enable Kafka Producer Auto Topic Creation": "Habilitar la Creación Automática de Temas del Productor de Kafka",
|
||||
"noGroupMonitorMsg": "No disponible. Cree primero un monitor de grupo.",
|
||||
"wayToGetFlashDutyKey": "Puedes ir a Canal -> (Seleccionar un Canal) -> Integraciones -> Agregar una nueva integración, agregar un 'Uptime Kuma' para obtener una dirección de envío, copiar la Clave de Integración en la dirección. Para más información, por favor visita",
|
||||
"wayToGetFlashDutyKey": "Puede ir a Canal -> (Seleccionar un canal) -> Integraciones -> Agregar una nueva página de integración, agregar un 'Evento personalizado' para obtener una dirección push, copiar la clave de integración en la dirección. Para mayor información por favor visite",
|
||||
"gamedigGuessPort": "Gamedig: Adivinar el puerto",
|
||||
"gamedigGuessPortDescription": "El puerto utilizado por Valve Server Query Protocol puede ser diferente del puerto del cliente. Pruebe esto si el monitor no puede conectarse a su servidor.",
|
||||
"twilioApiKey": "Clave de la API (opcional)",
|
||||
@@ -837,7 +839,7 @@
|
||||
"styleElapsedTimeShowNoLine": "Mostrar (sin línea)",
|
||||
"styleElapsedTimeShowWithLine": "Mostrar (Con línea)",
|
||||
"webhookCustomBodyDesc": "Define un cuerpo HTTP personalizado para la petición. Las variables que puedes usar como plantillas son {msg}, {heartbeat}, y {monitor}.",
|
||||
"webhookBodyPresetOption": "Preajuste - {0}",
|
||||
"webhookBodyPresetOption": "Preajuste- {0}",
|
||||
"tailscalePingWarning": "Para utilizar el monitor Tailscale Ping, debe instalar Uptime Kuma sin Docker y también instalar el cliente Tailscale en su servidor.",
|
||||
"Bark API Version": "Versión de la API Bark",
|
||||
"monitorToastMessagesDescription": "Las notificaciones Toast para monitores desaparecen después de un tiempo dado en segundos. Establecer a -1 desactiva el tiempo de espera. Si se establece en 0, se desactivan las notificaciones.",
|
||||
@@ -845,7 +847,7 @@
|
||||
"monitorToastMessagesLabel": "Monitorizar las notificaciones Toast",
|
||||
"toastSuccessTimeout": "Tiempo de espera para notificaciones de éxito",
|
||||
"toastErrorTimeout": "Tiempo de espera para notificaciones de error",
|
||||
"setupDatabaseChooseDatabase": "¿Qué base de datos te gustaría usar?",
|
||||
"setupDatabaseChooseDatabase": "¿Qué base de datos desea utilizar?",
|
||||
"setupDatabaseEmbeddedMariaDB": "No necesitas configurar nada. Esta imagen de Docker tiene incorporado y configurado MariaDB para ti automáticamente. Uptime Kuma se conectará a esta base de datos a través de un socket Unix.",
|
||||
"setupDatabaseMariaDB": "Conectarse a una base de datos MariaDB externa. Debe configurar la información de conexión a la base de datos.",
|
||||
"setupDatabaseSQLite": "Un archivo de base de datos simple, recomendado para despliegues a pequeña escala. Antes de la versión 2.0.0, Uptime Kuma utilizaba SQLite como base de datos predeterminada.",
|
||||
@@ -855,20 +857,20 @@
|
||||
"2faEnabled": "2FA habilitado.",
|
||||
"2faDisabled": "2FA deshabilitado.",
|
||||
"liquidIntroduction": "La plantilla se logra a través del lenguaje de plantillas Liquid. Consulte {0} para obtener instrucciones de uso. Estas son las variables disponibles:",
|
||||
"templateLimitedToUpDownCertNotifications": "disponible solo para notificaciones de arriba/abajo/caducidad del certificado",
|
||||
"templateLimitedToUpDownCertNotifications": "sólo disponible para las notificaciones de LEVANTADO/CAÍDO/Caducidad de certificado",
|
||||
"emailTemplateMsg": "mensaje de la notificación",
|
||||
"emailTemplateLimitedToUpDownNotification": "disponible solo para latidos de arriba/abajo, de lo contrario, nulo",
|
||||
"emailTemplateLimitedToUpDownNotification": "sólo disponible para pulsos LEVANTADO/CAÍDO, en caso contrario null",
|
||||
"setup a new monitor group": "configurar un nuevo grupo de monitores",
|
||||
"authUserInactiveOrDeleted": "El usuario está inactivo o eliminado.",
|
||||
"2faAlreadyEnabled": "2FA ya está activado.",
|
||||
"remoteBrowsersDescription": "Los navegadores remotos son una alternativa a ejecutar Chromium localmente. Configúralos con un servicio como browserless.io o conéctalos a tu propio servidor",
|
||||
"remoteBrowsersDescription": "Los navegadores remotos son una alternativa a la ejecución local de Chromium. Configúralos con un servicio como browserless.io o conéctate a uno propio",
|
||||
"successKeyword": "Palabra clave de éxito",
|
||||
"successKeywordExplanation": "MQTT Palabra clave que se considerará como éxito",
|
||||
"Remove the expiry notification": "Eliminar la notificación de vencimiento",
|
||||
"Browser Screenshot": "Captura de pantalla del navegador",
|
||||
"emailCustomisableContent": "Contenido personalizable",
|
||||
"smtpLiquidIntroduction": "Los siguientes dos campos son personalizables a través del lenguaje de plantillas Liquid. Por favor, consulta {0} para las instrucciones de uso. Estas son las variables disponibles:",
|
||||
"leave blank for default subject": "dejar en blanco para asunto predeterminado",
|
||||
"emailCustomisableContent": "Contenidos personalizables",
|
||||
"smtpLiquidIntroduction": "Los dos campos siguientes son planificables mediante el lenguaje de plantillas Liquid. Consulte las instrucciones de uso en {0}. Estas son las variables disponibles:",
|
||||
"leave blank for default subject": "dejar en blanco para el asunto por defecto",
|
||||
"emailCustomBody": "Cuerpo personalizado",
|
||||
"successAuthChangePassword": "La contraseña se ha actualizado correctamente.",
|
||||
"successDeleted": "Eliminado con éxito.",
|
||||
@@ -893,11 +895,11 @@
|
||||
"programmingLanguages": "Lenguajes de Programación",
|
||||
"templateMsg": "mensaje de la notificación",
|
||||
"templateMonitorJSON": "objeto que describe el monitor",
|
||||
"templateLimitedToUpDownNotifications": "disponible solo para notificaciones de arriba/abajo",
|
||||
"templateLimitedToUpDownNotifications": "sólo disponible para notificaciones LEVANTADO/CAÍDO",
|
||||
"Add a new expiry notification day": "Añadir una nueva notificación de vencimiento",
|
||||
"leave blank for default body": "dejar en blanco para el cuerpo por defecto",
|
||||
"emailTemplateServiceName": "Nombre del Servicio",
|
||||
"emailTemplateHostnameOrURL": "Nombre del Host o URL",
|
||||
"emailTemplateServiceName": "Nombre del servicio",
|
||||
"emailTemplateHostnameOrURL": "Nombre del host o URL",
|
||||
"emailTemplateStatus": "Estado",
|
||||
"emailTemplateMonitorJSON": "objeto que describe el monitor",
|
||||
"openModalTo": "abrir modal a {0}",
|
||||
@@ -911,10 +913,10 @@
|
||||
"settingUpDatabaseMSG": "Configurando la base de datos. Puede tomar un tiempo, sea paciente, por favor.",
|
||||
"Search monitored sites": "Buscar sitios monitoreados",
|
||||
"statusPageSpecialSlugDesc": "Identificador único especial {0}: esta página será mostrada cuando no se proporcione ningún identificador único",
|
||||
"emailTemplateHeartbeatJSON": "objeto que describe el latido",
|
||||
"ntfyPriorityHelptextAllEvents": "Todos los eventos se envían con la máxima prioridad",
|
||||
"emailTemplateHeartbeatJSON": "Objeto que describe la señal de vida",
|
||||
"ntfyPriorityHelptextAllEvents": "Todos los eventos son enviados con la máxima prioridad",
|
||||
"ntfyPriorityHelptextAllExceptDown": "Todos los eventos son enviados con esta prioridad, excepto los eventos {0}, que tienen una prioridad de {1}",
|
||||
"templateHeartbeatJSON": "objeto que describe el latido",
|
||||
"templateHeartbeatJSON": "Objeto que describe el latido",
|
||||
"What is a Remote Browser?": "¿Qué es un Navegador Remoto?",
|
||||
"Your User ID": "Tu ID de usuario",
|
||||
"Alphanumeric (recommended)": "Alfanumérico (recomendado)",
|
||||
@@ -922,71 +924,5 @@
|
||||
"wayToGetWhapiUrlAndToken": "Puedes obtener la URL de la API y el token accediendo al canal que desee desde {0}",
|
||||
"API URL": "API URL",
|
||||
"Allow Long SMS": "Permitir SMS largo",
|
||||
"cellsyntDestination": "Número de teléfono del destinatario en formato internacional precedido de 00, seguido del código de país, ej. 00447920110000 para el número del Reino Unido 07920 110 000 (máximo 17 dígitos en total). Máximo 25.000 destinatarios, separados por comas, por solicitud HTTP.",
|
||||
"Refresh Interval Description": "La página de estado se refrescará cada {0} segundos",
|
||||
"Refresh Interval": "Intervalo de refresco",
|
||||
"ignoreTLSErrorGeneral": "Ignorar errores SSL/TLS durante la conexión",
|
||||
"documentationOf": "Documentación de {0}",
|
||||
"wayToGetHeiiOnCallDetails": "Cómo obtener el ID del Disparador y las Claves API se explica en la {documentación}",
|
||||
"Command": "Comando",
|
||||
"wayToGetThreemaGateway": "Puedes registrarte para Threema Gateway {0}.",
|
||||
"threemaRecipient": "Destinatario",
|
||||
"threemaRecipientType": "Tipo de Destinatario",
|
||||
"threemaRecipientTypeIdentity": "ID de Threema",
|
||||
"threemaRecipientTypeIdentityFormat": "8 caracteres",
|
||||
"threemaRecipientTypePhone": "Número de Teléfono",
|
||||
"threemaRecipientTypeEmail": "Dirección de Correo Electrónico",
|
||||
"threemaSenderIdentity": "ID de Gateway",
|
||||
"threemaSenderIdentityFormat": "8 caracteres, generalmente comienza con *",
|
||||
"Host URL": "URL del anfitrión",
|
||||
"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": "Ingresa el nombre del host del servidor al que deseas conectarte o {localhost} si deseas usar un {local_mta}",
|
||||
"smspartnerPhoneNumberHelptext": "El número debe estar en el formato internacional {0}, {1}. Múltiples números deben estar separados por {2}",
|
||||
"smspartnerSenderName": "Nombre del remitente de SMS",
|
||||
"smspartnerApiurl": "Puedes encontrar tu clave API en tu panel de control en {0}",
|
||||
"smspartnerPhoneNumber": "Número(s) de teléfono",
|
||||
"max 11 alphanumeric characters": "máximo 11 caracteres alfanuméricos",
|
||||
"gtxMessagingFromHint": "En teléfonos móviles, tus destinatarios ven el TPOA mostrado como el remitente del mensaje. Se permiten hasta 11 caracteres alfanuméricos, un código corto, el código largo local o números internacionales ({e164}, {e212} o {e214})",
|
||||
"cellsyntOriginatortypeAlphanumeric": "Cadena alfanumérica (máximo 11 caracteres alfanuméricos). Los destinatarios no pueden responder al mensaje.",
|
||||
"cellsyntOriginatortypeNumeric": "Valor numérico (máximo 15 dígitos) con el número de teléfono en formato internacional sin el prefijo 00 (por ejemplo, el número del Reino Unido 07920 110 000 debe establecerse como 447920110000). Los destinatarios pueden responder al mensaje.",
|
||||
"Originator type": "Tipo de originador",
|
||||
"Telephone number": "Número de teléfono",
|
||||
"Mentioning": "Mencionando",
|
||||
"Don't mention people": "No mencionar a las personas",
|
||||
"Mention group": "Mencionar a {group}",
|
||||
"Bitrix24 Webhook URL": "URL del Webhook de Bitrix24",
|
||||
"wayToGetBitrix24Webhook": "Puedes crear un webhook siguiendo los pasos en {0}",
|
||||
"gtxMessagingApiKeyHint": "Puedes encontrar tu clave API en: My Routing Accounts > Show Account Information > API Credentials > REST API (v2.x)",
|
||||
"Originator": "Originador",
|
||||
"bitrix24SupportUserID": "Ingresa tu ID de usuario en Bitrix24. Puedes encontrar el ID en el enlace al ir al perfil del usuario.",
|
||||
"wayToWriteWhapiRecipient": "El número de teléfono con el prefijo internacional, pero sin el signo más al inicio ({0}), el ID de Contacto ({1}) o el ID de Grupo ({2}).",
|
||||
"From Phone Number / Transmission Path Originating Address (TPOA)": "Número de Teléfono del Remitente / Dirección de Origen de la Ruta de Transmisión (TPOA)",
|
||||
"To Phone Number": "Al Número de Teléfono",
|
||||
"Select message type": "Seleccionar tipo de mensaje",
|
||||
"Send to channel": "Enviar al canal",
|
||||
"max 15 digits": "máximo 15 dígitos",
|
||||
"mongodbCommandDescription": "Ejecuta un comando de MongoDB contra la base de datos. Para obtener información sobre los comandos disponibles, consulta la {documentación}",
|
||||
"whapiRecipient": "Número de Teléfono / ID de Contacto / ID de Grupo",
|
||||
"cellsyntSplitLongMessages": "Divide mensajes largos en hasta 6 partes. 153 x 6 = 918 caracteres.",
|
||||
"receiverSevenIO": "Número receptor",
|
||||
"apiKeySevenIO": "Clave API de SevenIO",
|
||||
"wayToGetSevenIOApiKey": "Visita el panel de control en app.seven.io > developer > api key > el botón verde de agregar",
|
||||
"senderSevenIO": "Número o nombre del remitente",
|
||||
"gtxMessagingToHint": "Formato internacional, con el signo \"+\" al inicio ({e164}, {e212} o {e214})",
|
||||
"locally configured mail transfer agent": "agente de transferencia de correo configurado localmente",
|
||||
"wayToGetDiscordThreadId": "Obtener un ID de hilo / publicación en el foro es similar a obtener un ID de canal. Lee más sobre cómo obtener IDs {0}",
|
||||
"smspartnerSenderNameInfo": "Debe tener entre 3..=11 caracteres normales",
|
||||
"receiverInfoSevenIO": "Si el número receptor no está ubicado en Alemania, debes agregar el código de país delante del número (por ejemplo, para el código de país 1 de EE. UU. usa 117612121212 en lugar de 017612121212)",
|
||||
"callMeBotGet": "Aquí puedes generar un endpoint para {0}, {1} y {2}. Ten en cuenta que podrías recibir limitaciones de tasa. Las limitaciones de tasa parecen ser: {3}",
|
||||
"cellsyntOriginator": "Visible en el teléfono móvil del destinatario como originador del mensaje. Los valores permitidos y la función dependen del parámetro originatortype.",
|
||||
"threemaRecipientTypePhoneFormat": "E.164, sin el signo + al inicio",
|
||||
"threemaApiAuthenticationSecret": "Clave Secreta del Gateway-ID",
|
||||
"threemaBasicModeInfo": "Nota: Esta integración utiliza Threema Gateway en modo básico (encriptación basada en servidor). Puedes encontrar más detalles en {0}.",
|
||||
"apiKeysDisabledMsg": "Las claves API están desactivadas porque la autenticación está desactivada.",
|
||||
"Channel access token (Long-lived)": "Token de acceso al canal (de larga duración)",
|
||||
"Create new forum post": "Crear nueva publicación en el foro",
|
||||
"postToExistingThread": "Publicar en hilo / publicación existente",
|
||||
"forumPostName": "Nombre de la publicación en el foro",
|
||||
"threadForumPostID": "ID del hilo / publicación en el foro",
|
||||
"e.g. {discordThreadID}": "por ejemplo, {discordThreadID}",
|
||||
"whatHappensAtForumPost": "Crear una nueva publicación en el foro. Esto NO publica mensajes en una publicación existente. Para publicar en una publicación existente usa \"{option}\""
|
||||
"cellsyntDestination": "Número de teléfono del destinatario en formato internacional precedido de 00, seguido del código de país, ej. 00447920110000 para el número del Reino Unido 07920 110 000 (máximo 17 dígitos en total). Máximo 25.000 destinatarios, separados por comas, por solicitud HTTP."
|
||||
}
|
||||
|
@@ -360,6 +360,7 @@
|
||||
"Discard": "Baztertu",
|
||||
"Cancel": "Ezeztatu",
|
||||
"Powered by": "Honekin egina:",
|
||||
"shrinkDatabaseDescription": "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
|
||||
"serwersms": "SerwerSMS.pl",
|
||||
"serwersmsAPIUser": "API erabiltzailea (webapi_ aurre-hizkia barne)",
|
||||
"serwersmsAPIPassword": "API pasahitza",
|
||||
|
144
src/lang/fa.json
144
src/lang/fa.json
@@ -76,7 +76,7 @@
|
||||
"Accepted Status Codes": "وضعیتهای (Status Code) های قابل قبول",
|
||||
"Save": "ذخیره",
|
||||
"Notifications": "اطلاعرسانیها",
|
||||
"Not available, please setup.": "هیچ موردی موجود نیست، اولین مورد را راهاندازی کنید.",
|
||||
"Not available, please setup.": "هیچ موردی موجود نیست، اولین مورد را راه اندازی کنید.",
|
||||
"Setup Notification": "راه اندازی اطلاعرسانی",
|
||||
"Light": "روشن",
|
||||
"Dark": "تاریک",
|
||||
@@ -335,6 +335,7 @@
|
||||
"About": "درباره آپتایم کوما",
|
||||
"wayToGetCloudflaredURL": "(دریافت Cloudflared از {0})",
|
||||
"cloudflareWebsite": "وب سایت کلادفلر",
|
||||
"shrinkDatabaseDescription": "تریگر VACUUM برای SQLite. اگر دیتابیس شما بعد از 1.10.0 ایجاد شده باشد، AUTO_VACUUM قبلاً فعال شده است و لازم نیست این عمل انجام شود. (Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.).",
|
||||
"Message:": "پیام:",
|
||||
"HTTP Headers": "هدر های HTTP",
|
||||
"Bot Token": "توکن بات",
|
||||
@@ -385,7 +386,7 @@
|
||||
"Switch to Dark Theme": "تغییر به حالت تیره",
|
||||
"Show Tags": "نمایش تگ ها",
|
||||
"Hide Tags": "مخفی سازی تگ ها",
|
||||
"Description": "توضیحات",
|
||||
"Description": "توضحیات",
|
||||
"Custom CSS": "CSS اختصاصی",
|
||||
"deleteStatusPageMsg": "آیا بابت حذف این استاتوس پیچ مطمئن هستید؟",
|
||||
"Proxies": "پروکسی ها",
|
||||
@@ -758,10 +759,11 @@
|
||||
"filterActive": "فعال",
|
||||
"webhookCustomBodyDesc": "یک بدنه HTTP سفارشی برای ریکوئست تعریف کنید. متغیر های قابل استفاده: {msg}, {heartbeat}, {monitor}.",
|
||||
"tailscalePingWarning": "برای استفاده از Tailscale Ping monitor، شما باید آپتایم کوما را بدون استفاده از داکر و همچنین Tailscale client را نیز بر روی سرور خود نصب داشته باشید.",
|
||||
"jsonQueryDescription": "یک کوئری json در برابر پاسخ انجام دهید و مقدار مورد انتظار را (مقدار برگشتی برای مقایسه به رشته تبدیل می شود). برای مستندات درباره زبان کوئری، {0} مشاهده کنید. همچنین محیط تست را میتوانید در {1} پیدا کنید.",
|
||||
"Enter the list of brokers": "لیست بروکر هارا وارد کنید",
|
||||
"Enable Kafka Producer Auto Topic Creation": "فعال سازی ایجاپ موضوع اتوماتیک تهیه کننده",
|
||||
"Secret AccessKey": "کلید محرمانه AccessKey",
|
||||
"wayToGetFlashDutyKey": "می توانید به کانال -> (انتخاب یک کانال) -> یکپارچه سازی -> یکپارچه سازی جدید بروید، یک \"Uptime Kuma\" را برای دریافت یک آدرس پوش اضافه کنید، کلید یکپارچه سازی را در آدرس کپی کنید. برای اطلاعات بیشتر لطفا مراجعه کنید به",
|
||||
"wayToGetFlashDutyKey": "می توانید به کانال -> (انتخاب یک کانال) -> یکپارچه سازی -> صفحه یکپارچه سازی جدید بروید، یک \"رویداد سفارشی\" را برای دریافت یک آدرس فشار اضافه کنید، کلید یکپارچه سازی را در آدرس کپی کنید. برای اطلاعات بیشتر لطفا مراجعه کنید به",
|
||||
"showCertificateExpiry": "نمایش زمان به پایان رسیدن اعتبار سرتیفیکیت",
|
||||
"gamedigGuessPortDescription": "پورت مورد استفاده توسط پروتکل Query Valve Server ممکن است با پورت مشتری متفاوت باشد. اگر مانیتور نمی تواند به سرور شما متصل شود، این را امتحان کنید.",
|
||||
"invertKeywordDescription": "دنبال کلمات کلیدی ناموجود باشید تا آنهایی که موجود است.",
|
||||
@@ -810,8 +812,8 @@
|
||||
"Expected Value": "مقدار مورد انتظار",
|
||||
"Json Query": "کوئری جیسون",
|
||||
"Saved.": "ذخیره شده.",
|
||||
"setupDatabaseChooseDatabase": "از چه دیتابیسی میخواهید استفاده کنید؟",
|
||||
"setupDatabaseEmbeddedMariaDB": "شما نیازی نیست چیزی را تنظیم کنید . این Image داکر، MariaDB را به طور خودکار برای شما جاسازی و پیکربندی کرده است. آپتایم کوما از طریق سوکت یونیکس به این دیتابیس متصل می شود.",
|
||||
"setupDatabaseChooseDatabase": "از چه دیتابیسی میخواهید استفاده کنید؟",
|
||||
"setupDatabaseEmbeddedMariaDB": "شما نیازی نیست چیزی را تنظیم کنید . این Image داکر MariaDB را به طور خودکار برای شما جاسازی و پیکربندی کرده است. آپتایم کوما از طریق سوکت یونیکس به این دیتابیس متصل می شود.",
|
||||
"setupDatabaseSQLite": "یک فایل دیتابیس ساده که برای استقرار در مقیاس کوچک توصیه می شود. قبل از نسخه 2.0.0، Uptime Kuma از SQLite به عنوان دیتابیس پیش فرض استفاده می کرد.",
|
||||
"enableNSCD": "فعال سازی NSCD (Name Service Cache Daemon) برای کش کردن تمام ریکوئست های DNS",
|
||||
"setupDatabaseMariaDB": "به یک دیتابیس خارجی MariaDB متصل شوید. شما باید اطلاعات اتصال دیتابیس را تنظیم کنید.",
|
||||
@@ -890,135 +892,5 @@
|
||||
"documentationOf": "کلید مطلب ها",
|
||||
"wayToGetHeiiOnCallDetails": "روش دریافت ایدی کلید و کلید ای پی آی درمطلب توضیح داده شده است",
|
||||
"Channel access token (Long-lived)": "توکن دسترسی به کانال",
|
||||
"Your User ID": "آیدی حساب کاربری شما",
|
||||
"ignoreTLSErrorGeneral": "نادیده گرفتن ارور TLS/SSL برای برقراری ارتباط",
|
||||
"Originator": "ایجاد کننده",
|
||||
"cellsyntOriginatortypeNumeric": "مقدار عددی (حداکثر 15 رقم) با شماره تلفن در قالب بین المللی بدون 00 اول (به عنوان مثال شماره 07920 110 000 بریتانیا باید به عنوان 447920110000 تنظیم شود). گیرندگان می توانند به پیام پاسخ دهند.",
|
||||
"threemaRecipient": "گیرنده",
|
||||
"threemaRecipientType": "نوع گیرنده",
|
||||
"threemaRecipientTypeIdentity": "شناسه Threema",
|
||||
"threemaRecipientTypeIdentityFormat": "8 کاراکتر",
|
||||
"threemaRecipientTypePhone": "شماره موبایل",
|
||||
"threemaRecipientTypePhoneFormat": "E.164، بدون پیشوند +",
|
||||
"threemaRecipientTypeEmail": "آدرس ایمیل",
|
||||
"threemaSenderIdentity": "شناسه ورودی",
|
||||
"threemaApiAuthenticationSecret": "کلید شناسه ورودی",
|
||||
"threemaBasicModeInfo": "توجه: این ادغام از ورودی Threema در حالت اولیه (رمزگذاری مبتنی بر سرور) استفاده می کند. جزئیات بیشتر را می توان در {0} یافت.",
|
||||
"smspartnerApiurl": "شما میتوانید کلید API را در داشبورد خود در {0} پیدا کنید",
|
||||
"smspartnerPhoneNumber": "شماره موبایل(ها)",
|
||||
"Host URL": "آدرس هاست",
|
||||
"gtxMessagingApiKeyHint": "شما میتوانید کلید API خود را در این آدرس پیدا کنید: My Routing Accounts > Show Account Information > API Credentials > REST API (v2.x)",
|
||||
"Bitrix24 Webhook URL": "آدرس وب هوک Bitrix24",
|
||||
"wayToGetBitrix24Webhook": "شما می توانید با دنبال کردن مراحل در {0} یک وب هوک ایجاد کنید",
|
||||
"whapiRecipient": "شماره تلفن / شناسه ارتباط / شناسه گروه",
|
||||
"gtxMessagingToHint": "قالب بینالمللی، با پیشوند \"+\" ({e164}، {e212} یا {e214})",
|
||||
"Mentioning": "منشن کردن",
|
||||
"Send to channel": "ارسال به کانال",
|
||||
"forumPostName": "نام پست انجمن",
|
||||
"threadForumPostID": "شناسه پست رشته / انجمن",
|
||||
"e.g. {discordThreadID}": "به عنوان مثال: {discordThreadID}",
|
||||
"wayToGetDiscordThreadId": "دریافت شناسه پست / انجمن شبیه به دریافت شناسه کانال است. درباره نحوه دریافت شناسه بیشتر بخوانید {0}",
|
||||
"senderSevenIO": "شماره یا نام ارسال کننده",
|
||||
"receiverSevenIO": "شماره گیرنده",
|
||||
"Don't mention people": "این آدم هارا منشن نکن",
|
||||
"Mention group": "منشن {group}",
|
||||
"smspartnerSenderName": "نام ارسال کننده اس ام اس",
|
||||
"apiKeySevenIO": "کلید SevenIO API",
|
||||
"API URL": "آدرس API",
|
||||
"callMeBotGet": "اینجا شما می توانید یک نقطه پایانی برای {0}، {1} و {2} ایجاد کنید. به خاطر داشته باشید که ممکن است ریت لیمیت (Rate Limit) دریافت کنید. به نظر می رسد ریت لیمیت ها عبارتند از: {3}",
|
||||
"To Phone Number": "به شماره موبایل",
|
||||
"Originator type": "نوع Originator",
|
||||
"Destination": "مقصد",
|
||||
"Allow Long SMS": "اجازه ارسال پیام بلند",
|
||||
"max 15 digits": "حداکثر 15 عدد",
|
||||
"max 11 alphanumeric characters": "حداکثر 11 کاراکتر الفبا و اعداد",
|
||||
"locally configured mail transfer agent": "نماینده انتقال نامه به صورت لوکال",
|
||||
"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": "یا آدرس هاست سروری که میخواهید به آن متصل شوید را وارد کنید یا {localhost} اگر میخواهید از یک {local_mta} استفاده کنید",
|
||||
"Refresh Interval": "وقفه بین بارگزاری مجدد",
|
||||
"Refresh Interval Description": "صفحه وضعیت هر {0} ثانیه یک بار سایت را بهروزرسانی میکند",
|
||||
"Select message type": "انتخاب نوع پیام",
|
||||
"Create new forum post": "ایحاد یک پست جدید در انجمن",
|
||||
"postToExistingThread": "پست در یک رشته / انجمن موجود",
|
||||
"whatHappensAtForumPost": "ایحاد یک پست جدید در انجمن. این گزینه در یک پست موجود پیامی را ارسال نمی کند. برای پست در یک پیام موجود از گزینه\"{option}\" استفاده کنید",
|
||||
"smspartnerPhoneNumberHelptext": "شماره باید در قالب بین المللی {0}، {1} باشد. شماره ها باید با {2} از هم جدا شوند",
|
||||
"smspartnerSenderNameInfo": "باید بین 3..=11 کاراکتر معمولی باشد",
|
||||
"bitrix24SupportUserID": "لطفا شناسه کاربری خود را در Bitrix24 وارد کنید. با رفتن به پروفایل کاربر می توانید شناسه را از لینک دریافت کنید.",
|
||||
"Command": "دستور",
|
||||
"mongodbCommandDescription": "یک دستور MongoDB را در پایگاه داده اجرا کنید. برای اطلاعات در مورد دستورات موجود، {documentation} را بررسی کنید",
|
||||
"wayToGetSevenIOApiKey": "دز داشبورد به آدرس app.seven.io > developer > api key بر روی دکمه سبز اضافه کلیک کنید",
|
||||
"receiverInfoSevenIO": "اگر شماره دریافت کننده در آلمان نیست، شما باید کد کشور را در مقابل شماره اضافه کنید (به عنوان مثال برای کد کشور 1 از ایالات متحده به جای 017612121212 از 117612121212 استفاده کنید)",
|
||||
"wayToWriteWhapiRecipient": "شماره تلفن با پیشوند بینالمللی، اما بدون علامت مثبت در شروع ({0})، شناسه تماس ({1}) یا شناسه گروه ({2}).",
|
||||
"wayToGetWhapiUrlAndToken": "شما با رفتن به کانال مورد نظر خود از {0} می توانید URL API و توکن را دریافت کنید",
|
||||
"gtxMessagingFromHint": "در موبایل ها، گیرندگان شما TPOA را که به عنوان فرستنده پیام نمایش داده می شود، می بینند. حداکثر 11 کاراکتر (فقط الفبا و اعداد)، یک کد کوتاه، کد طولانی محلی یا اعداد بینالمللی مجاز هستند ({e164}، {e212} یا {e214})",
|
||||
"cellsyntOriginator": "روی تلفن همراه گیرنده به عنوان Originator پیام قابل مشاهده است. مقادیر و تابع مجاز به پارامتر مبدا بستگی دارد.",
|
||||
"cellsyntDestination": "شماره تلفن گیرنده با استفاده از فرمت بینالمللی با 00 اصلی و به دنبال آن کد کشور، به عنوان مثال. 00447920110000 برای بریتانیا شماره 07920 110 000 (حداکثر 17 رقم در کل). حداکثر 25000 گیرنده جدا شده با کاما در هر درخواست HTTP.",
|
||||
"cellsyntSplitLongMessages": "پیام های طولانی را به حداکثر 6 قسمت تقسیم کنید. 153 x 6 = 918 کاراکتر.",
|
||||
"wayToGetThreemaGateway": "شما می توانید برای Threema Gateway {0} ثبت نام کنید.",
|
||||
"threemaSenderIdentityFormat": "8 کاراکتر، معمولا با * (ستاره) شروع می شود",
|
||||
"apiKeysDisabledMsg": "کلیدهای API غیرفعال هستند زیرا احراز هویت غیرفعال است.",
|
||||
"From Phone Number / Transmission Path Originating Address (TPOA)": "از شماره تلفن / آدرس مبدأ مسیر انتقال (TPOA)",
|
||||
"Alphanumeric (recommended)": "الفبا و اعداد (پیشنهاد شده)",
|
||||
"Telephone number": "شماره تلفن",
|
||||
"cellsyntOriginatortypeAlphanumeric": "الفبا و اعداد (حداکثر 11 کاراکتر). گیرندگان نمی توانند به پیام پاسخ دهند.",
|
||||
"cacheBusterParam": "افزودن پارامتر {0}",
|
||||
"Private Number": "شماره شخصی",
|
||||
"jsonQueryDescription": "با استفاده از JSON query پاسخ JSON سرور را تحلیل و استخراج کنید یا از «$» برای پاسخ خام استفاده کنید، در صورتی که JSON نباشد. سپس نتیجه با مقدار مورد نظر به عنوان رشته مقایسه میشود. برای مستندات به {0} بروید و از {1} برای آزمایش کوئریها استفاده کنید.",
|
||||
"now": "الان",
|
||||
"-year": "-سال",
|
||||
"time ago": "{0} پیش",
|
||||
"and": "و",
|
||||
"Condition": "شرط",
|
||||
"SNMP Version": "نسخه SNMP",
|
||||
"privateOnesenderDesc": "مطمئن شوید که شماره تلفن درست است. برای ارسال پیام به شماره شخصی، مثل: 09123456789",
|
||||
"Group ID": "شناسه گروه",
|
||||
"Add Remote Browser": "افزودن مرورگر ریموت",
|
||||
"New Group": "گروه جدید",
|
||||
"Group Name": "نام گروه",
|
||||
"Authentication Method": "روش احراز هویت",
|
||||
"Client ID": "شناسه کلاینت",
|
||||
"Client Secret": "رمز کلاینت",
|
||||
"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",
|
||||
"Conditions": "شرایط",
|
||||
"conditionAdd": "افزودن شرط",
|
||||
"conditionDelete": "حذف شرط",
|
||||
"conditionAddGroup": "افزودن گروه",
|
||||
"conditionDeleteGroup": "حذف گروه",
|
||||
"conditionValuePlaceholder": "مقدار",
|
||||
"equals": "برابر",
|
||||
"not equals": "نابرابر",
|
||||
"contains": "شامل",
|
||||
"starts with": "شروع با",
|
||||
"not starts with": "عدم شروع با",
|
||||
"not contains": "شامل نباشد",
|
||||
"ends with": "پایان با",
|
||||
"not ends with": "عدم پایان با",
|
||||
"less than": "کمتر از",
|
||||
"greater than": "بیشتر از",
|
||||
"less than or equal to": "کمتر یا مساوی با",
|
||||
"greater than or equal to": "بیشتر یا مساوی با",
|
||||
"record": "رکورد",
|
||||
"snmpCommunityStringHelptext": "این رشته به عنوان پسورد برای احراز هویت و کنترل دسترسی به دستگاههای آماده SNMP استفاده میشود. این پسورد را با توجه به تنظیمات دستگاه SNMP خود مطابقت دهید.",
|
||||
"snmpOIDHelptext": "OID برای سنسور یا وضعیتی که میخواهید مانیتور کنید را وارد کنید. اگر درباره مقدار OID اطمینان ندارید از ابزارهای مدیریت شبکهای مانند MIB یا SNMP استفاده کنید.",
|
||||
"Json Query Expression": "عبارت کوئری JSON",
|
||||
"cacheBusterParamDescription": "پارامتر تولید شده رندوم برای رد کردن کش.",
|
||||
"Community String": "رشته انجمن",
|
||||
"OID (Object Identifier)": "OID (شناسه شئ)",
|
||||
"Please enter a valid OID.": "لطفا یک OID معتبر وارد کنید.",
|
||||
"Host Onesender": "هاست Onesender",
|
||||
"Token Onesender": "توکن Onesender",
|
||||
"Recipient Type": "نوع دریافت کننده",
|
||||
"groupOnesenderDesc": "از معتبر بودن GroupID اطمینان حاصل کنید. برای ارسال پیام در گروه، برای مثال: 628123456789-342345",
|
||||
"wayToGetOnesenderUrlandToken": "برای دریافت URL و توکن میتوانید به وبسایت Onesender مراجعه گنید. اطلاعات بیشتر {0}",
|
||||
"OAuth2: Client Credentials": "OAuth2: گواهی مشتری",
|
||||
"Authorization Header": "هدر Authorization",
|
||||
"Form Data Body": "از اطلاعات بدنه",
|
||||
"OAuth Token URL": "URL توکن OAuth",
|
||||
"OAuth Scope": "اسکوپ OAuth",
|
||||
"Optional: Space separated list of scopes": "اختیاری: لیست جدا شده با فاصله از اسکوپها",
|
||||
"signl4Docs": "شما میتوانید اطلاعات بیشتر در رابطه با نحوه تنظیم SIGNL4 و به دست آوردن URL وبهوک SIGNL4 را در {0} پیدا کنید."
|
||||
"Your User ID": "آیدی حساب کاربری شما"
|
||||
}
|
||||
|
@@ -328,7 +328,7 @@
|
||||
"Query": "Tiedustelu (Query)",
|
||||
"settingsCertificateExpiry": "TLS-varmenteen vanheneminen",
|
||||
"certificationExpiryDescription": "HTTPS-seuraimet käynnistävät ilmoituksen, kun TLS-varmenne vanhenee:",
|
||||
"Setup Docker Host": "Määritä Docker-isäntä",
|
||||
"Setup Docker Host": "Asenna Docker-isäntä",
|
||||
"Connection Type": "Yhteystyyppi",
|
||||
"tcp": "TCP / HTTP",
|
||||
"Docker Container": "Docker-kontti",
|
||||
@@ -554,7 +554,7 @@
|
||||
"High": "Korkea",
|
||||
"Topic": "Aihe",
|
||||
"WeCom Bot Key": "WeCom-bottiavain",
|
||||
"Setup Proxy": "Määritä välityspalvelin",
|
||||
"Setup Proxy": "Asenna välityspalvelin",
|
||||
"Proxy Protocol": "Välityspalvelinprotokolla",
|
||||
"Proxy Server": "Välityspalvelin",
|
||||
"matrix": "Matriisi",
|
||||
@@ -640,6 +640,7 @@
|
||||
"light": "vaalea",
|
||||
"Switch to Light Theme": "Vaihda vaaleaan teemaan",
|
||||
"Powered by": "Voimanlähteenä",
|
||||
"shrinkDatabaseDescription": "Käynnistä tietokannan VACUUM SQLitelle. Jos tietokanta on luotu 1.10.0:n jälkeen, AUTO_VACUUM on jo käytössä eikä tätä toimintoa tarvita.",
|
||||
"Accept characters:": "Hyväksy merkit:",
|
||||
"New Status Page": "Uusi tilasivu",
|
||||
"wayToGetCloudflaredURL": "(Lataa cloudflared osoitteesta {0})",
|
||||
@@ -791,6 +792,7 @@
|
||||
"emailTemplateLimitedToUpDownNotification": "saatavilla vain YLÖS/ALAS sydämensykkeille, muulloin null",
|
||||
"Your User ID": "Käyttäjätunnuksesi",
|
||||
"invertKeywordDescription": "Etsi puuttuvaa avainsanaa.",
|
||||
"jsonQueryDescription": "Suorita JSON-kysely vastaukselle ja tarkista odotettu arvo (Paluuarvo muutetaan merkkijonoksi vertailua varten). Katso kyselykielen ohjeita osoitteesta {0}. Leikkikenttä löytyy osoitteesta {1}.",
|
||||
"Bark API Version": "Bark API-versio",
|
||||
"Notify Channel": "Ilmoitus kanavalle",
|
||||
"aboutNotifyChannel": "Ilmoitus kanavalle antaa työpöytä- tai mobiili-ilmoituksen kaikille kanavan jäsenille; riippumatta ovatko he paikalla vai poissa.",
|
||||
@@ -969,84 +971,5 @@
|
||||
"threadForumPostID": "Langan / Keskustelualueketjun ID",
|
||||
"e.g. {discordThreadID}": "esim. {discordThreadID}",
|
||||
"whatHappensAtForumPost": "Luo uusi keskustelualueketju. Tämä EI jatka edellistä ketjua. Lähettääksesi olemassaolevaan ketjuun käytä toimintoa \"{option}\"",
|
||||
"Command": "Komento",
|
||||
"smspartnerApiurl": "Löydät API-avaimesi kojelaudalta osoitteesta {0}",
|
||||
"smspartnerPhoneNumber": "Puhelinnumero(t)",
|
||||
"smspartnerPhoneNumberHelptext": "Numeron täytyy olla kansainvälisessä muodossa {0},{1}. Jos numeroita on useita, ne täytyy erottaa merkillä {2}",
|
||||
"smspartnerSenderName": "SMS-lähettäjän nimi",
|
||||
"smspartnerSenderNameInfo": "Täytyy sisältää 3..=11 tavallista merkkiä",
|
||||
"threemaRecipient": "Vastaanottaja",
|
||||
"threemaRecipientType": "Vastaanottajan tyyppi",
|
||||
"threemaRecipientTypeIdentityFormat": "8 merkkiä",
|
||||
"threemaRecipientTypePhone": "Puhelinnumero",
|
||||
"threemaRecipientTypePhoneFormat": "E.164, ilman etuliitettä +",
|
||||
"threemaRecipientTypeEmail": "Sähköpostiosoite",
|
||||
"threemaSenderIdentity": "Yhdyskäytävän tunnus (Gateway-ID)",
|
||||
"threemaApiAuthenticationSecret": "Gateway-ID salaisuus",
|
||||
"wayToGetThreemaGateway": "Voit rekisteröityä Threema Gateway:n käyttäjäksi {0}.",
|
||||
"threemaRecipientTypeIdentity": "Threema-tunnus (Threema-ID)",
|
||||
"threemaSenderIdentityFormat": "8 merkkiä, alkaa useimmiten merkillä *",
|
||||
"threemaBasicModeInfo": "Huomio: Tämä integraatio käyttää Threema Gateway:tä perustilassa (palvelinpohjainen salaus). Lisätietoja löytyy {0}.",
|
||||
"apiKeysDisabledMsg": "API-avaimet eivät ole käytössä koska tunnistautuminen ei ole käytössä.",
|
||||
"snmpCommunityStringHelptext": "Tämä merkkijono toimii salasanana ja pääsyoikeutena SNMP-laitteisiin. Aseta se samaksi kuin SNMP-laitteen asetuksissa.",
|
||||
"privateOnesenderDesc": "Varmista, että puhelinnumero on kelvollinen. Lähettääksesi viestin yksityiseen numeroon esim. 628123456789",
|
||||
"Authorization Header": "Tunnistautumisen otsikko (Authorization Header)",
|
||||
"Optional: Space separated list of scopes": "Valinnainen: Luettelo näkyvyysalueista (scope) välilyönnillä erotettuna",
|
||||
"jsonQueryDescription": "Valitse sisältö palvelimen JSON-vastauksesta käyttämällä JSON-kyselyä tai käytä alkuperäistä sisältöä merkillä \"$\", jos palvelimen vastaus ei ole JSON-muodossa. Tämän jälkeen sisältöä verrataan odotettuun arvoon (merkkijonona). Katso kyselykielen ohjeita osoitteesta {0}. Leikkikenttä löytyy osoitteesta {1}.",
|
||||
"now": "nyt",
|
||||
"time ago": "{0} sitten",
|
||||
"-year": "-vuosi",
|
||||
"Json Query Expression": "Json-kyselylauseke",
|
||||
"and": "ja",
|
||||
"cacheBusterParam": "Lisää parametri {0}",
|
||||
"cacheBusterParamDescription": "Satunnaisesti luotu parametri välimuistien ohittamiseksi.",
|
||||
"Community String": "Yhteisömerkkijono",
|
||||
"OID (Object Identifier)": "OID (Objektin tunniste)",
|
||||
"snmpOIDHelptext": "Syötä OID anturille tai tilalle jota haluat valvoa. Käytä verkonvalvontatyökaluja kuten MIB-selaimia tai SNMP-ohjelmistoja jos olet epävarma OID:n valinnasta.",
|
||||
"Condition": "Ehto",
|
||||
"SNMP Version": "SNMP-versio",
|
||||
"Please enter a valid OID.": "Ole hyvä ja syötä kelvollinen OID.",
|
||||
"Host Onesender": "OneSender-isäntä",
|
||||
"Token Onesender": "OneSender-tokeni",
|
||||
"Recipient Type": "Vastaanottajan tyyppi",
|
||||
"Private Number": "Yksityinen numero",
|
||||
"Group ID": "Ryhmän tunnus (Group ID)",
|
||||
"groupOnesenderDesc": "Varmista, että ryhmän tunnus (Group ID) on kelvollinen. Lähettääksesi viestin ryhmälle, esim. 628123456789-342345",
|
||||
"wayToGetOnesenderUrlandToken": "Saat URL-osoitteen ja tokenin vierailemalla OneSenderin verkkosivulla. Lisätietoja osoitteesta {0}",
|
||||
"Add Remote Browser": "Lisää etäselain",
|
||||
"New Group": "Uusi ryhmä",
|
||||
"Group Name": "Ryhmän nimi",
|
||||
"OAuth2: Client Credentials": "OAuth2: Asiakkaan tunnukset",
|
||||
"Authentication Method": "Tunnistustapa",
|
||||
"Form Data Body": "Lomakkeen tietosisältö (Form data body)",
|
||||
"OAuth Token URL": "OAuth tokenin URL",
|
||||
"Client ID": "Asiakkaan ID",
|
||||
"Client Secret": "Asiakkaan salaisuus",
|
||||
"OAuth Scope": "OAuth-näkyvyysalue (scope)",
|
||||
"Go back to home page.": "Palaa etusivulle.",
|
||||
"No tags found.": "Tunnisteita ei löytynyt.",
|
||||
"Lost connection to the socket server.": "Menetettiin yhteys Socket-palvelimeen.",
|
||||
"Cannot connect to the socket server.": "Socket-palvelimeen ei voi yhdistää.",
|
||||
"SIGNL4": "SIGNL4",
|
||||
"SIGNL4 Webhook URL": "SIGNL4 Webhookin URL",
|
||||
"signl4Docs": "Löydät lisätietoja SIGNL4-asetuksista ja webhook URLin hankinnasta osoitteesta {0}.",
|
||||
"Conditions": "Ehdot",
|
||||
"conditionAdd": "Lisää ehto",
|
||||
"conditionDelete": "Poista ehto",
|
||||
"conditionAddGroup": "Lisää ryhmä",
|
||||
"conditionDeleteGroup": "Poista ryhmä",
|
||||
"conditionValuePlaceholder": "Arvo",
|
||||
"contains": "sisältää",
|
||||
"not contains": "ei sisällä",
|
||||
"not equals": "ei yhtä suuri kuin",
|
||||
"equals": "yhtä suuri kuin",
|
||||
"starts with": "alkaa",
|
||||
"not starts with": "ei ala",
|
||||
"ends with": "päättyy",
|
||||
"not ends with": "ei pääty",
|
||||
"less than": "vähemmän kuin",
|
||||
"greater than": "enemmän kuin",
|
||||
"less than or equal to": "vähemmän tai yhtä paljon kuin",
|
||||
"greater than or equal to": "enemmän tai yhtä paljon kuin",
|
||||
"record": "tietue"
|
||||
"Command": "Komento"
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user