mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-24 08:32:23 +02:00
Merge remote-tracking branch 'origin/master' into miles/invert-keyword
# Conflicts: # server/database.js
This commit is contained in:
commit
ed6b4e5ae5
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -61,8 +61,8 @@ body:
|
||||
id: operating-system
|
||||
attributes:
|
||||
label: "💻 Operating System and Arch"
|
||||
description: "Which OS is your server/device running on?"
|
||||
placeholder: "Ex. Ubuntu 20.04 x86"
|
||||
description: "Which OS is your server/device running on? (For Replit, please do not report this bug)"
|
||||
placeholder: "Ex. Ubuntu 20.04 x64 "
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
34
.github/workflows/auto-test.yml
vendored
34
.github/workflows/auto-test.yml
vendored
@ -1,4 +1,4 @@
|
||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Auto Test
|
||||
@ -21,8 +21,8 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
node: [ 14, 16, 18, 19 ]
|
||||
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
|
||||
node: [ 14, 18 ]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
@ -33,7 +33,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: 'npm'
|
||||
- run: npm install npm@latest -g
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm test
|
||||
@ -41,6 +41,29 @@ jobs:
|
||||
HEADLESS_TEST: 1
|
||||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
||||
|
||||
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
|
||||
armv7-simple-test:
|
||||
needs: [ check-linters ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ARMv7 ]
|
||||
node: [ 14.21.3, 18.16.1 ]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm install npm@latest -g
|
||||
- run: npm ci --production
|
||||
|
||||
check-linters:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@ -52,7 +75,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run lint
|
||||
|
||||
@ -67,7 +89,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm run cy:test
|
||||
@ -83,7 +104,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm run cy:run:unit
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -23,3 +23,6 @@ cypress/screenshots
|
||||
|
||||
extra/exe-builder/bin
|
||||
extra/exe-builder/obj
|
||||
|
||||
.vs
|
||||
.vscode
|
||||
|
@ -47,17 +47,17 @@ Here are some references:
|
||||
|
||||
❌ Won't Merge
|
||||
- A dedicated pr for translating existing languages (You can now translate on https://weblate.kuma.pet)
|
||||
- Do not pass auto test
|
||||
- Do not pass the auto test
|
||||
- Any breaking changes
|
||||
- Duplicated pull request
|
||||
- Duplicated pull requests
|
||||
- Buggy
|
||||
- UI/UX is not close to Uptime Kuma
|
||||
- Existing logic is completely modified or deleted for no reason
|
||||
- A function that is completely out of scope
|
||||
- Convert existing code into other programming languages
|
||||
- Unnecessary large code changes (Hard to review, causes code conflicts to other pull requests)
|
||||
- Modifications or deletions of existing logic without a valid reason.
|
||||
- Adding functions that is completely out of scope
|
||||
- Converting existing code into other programming languages
|
||||
- Unnecessarily large code changes that are hard to review and cause conflicts with other PRs.
|
||||
|
||||
The above cases cannot cover all situations.
|
||||
The above cases may not cover all possible situations.
|
||||
|
||||
I (@louislam) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spend on it. Therefore, it is essential to have a discussion beforehand.
|
||||
|
||||
|
23
README.md
23
README.md
@ -49,8 +49,12 @@ Uptime Kuma is now running on http://localhost:3001
|
||||
|
||||
### 💪🏻 Non-Docker
|
||||
|
||||
Required Tools:
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 14
|
||||
Requirements:
|
||||
- Platform
|
||||
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
|
||||
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
|
||||
- ❌ Replit / Heroku
|
||||
- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 (20 is not supported)
|
||||
- [npm](https://docs.npmjs.com/cli/) >= 7
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
|
||||
@ -87,6 +91,10 @@ pm2 monit
|
||||
pm2 save && pm2 startup
|
||||
```
|
||||
|
||||
### Windows Portable (x64)
|
||||
|
||||
https://github.com/louislam/uptime-kuma/files/11886108/uptime-kuma-win64-portable-1.0.1.zip
|
||||
|
||||
### Advanced Installation
|
||||
|
||||
If you need more options or need to browse via a reverse proxy, please read:
|
||||
@ -144,17 +152,18 @@ Telegram Notification Sample:
|
||||
|
||||
If you love this project, please consider giving me a ⭐.
|
||||
|
||||
## 🗣️ Discussion
|
||||
## 🗣️ Discussion / Ask for Help
|
||||
|
||||
### Issues Page
|
||||
⚠️ For any general or technical questions, please don't send me an email, as I am unable to provide support in that manner. I will not response if you asked such questions.
|
||||
|
||||
You can discuss or ask for help in [issues](https://github.com/louislam/uptime-kuma/issues).
|
||||
I recommend using Google, GitHub Issues, or Uptime Kuma's Subreddit for finding answers to your question. If you cannot find the information you need, feel free to ask:
|
||||
|
||||
### Subreddit
|
||||
- [GitHub Issues](https://github.com/louislam/uptime-kuma/issues)
|
||||
- [Subreddit r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
|
||||
|
||||
My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam).
|
||||
You can mention me if you ask a question on Reddit.
|
||||
[r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
|
||||
|
||||
|
||||
## Contribute
|
||||
|
||||
|
6
db/patch-add-parent-monitor.sql
Normal file
6
db/patch-add-parent-monitor.sql
Normal file
@ -0,0 +1,6 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD parent INTEGER REFERENCES [monitor] ([id]) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
COMMIT
|
@ -4,5 +4,5 @@ WORKDIR /app
|
||||
|
||||
# Install apprise, iputils for non-root ping, setpriv
|
||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib git && \
|
||||
pip3 --no-cache-dir install apprise==1.3.0 && \
|
||||
pip3 --no-cache-dir install apprise==1.4.0 && \
|
||||
rm -rf /root/.cache
|
||||
|
@ -8,21 +8,21 @@ WORKDIR /app
|
||||
# Install Curl
|
||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine!
|
||||
RUN apt update && \
|
||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||
sqlite3 iputils-ping util-linux dumb-init git && \
|
||||
pip3 --no-cache-dir install apprise==1.3.0 && \
|
||||
RUN apt-get update && \
|
||||
apt-get --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||
sqlite3 iputils-ping util-linux dumb-init git curl ca-certificates && \
|
||||
pip3 --no-cache-dir install apprise==1.4.0 && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt --yes autoremove
|
||||
|
||||
# Install cloudflared
|
||||
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
|
||||
COPY extra/download-cloudflared.js ./extra/download-cloudflared.js
|
||||
RUN node ./extra/download-cloudflared.js $TARGETPLATFORM && \
|
||||
dpkg --add-architecture arm && \
|
||||
apt update && \
|
||||
apt --yes --no-install-recommends install ./cloudflared.deb && \
|
||||
RUN set -eux && \
|
||||
mkdir -p --mode=0755 /usr/share/keyrings && \
|
||||
curl --fail --show-error --silent --location --insecure https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared buster main' | tee /etc/apt/sources.list.d/cloudflared.list && \
|
||||
apt-get update && \
|
||||
apt-get install --yes --no-install-recommends cloudflared && \
|
||||
cloudflared version && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
rm -f cloudflared.deb && \
|
||||
apt --yes autoremove
|
||||
|
||||
|
@ -26,6 +26,8 @@ RUN chmod +x /app/extra/entrypoint.sh
|
||||
FROM louislam/uptime-kuma:base-debian AS release
|
||||
WORKDIR /app
|
||||
|
||||
ENV UPTIME_KUMA_IS_CONTAINER=1
|
||||
|
||||
# Copy app files from build layer
|
||||
COPY --from=build /app /app
|
||||
|
||||
|
@ -1,48 +0,0 @@
|
||||
//
|
||||
|
||||
const http = require("https"); // or 'https' for https:// URLs
|
||||
const fs = require("fs");
|
||||
|
||||
const platform = process.argv[2];
|
||||
|
||||
if (!platform) {
|
||||
console.error("No platform??");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let arch = null;
|
||||
|
||||
if (platform === "linux/amd64") {
|
||||
arch = "amd64";
|
||||
} else if (platform === "linux/arm64") {
|
||||
arch = "arm64";
|
||||
} else if (platform === "linux/arm/v7") {
|
||||
arch = "arm";
|
||||
} else {
|
||||
console.error("Invalid platform?? " + platform);
|
||||
}
|
||||
|
||||
const file = fs.createWriteStream("cloudflared.deb");
|
||||
get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb");
|
||||
|
||||
/**
|
||||
* Download specified file
|
||||
* @param {string} url URL to request
|
||||
*/
|
||||
function get(url) {
|
||||
http.get(url, function (res) {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
console.log("Redirect to " + res.headers.location);
|
||||
get(res.headers.location);
|
||||
} else if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
res.pipe(file);
|
||||
|
||||
res.on("end", function () {
|
||||
console.log("Downloaded");
|
||||
});
|
||||
} else {
|
||||
console.error(res.statusCode);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<Costura />
|
||||
<Costura DisableCompression='true' IncludeDebugSymbols='false' />
|
||||
</Weavers>
|
@ -6,9 +6,9 @@ using System.Runtime.InteropServices;
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Uptime Kuma")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyDescription("A portable executable for running Uptime Kuma")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyCompany("Uptime Kuma")]
|
||||
[assembly: AssemblyProduct("Uptime Kuma")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2023 Louis Lam")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
@ -20,7 +20,7 @@ using System.Runtime.InteropServices;
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("2DB53988-1D93-4AC0-90C4-96ADEAAC5C04")]
|
||||
[assembly: Guid("86B40AFB-61FC-433D-8C31-650B0F32EA8F")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
[assembly: AssemblyVersion("1.0.1.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.1.0")]
|
||||
|
9
extra/test-docker.js
Normal file
9
extra/test-docker.js
Normal file
@ -0,0 +1,9 @@
|
||||
// Check if docker is running
|
||||
const { exec } = require("child_process");
|
||||
|
||||
exec("docker ps", (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error("Docker is not running. Please start docker and try again.");
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
5071
package-lock.json
generated
5071
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.21.2",
|
||||
"version": "1.22.1",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "14.* || >=16.*"
|
||||
"node": "14.* || 16.* || 18.*"
|
||||
},
|
||||
"scripts": {
|
||||
"install-legacy": "npm install",
|
||||
@ -34,12 +34,12 @@
|
||||
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
|
||||
"build-docker-alpine": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:$VERSION-alpine --target release . --push",
|
||||
"build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
|
||||
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||
"build-docker-nightly": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --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.21.2 && npm ci --production && npm run download-dist",
|
||||
"setup": "git checkout 1.22.1 && 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",
|
||||
@ -54,8 +54,8 @@
|
||||
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
|
||||
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
|
||||
"ncu-patch": "npm-check-updates -u -t patch",
|
||||
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
||||
"release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||
"release-beta": "node ./extra/test-docker.js && node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
||||
"git-remove-tag": "git tag -d",
|
||||
"build-dist-and-restart": "npm run build && npm run start-server-dev",
|
||||
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
|
||||
@ -64,7 +64,7 @@
|
||||
"cy:run:unit": "npx cypress run --browser chrome --headless --config-file ./config/cypress.frontend.config.js",
|
||||
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
|
||||
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
|
||||
"depoly-demo-server": "node extra/deploy-demo-server.js",
|
||||
"deploy-demo-server": "node extra/deploy-demo-server.js",
|
||||
"sort-contributors": "node extra/sort-contributors.js"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -76,7 +76,6 @@
|
||||
"axios-ntlm": "1.3.0",
|
||||
"badge-maker": "~3.3.1",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"bree": "~7.1.5",
|
||||
"cacheable-lookup": "~6.0.4",
|
||||
"chardet": "~1.4.0",
|
||||
"check-password-strength": "^2.0.5",
|
||||
@ -85,14 +84,14 @@
|
||||
"command-exists": "~1.2.9",
|
||||
"compare-versions": "~3.6.0",
|
||||
"compression": "~1.7.4",
|
||||
"croner": "^6.0.3",
|
||||
"croner": "~6.0.5",
|
||||
"dayjs": "~1.11.5",
|
||||
"dotenv": "~16.0.3",
|
||||
"express": "~4.17.3",
|
||||
"express-basic-auth": "~1.2.1",
|
||||
"express-static-gzip": "~2.1.7",
|
||||
"form-data": "~4.0.0",
|
||||
"gamedig": "^4.0.5",
|
||||
"gamedig": "~4.0.5",
|
||||
"http-graceful-shutdown": "~3.1.7",
|
||||
"http-proxy-agent": "~5.0.0",
|
||||
"https-proxy-agent": "~5.0.1",
|
||||
@ -105,7 +104,7 @@
|
||||
"mqtt": "~4.3.7",
|
||||
"mssql": "~8.1.4",
|
||||
"mysql2": "~2.3.3",
|
||||
"nanoid": "^3.3.4",
|
||||
"nanoid": "~3.3.4",
|
||||
"node-cloudflared-tunnel": "~1.0.9",
|
||||
"node-radius-client": "~1.0.0",
|
||||
"nodemailer": "~6.6.5",
|
||||
@ -113,14 +112,15 @@
|
||||
"password-hash": "~1.2.2",
|
||||
"pg": "~8.8.0",
|
||||
"pg-connection-string": "~2.5.0",
|
||||
"playwright-core": "~1.35.1",
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.1",
|
||||
"protobufjs": "~7.1.1",
|
||||
"qs": "~6.10.4",
|
||||
"redbean-node": "~0.2.0",
|
||||
"redbean-node": "~0.3.0",
|
||||
"redis": "~4.5.1",
|
||||
"socket.io": "~4.5.3",
|
||||
"socket.io-client": "~4.5.3",
|
||||
"socket.io": "~4.6.1",
|
||||
"socket.io-client": "~4.6.1",
|
||||
"socks-proxy-agent": "6.1.1",
|
||||
"tar": "~6.1.11",
|
||||
"tcp-ping": "~0.1.1",
|
||||
@ -143,8 +143,8 @@
|
||||
"aedes": "^0.46.3",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"bootstrap": "5.1.3",
|
||||
"chart.js": "~3.6.2",
|
||||
"chartjs-adapter-dayjs": "~1.0.0",
|
||||
"chart.js": "~4.2.1",
|
||||
"chartjs-adapter-dayjs-4": "~1.0.4",
|
||||
"concurrently": "^7.1.0",
|
||||
"core-js": "~3.26.1",
|
||||
"cronstrue": "~2.24.0",
|
||||
@ -166,16 +166,16 @@
|
||||
"qrcode": "~1.5.0",
|
||||
"rollup-plugin-visualizer": "^5.6.0",
|
||||
"sass": "~1.42.1",
|
||||
"stylelint": "~14.7.1",
|
||||
"stylelint": "~15.9.0",
|
||||
"stylelint-config-standard": "~25.0.0",
|
||||
"terser": "~5.15.0",
|
||||
"timezones-list": "~3.0.1",
|
||||
"typescript": "~4.4.4",
|
||||
"v-pagination-3": "~0.1.7",
|
||||
"vite": "~3.1.0",
|
||||
"vite": "~3.2.7",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue": "~3.2.47",
|
||||
"vue-chart-3": "3.0.9",
|
||||
"vue-chartjs": "~5.2.0",
|
||||
"vue-confirm-dialog": "~1.0.2",
|
||||
"vue-contenteditable": "~3.0.4",
|
||||
"vue-i18n": "~9.2.2",
|
||||
@ -186,6 +186,7 @@
|
||||
"vue-router": "~4.0.14",
|
||||
"vue-toastification": "~2.0.0-rc.5",
|
||||
"vuedraggable": "~4.1.0",
|
||||
"wait-on": "^6.0.1"
|
||||
"wait-on": "^6.0.1",
|
||||
"whatwg-url": "~12.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ const basicAuth = require("express-basic-auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
const { R } = require("redbean-node");
|
||||
const { setting } = require("./util-server");
|
||||
const { log } = require("../src/util");
|
||||
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
|
||||
const { Settings } = require("./settings");
|
||||
const dayjs = require("dayjs");
|
||||
@ -81,12 +82,16 @@ function apiAuthorizer(username, password, callback) {
|
||||
apiRateLimiter.pass(null, 0).then((pass) => {
|
||||
if (pass) {
|
||||
verifyAPIKey(password).then((valid) => {
|
||||
if (!valid) {
|
||||
log.warn("api-auth", "Failed API auth attempt: invalid API Key");
|
||||
}
|
||||
callback(null, valid);
|
||||
// Only allow a set number of api requests per minute
|
||||
// (currently set to 60)
|
||||
apiRateLimiter.removeTokens(1);
|
||||
});
|
||||
} else {
|
||||
log.warn("api-auth", "Failed API auth attempt: rate limit exceeded");
|
||||
callback(null, false);
|
||||
}
|
||||
});
|
||||
@ -106,10 +111,12 @@ function userAuthorizer(username, password, callback) {
|
||||
callback(null, user != null);
|
||||
|
||||
if (user == null) {
|
||||
log.warn("basic-auth", "Failed basic auth attempt: invalid username/password");
|
||||
loginRateLimiter.removeTokens(1);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.warn("basic-auth", "Failed basic auth attempt: rate limit exceeded");
|
||||
callback(null, false);
|
||||
}
|
||||
});
|
||||
|
@ -1,27 +1,33 @@
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
const axios = require("axios");
|
||||
const compareVersions = require("compare-versions");
|
||||
const { log } = require("../src/util");
|
||||
|
||||
exports.version = require("../package.json").version;
|
||||
exports.latestVersion = null;
|
||||
|
||||
// How much time in ms to wait between update checks
|
||||
const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48;
|
||||
const UPDATE_CHECKER_LATEST_VERSION_URL = "https://uptime.kuma.pet/version";
|
||||
|
||||
let interval;
|
||||
|
||||
/** Start 48 hour check interval */
|
||||
exports.startInterval = () => {
|
||||
let check = async () => {
|
||||
if (await setting("checkUpdate") === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("update-checker", "Retrieving latest versions");
|
||||
|
||||
try {
|
||||
const res = await axios.get("https://uptime.kuma.pet/version");
|
||||
const res = await axios.get(UPDATE_CHECKER_LATEST_VERSION_URL);
|
||||
|
||||
// For debug
|
||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||
res.data.slow = "1000.0.0";
|
||||
}
|
||||
|
||||
if (await setting("checkUpdate") === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
let checkBeta = await setting("checkBeta");
|
||||
|
||||
if (checkBeta && res.data.beta) {
|
||||
@ -35,12 +41,14 @@ exports.startInterval = () => {
|
||||
exports.latestVersion = res.data.slow;
|
||||
}
|
||||
|
||||
} catch (_) { }
|
||||
} catch (_) {
|
||||
log.info("update-checker", "Failed to check for new versions");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
check();
|
||||
interval = setInterval(check, 3600 * 1000 * 48);
|
||||
interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -2,9 +2,7 @@ const fs = require("fs");
|
||||
const { R } = require("redbean-node");
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
const { log, sleep } = require("../src/util");
|
||||
const dayjs = require("dayjs");
|
||||
const knex = require("knex");
|
||||
const { PluginsManager } = require("./plugins-manager");
|
||||
|
||||
/**
|
||||
* Database & App Data Folder
|
||||
@ -23,6 +21,8 @@ class Database {
|
||||
*/
|
||||
static uploadDir;
|
||||
|
||||
static screenshotDir;
|
||||
|
||||
static path;
|
||||
|
||||
/**
|
||||
@ -70,6 +70,7 @@ class Database {
|
||||
"patch-api-key-table.sql": true,
|
||||
"patch-monitor-tls.sql": true,
|
||||
"patch-maintenance-cron.sql": true,
|
||||
"patch-add-parent-monitor.sql": true,
|
||||
"patch-add-invert-keyword.sql": true,
|
||||
};
|
||||
|
||||
@ -89,12 +90,6 @@ class Database {
|
||||
// Data Directory (must be end with "/")
|
||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||
|
||||
// Plugin feature is working only if the dataDir = "./data";
|
||||
if (Database.dataDir !== "./data/") {
|
||||
log.warn("PLUGIN", "Warning: In order to enable plugin feature, you need to use the default data directory: ./data/");
|
||||
PluginsManager.disable = true;
|
||||
}
|
||||
|
||||
Database.path = Database.dataDir + "kuma.db";
|
||||
if (! fs.existsSync(Database.dataDir)) {
|
||||
fs.mkdirSync(Database.dataDir, { recursive: true });
|
||||
@ -106,6 +101,12 @@ class Database {
|
||||
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create screenshot dir
|
||||
Database.screenshotDir = Database.dataDir + "screenshots/";
|
||||
if (! fs.existsSync(Database.screenshotDir)) {
|
||||
fs.mkdirSync(Database.screenshotDir, { recursive: true });
|
||||
}
|
||||
|
||||
log.info("db", `Data Dir: ${Database.dataDir}`);
|
||||
}
|
||||
|
||||
@ -162,12 +163,12 @@ class Database {
|
||||
await R.exec("PRAGMA journal_mode = WAL");
|
||||
}
|
||||
await R.exec("PRAGMA cache_size = -12000");
|
||||
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||
await R.exec("PRAGMA auto_vacuum = INCREMENTAL");
|
||||
|
||||
// This ensures that an operating system crash or power failure will not corrupt the database.
|
||||
// FULL synchronous is very safe, but it is also slower.
|
||||
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
||||
await R.exec("PRAGMA synchronous = FULL");
|
||||
await R.exec("PRAGMA synchronous = NORMAL");
|
||||
|
||||
if (!noLog) {
|
||||
log.info("db", "SQLite config:");
|
||||
@ -418,6 +419,9 @@ class Database {
|
||||
|
||||
log.info("db", "Closing the database");
|
||||
|
||||
// Flush WAL to main database
|
||||
await R.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
||||
|
||||
while (true) {
|
||||
Database.noReject = true;
|
||||
await R.close();
|
||||
|
@ -1,24 +0,0 @@
|
||||
const childProcess = require("child_process");
|
||||
|
||||
class Git {
|
||||
|
||||
static clone(repoURL, cwd, targetDir = ".") {
|
||||
let result = childProcess.spawnSync("git", [
|
||||
"clone",
|
||||
repoURL,
|
||||
targetDir,
|
||||
], {
|
||||
cwd: cwd,
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr.toString("utf-8"));
|
||||
} else {
|
||||
return result.stdout.toString("utf-8") + result.stderr.toString("utf-8");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Git,
|
||||
};
|
@ -1,41 +1,44 @@
|
||||
const path = require("path");
|
||||
const Bree = require("bree");
|
||||
const { SHARE_ENV } = require("worker_threads");
|
||||
const { log } = require("../src/util");
|
||||
let bree;
|
||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||
const { clearOldData } = require("./jobs/clear-old-data");
|
||||
const Cron = require("croner");
|
||||
|
||||
const jobs = [
|
||||
{
|
||||
name: "clear-old-data",
|
||||
interval: "at 03:14",
|
||||
interval: "14 03 * * *",
|
||||
jobFunc: clearOldData,
|
||||
croner: null,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Initialize background jobs
|
||||
* @param {Object} args Arguments to pass to workers
|
||||
* @returns {Bree}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const initBackgroundJobs = function (args) {
|
||||
bree = new Bree({
|
||||
root: path.resolve("server", "jobs"),
|
||||
jobs,
|
||||
worker: {
|
||||
env: SHARE_ENV,
|
||||
workerData: args,
|
||||
},
|
||||
workerMessageHandler: (message) => {
|
||||
log.info("jobs", message);
|
||||
}
|
||||
});
|
||||
const initBackgroundJobs = async function () {
|
||||
const timezone = await UptimeKumaServer.getInstance().getTimezone();
|
||||
|
||||
for (const job of jobs) {
|
||||
const cornerJob = new Cron(
|
||||
job.interval,
|
||||
{
|
||||
name: job.name,
|
||||
timezone,
|
||||
},
|
||||
job.jobFunc,
|
||||
);
|
||||
job.croner = cornerJob;
|
||||
}
|
||||
|
||||
bree.start();
|
||||
return bree;
|
||||
};
|
||||
|
||||
/** Stop all background jobs if running */
|
||||
const stopBackgroundJobs = function () {
|
||||
if (bree) {
|
||||
bree.stop();
|
||||
for (const job of jobs) {
|
||||
if (job.croner) {
|
||||
job.croner.stop();
|
||||
job.croner = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,12 +1,15 @@
|
||||
const { log, exit, connectDb } = require("./util-worker");
|
||||
const { R } = require("redbean-node");
|
||||
const { log } = require("../../src/util");
|
||||
const { setSetting, setting } = require("../util-server");
|
||||
|
||||
const DEFAULT_KEEP_PERIOD = 180;
|
||||
|
||||
(async () => {
|
||||
await connectDb();
|
||||
/**
|
||||
* Clears old data from the heartbeat table of the database.
|
||||
* @return {Promise<void>} A promise that resolves when the data has been cleared.
|
||||
*/
|
||||
|
||||
const clearOldData = async () => {
|
||||
let period = await setting("keepDataPeriodDays");
|
||||
|
||||
// Set Default Period
|
||||
@ -20,16 +23,16 @@ const DEFAULT_KEEP_PERIOD = 180;
|
||||
try {
|
||||
parsedPeriod = parseInt(period);
|
||||
} catch (_) {
|
||||
log("Failed to parse setting, resetting to default..");
|
||||
log.warn("clearOldData", "Failed to parse setting, resetting to default..");
|
||||
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||
parsedPeriod = DEFAULT_KEEP_PERIOD;
|
||||
}
|
||||
|
||||
if (parsedPeriod < 1) {
|
||||
log(`Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
|
||||
log.info("clearOldData", `Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
|
||||
} else {
|
||||
|
||||
log(`Clearing Data older than ${parsedPeriod} days...`);
|
||||
log.debug("clearOldData", `Clearing Data older than ${parsedPeriod} days...`);
|
||||
|
||||
try {
|
||||
await R.exec(
|
||||
@ -37,9 +40,11 @@ const DEFAULT_KEEP_PERIOD = 180;
|
||||
[ parsedPeriod ]
|
||||
);
|
||||
} catch (e) {
|
||||
log(`Failed to clear old data: ${e.message}`);
|
||||
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exit();
|
||||
})();
|
||||
module.exports = {
|
||||
clearOldData,
|
||||
};
|
||||
|
@ -1,50 +0,0 @@
|
||||
const { parentPort, workerData } = require("worker_threads");
|
||||
const Database = require("../database");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Send message to parent process for logging
|
||||
* since worker_thread does not have access to stdout, this is used
|
||||
* instead of console.log()
|
||||
* @param {any} any The message to log
|
||||
*/
|
||||
const log = function (any) {
|
||||
if (parentPort) {
|
||||
parentPort.postMessage(any);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Exit the worker process
|
||||
* @param {number} error The status code to exit
|
||||
*/
|
||||
const exit = function (error) {
|
||||
if (error && error !== 0) {
|
||||
process.exit(error);
|
||||
} else {
|
||||
if (parentPort) {
|
||||
parentPort.postMessage("done");
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** Connects to the database */
|
||||
const connectDb = async function () {
|
||||
const dbPath = path.join(
|
||||
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
|
||||
);
|
||||
|
||||
Database.init({
|
||||
"data-dir": dbPath,
|
||||
});
|
||||
|
||||
await Database.connect();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
log,
|
||||
exit,
|
||||
connectDb,
|
||||
};
|
@ -47,7 +47,8 @@ class Maintenance extends BeanModel {
|
||||
cron: this.cron,
|
||||
duration: this.duration,
|
||||
durationMinutes: parseInt(this.duration / 60),
|
||||
timezone: await this.getTimezone(),
|
||||
timezone: await this.getTimezone(), // Only valid timezone
|
||||
timezoneOption: this.timezone, // Mainly for dropdown menu, because there is a option "SAME_AS_SERVER"
|
||||
timezoneOffset: await this.getTimezoneOffset(),
|
||||
status: await this.getStatus(),
|
||||
};
|
||||
@ -153,7 +154,7 @@ class Maintenance extends BeanModel {
|
||||
bean.description = obj.description;
|
||||
bean.strategy = obj.strategy;
|
||||
bean.interval_day = obj.intervalDay;
|
||||
bean.timezone = obj.timezone;
|
||||
bean.timezone = obj.timezoneOption;
|
||||
bean.active = obj.active;
|
||||
|
||||
if (obj.dateRange[0]) {
|
||||
@ -290,7 +291,7 @@ class Maintenance extends BeanModel {
|
||||
}
|
||||
|
||||
getRunningTimeslot() {
|
||||
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").format("YYYY-MM-DD HH:mm:ss")));
|
||||
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").toDate()));
|
||||
let end = start.add(this.duration, "second");
|
||||
let current = dayjs();
|
||||
|
||||
@ -316,7 +317,7 @@ class Maintenance extends BeanModel {
|
||||
}
|
||||
|
||||
async getTimezone() {
|
||||
if (!this.timezone) {
|
||||
if (!this.timezone || this.timezone === "SAME_AS_SERVER") {
|
||||
return await UptimeKumaServer.getInstance().getTimezone();
|
||||
}
|
||||
return this.timezone;
|
||||
|
@ -2,7 +2,9 @@ const https = require("https");
|
||||
const dayjs = require("dayjs");
|
||||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } = require("../../src/util");
|
||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
||||
SQL_DATETIME_FORMAT
|
||||
} = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
||||
redisPingAsync, mongodbPing,
|
||||
} = require("../util-server");
|
||||
@ -18,6 +20,7 @@ const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||
const { DockerHost } = require("../docker");
|
||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
||||
const Gamedig = require("gamedig");
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
/**
|
||||
* status:
|
||||
@ -68,17 +71,27 @@ class Monitor extends BeanModel {
|
||||
|
||||
const tags = await this.getTags();
|
||||
|
||||
let screenshot = null;
|
||||
|
||||
if (this.type === "real-browser") {
|
||||
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
|
||||
}
|
||||
|
||||
let data = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
pathName: await this.getPathName(),
|
||||
parent: this.parent,
|
||||
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: this.active,
|
||||
active: await this.isActive(),
|
||||
forceInactive: !await Monitor.isParentActive(this.id),
|
||||
type: this.type,
|
||||
interval: this.interval,
|
||||
retryInterval: this.retryInterval,
|
||||
@ -112,7 +125,8 @@ class Monitor extends BeanModel {
|
||||
radiusCalledStationId: this.radiusCalledStationId,
|
||||
radiusCallingStationId: this.radiusCallingStationId,
|
||||
game: this.game,
|
||||
httpBodyEncoding: this.httpBodyEncoding
|
||||
httpBodyEncoding: this.httpBodyEncoding,
|
||||
screenshot,
|
||||
};
|
||||
|
||||
if (includeSensitiveData) {
|
||||
@ -143,6 +157,16 @@ class Monitor extends BeanModel {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the monitor is active based on itself and its parents
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
async isActive() {
|
||||
const parentActive = await Monitor.isParentActive(this.id);
|
||||
|
||||
return this.active && parentActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags applied to this monitor
|
||||
* @returns {Promise<LooseObject<any>[]>}
|
||||
@ -266,6 +290,36 @@ class Monitor extends BeanModel {
|
||||
if (await Monitor.isUnderMaintenance(this.id)) {
|
||||
bean.msg = "Monitor under maintenance";
|
||||
bean.status = MAINTENANCE;
|
||||
} else if (this.type === "group") {
|
||||
const children = await Monitor.getChildren(this.id);
|
||||
|
||||
if (children.length > 0) {
|
||||
bean.status = UP;
|
||||
bean.msg = "All children up and running";
|
||||
for (const child of children) {
|
||||
if (!child.active) {
|
||||
// Ignore inactive childs
|
||||
continue;
|
||||
}
|
||||
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
|
||||
|
||||
// Only change state if the monitor is in worse conditions then the ones before
|
||||
if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
|
||||
bean.status = lastBeat.status;
|
||||
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
|
||||
bean.status = lastBeat.status;
|
||||
}
|
||||
}
|
||||
|
||||
if (bean.status !== UP) {
|
||||
bean.msg = "Child inaccessible";
|
||||
}
|
||||
} else {
|
||||
// Set status pending if group is empty
|
||||
bean.status = PENDING;
|
||||
bean.msg = "Group empty";
|
||||
}
|
||||
|
||||
} else if (this.type === "http" || this.type === "keyword") {
|
||||
// Do not do any queries/high loading things before the "bean.ping"
|
||||
let startTime = dayjs().valueOf();
|
||||
@ -372,8 +426,8 @@ class Monitor extends BeanModel {
|
||||
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
||||
|
||||
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
|
||||
log.debug("monitor", `[${this.name}] call sendCertNotification`);
|
||||
await this.sendCertNotification(tlsInfoObject);
|
||||
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
|
||||
await this.checkCertExpiryNotifications(tlsInfoObject);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
@ -408,7 +462,7 @@ class Monitor extends BeanModel {
|
||||
bean.msg += ", keyword " + (keywordFound ? "is" : "not") + " found";
|
||||
bean.status = UP;
|
||||
} else {
|
||||
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
|
||||
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
|
||||
if (data.length > 50) {
|
||||
data = data.substring(0, 47) + "...";
|
||||
}
|
||||
@ -583,9 +637,15 @@ class Monitor extends BeanModel {
|
||||
|
||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||
let res = await axios.request(options);
|
||||
|
||||
if (res.data.State.Running) {
|
||||
bean.status = UP;
|
||||
bean.msg = res.data.State.Status;
|
||||
if (res.data.State.Health && res.data.State.Health.Status !== "healthy") {
|
||||
bean.status = PENDING;
|
||||
bean.msg = res.data.State.Health.Status;
|
||||
} else {
|
||||
bean.status = UP;
|
||||
bean.msg = res.data.State.Health ? res.data.State.Health.Status : res.data.State.Status;
|
||||
}
|
||||
} else {
|
||||
throw Error("Container State is " + res.data.State.Status);
|
||||
}
|
||||
@ -705,7 +765,7 @@ class Monitor extends BeanModel {
|
||||
} else if (this.type in UptimeKumaServer.monitorTypeList) {
|
||||
let startTime = dayjs().valueOf();
|
||||
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
|
||||
await monitorType.check(this, bean);
|
||||
await monitorType.check(this, bean, UptimeKumaServer.getInstance());
|
||||
if (!bean.ping) {
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
}
|
||||
@ -1187,12 +1247,18 @@ class Monitor extends BeanModel {
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||
const heartbeatJSON = bean.toJSON();
|
||||
|
||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||
if (!heartbeatJSON["msg"]) {
|
||||
heartbeatJSON["msg"] = "N/A";
|
||||
}
|
||||
|
||||
// Also provide the time in server timezone
|
||||
heartbeatJSON["timezone"] = await UptimeKumaServer.getInstance().getTimezone();
|
||||
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
|
||||
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
|
||||
|
||||
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
|
||||
} catch (e) {
|
||||
log.error("monitor", "Cannot send notification to " + notification.name);
|
||||
@ -1215,13 +1281,19 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification about a certificate
|
||||
* checks certificate chain for expiring certificates
|
||||
* @param {Object} tlsInfoObject Information about certificate
|
||||
*/
|
||||
async sendCertNotification(tlsInfoObject) {
|
||||
async checkCertExpiryNotifications(tlsInfoObject) {
|
||||
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
||||
const notificationList = await Monitor.getNotificationList(this);
|
||||
|
||||
if (! notificationList.length > 0) {
|
||||
// fail fast. If no notification is set, all the following checks can be skipped.
|
||||
log.debug("monitor", "No notification, no need to send cert notification");
|
||||
return;
|
||||
}
|
||||
|
||||
let notifyDays = await setting("tlsExpiryNotifyDays");
|
||||
if (notifyDays == null || !Array.isArray(notifyDays)) {
|
||||
// Reset Default
|
||||
@ -1229,10 +1301,19 @@ class Monitor extends BeanModel {
|
||||
notifyDays = [ 7, 14, 21 ];
|
||||
}
|
||||
|
||||
if (notifyDays != null && Array.isArray(notifyDays)) {
|
||||
for (const day of notifyDays) {
|
||||
log.debug("monitor", "call sendCertNotificationByTargetDays", day);
|
||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, day, notificationList);
|
||||
if (Array.isArray(notifyDays)) {
|
||||
for (const targetDays of notifyDays) {
|
||||
let certInfo = tlsInfoObject.certInfo;
|
||||
while (certInfo) {
|
||||
let subjectCN = certInfo.subject["CN"];
|
||||
if (certInfo.daysRemaining > targetDays) {
|
||||
log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
|
||||
} else {
|
||||
log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`);
|
||||
await this.sendCertNotificationByTargetDays(subjectCN, certInfo.certType, certInfo.daysRemaining, targetDays, notificationList);
|
||||
}
|
||||
certInfo = certInfo.issuerCertificate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1241,55 +1322,47 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Send a certificate notification when certificate expires in less
|
||||
* than target days
|
||||
* @param {number} daysRemaining Number of days remaining on certifcate
|
||||
* @param {string} certCN Common Name attribute from the certificate subject
|
||||
* @param {string} certType certificate type
|
||||
* @param {number} daysRemaining Number of days remaining on certificate
|
||||
* @param {number} targetDays Number of days to alert after
|
||||
* @param {LooseObject<any>[]} notificationList List of notification providers
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
|
||||
async sendCertNotificationByTargetDays(certCN, certType, daysRemaining, targetDays, notificationList) {
|
||||
|
||||
if (daysRemaining > targetDays) {
|
||||
log.debug("monitor", `No need to send cert notification. ${daysRemaining} > ${targetDays}`);
|
||||
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [
|
||||
"certificate",
|
||||
this.id,
|
||||
targetDays,
|
||||
]);
|
||||
|
||||
// Sent already, no need to send again
|
||||
if (row) {
|
||||
log.debug("monitor", "Sent already, no need to send again");
|
||||
return;
|
||||
}
|
||||
|
||||
if (notificationList.length > 0) {
|
||||
let sent = false;
|
||||
log.debug("monitor", "Send certificate notification");
|
||||
|
||||
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
log.debug("monitor", "Sending to " + notification.name);
|
||||
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] ${certType} certificate ${certCN} will be expired in ${daysRemaining} days`);
|
||||
sent = true;
|
||||
} catch (e) {
|
||||
log.error("monitor", "Cannot send cert notification to " + notification.name);
|
||||
log.error("monitor", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (sent) {
|
||||
await R.exec("INSERT INTO notification_sent_history (type, monitor_id, days) VALUES(?, ?, ?)", [
|
||||
"certificate",
|
||||
this.id,
|
||||
targetDays,
|
||||
]);
|
||||
|
||||
// Sent already, no need to send again
|
||||
if (row) {
|
||||
log.debug("monitor", "Sent already, no need to send again");
|
||||
return;
|
||||
}
|
||||
|
||||
let sent = false;
|
||||
log.debug("monitor", "Send certificate notification");
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
log.debug("monitor", "Sending to " + notification.name);
|
||||
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] Certificate will expire in ${daysRemaining} days`);
|
||||
sent = true;
|
||||
} catch (e) {
|
||||
log.error("monitor", "Cannot send cert notification to " + notification.name);
|
||||
log.error("monitor", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (sent) {
|
||||
await R.exec("INSERT INTO notification_sent_history (type, monitor_id, days) VALUES(?, ?, ?)", [
|
||||
"certificate",
|
||||
this.id,
|
||||
targetDays,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
log.debug("monitor", "No notification, no need to send cert notification");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1325,6 +1398,11 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await Monitor.getParent(monitorID);
|
||||
if (parent != null) {
|
||||
return await Monitor.isUnderMaintenance(parent.id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1337,6 +1415,105 @@ class Monitor extends BeanModel {
|
||||
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets Parent of the monitor
|
||||
* @param {number} monitorID ID of monitor to get
|
||||
* @returns {Promise<LooseObject<any>>}
|
||||
*/
|
||||
static async getParent(monitorID) {
|
||||
return await R.getRow(`
|
||||
SELECT parent.* FROM monitor parent
|
||||
LEFT JOIN monitor child
|
||||
ON child.parent = parent.id
|
||||
WHERE child.id = ?
|
||||
`, [
|
||||
monitorID,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all Children of the monitor
|
||||
* @param {number} monitorID ID of monitor to get
|
||||
* @returns {Promise<LooseObject<any>>}
|
||||
*/
|
||||
static async getChildren(monitorID) {
|
||||
return await R.getAll(`
|
||||
SELECT * FROM monitor
|
||||
WHERE parent = ?
|
||||
`, [
|
||||
monitorID,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets Full Path-Name (Groups and Name)
|
||||
* @returns {Promise<String>}
|
||||
*/
|
||||
async getPathName() {
|
||||
let path = this.name;
|
||||
|
||||
if (this.parent === null) {
|
||||
return path;
|
||||
}
|
||||
|
||||
let parent = await Monitor.getParent(this.id);
|
||||
while (parent !== null) {
|
||||
path = `${parent.name} / ${path}`;
|
||||
parent = await Monitor.getParent(parent.id);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets recursive all child ids
|
||||
* @param {number} monitorID ID of the monitor to get
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
static async getAllChildrenIDs(monitorID) {
|
||||
const childs = await Monitor.getChildren(monitorID);
|
||||
|
||||
if (childs === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let childrenIDs = [];
|
||||
|
||||
for (const child of childs) {
|
||||
childrenIDs.push(child.id);
|
||||
childrenIDs = childrenIDs.concat(await Monitor.getAllChildrenIDs(child.id));
|
||||
}
|
||||
|
||||
return childrenIDs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlinks all children of the the group monitor
|
||||
* @param {number} groupID ID of group to remove children of
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async unlinkAllChildren(groupID) {
|
||||
return await R.exec("UPDATE `monitor` SET parent = ? WHERE parent = ? ", [
|
||||
null, groupID
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks recursive if parent (ancestors) are active
|
||||
* @param {number} monitorID ID of the monitor to get
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
static async isParentActive(monitorID) {
|
||||
const parent = await Monitor.getParent(monitorID);
|
||||
|
||||
if (parent === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const parentActive = await Monitor.isParentActive(parent.id);
|
||||
return parent.active && parentActive;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Monitor;
|
||||
|
@ -6,9 +6,10 @@ class MonitorType {
|
||||
*
|
||||
* @param {Monitor} monitor
|
||||
* @param {Heartbeat} heartbeat
|
||||
* @param {UptimeKumaServer} server
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async check(monitor, heartbeat) {
|
||||
async check(monitor, heartbeat, server) {
|
||||
throw new Error("You need to override check()");
|
||||
}
|
||||
|
||||
|
212
server/monitor-types/real-browser-monitor-type.js
Normal file
212
server/monitor-types/real-browser-monitor-type.js
Normal file
@ -0,0 +1,212 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { chromium } = require("playwright-core");
|
||||
const { UP, log } = require("../../src/util");
|
||||
const { Settings } = require("../settings");
|
||||
const commandExistsSync = require("command-exists").sync;
|
||||
const childProcess = require("child_process");
|
||||
const path = require("path");
|
||||
const Database = require("../database");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const config = require("../config");
|
||||
|
||||
let browser = null;
|
||||
|
||||
let allowedList = [];
|
||||
let lastAutoDetectChromeExecutable = null;
|
||||
|
||||
if (process.platform === "win32") {
|
||||
allowedList.push(process.env.LOCALAPPDATA + "\\Google\\Chrome\\Application\\chrome.exe");
|
||||
allowedList.push(process.env.PROGRAMFILES + "\\Google\\Chrome\\Application\\chrome.exe");
|
||||
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Google\\Chrome\\Application\\chrome.exe");
|
||||
|
||||
// Allow Chromium too
|
||||
allowedList.push(process.env.LOCALAPPDATA + "\\Chromium\\Application\\chrome.exe");
|
||||
allowedList.push(process.env.PROGRAMFILES + "\\Chromium\\Application\\chrome.exe");
|
||||
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Chromium\\Application\\chrome.exe");
|
||||
|
||||
// For Loop A to Z
|
||||
for (let i = 65; i <= 90; i++) {
|
||||
let drive = String.fromCharCode(i);
|
||||
allowedList.push(drive + ":\\Program Files\\Google\\Chrome\\Application\\chrome.exe");
|
||||
allowedList.push(drive + ":\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe");
|
||||
}
|
||||
|
||||
} else if (process.platform === "linux") {
|
||||
allowedList = [
|
||||
"chromium",
|
||||
"chromium-browser",
|
||||
"google-chrome",
|
||||
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/chromium-browser",
|
||||
"/usr/bin/google-chrome",
|
||||
];
|
||||
} else if (process.platform === "darwin") {
|
||||
// TODO: Generated by GitHub Copilot, but not sure if it's correct
|
||||
allowedList = [
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
];
|
||||
}
|
||||
|
||||
log.debug("chrome", allowedList);
|
||||
|
||||
async function isAllowedChromeExecutable(executablePath) {
|
||||
console.log(config.args);
|
||||
if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the executablePath is in the list of allowed executables
|
||||
return allowedList.includes(executablePath);
|
||||
}
|
||||
|
||||
async function getBrowser() {
|
||||
if (!browser) {
|
||||
let executablePath = await Settings.get("chromeExecutable");
|
||||
|
||||
executablePath = await prepareChromeExecutable(executablePath);
|
||||
|
||||
browser = await chromium.launch({
|
||||
//headless: false,
|
||||
executablePath,
|
||||
});
|
||||
}
|
||||
return browser;
|
||||
}
|
||||
|
||||
async function prepareChromeExecutable(executablePath) {
|
||||
// Special code for using the playwright_chromium
|
||||
if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") {
|
||||
// Set to undefined = use playwright_chromium
|
||||
executablePath = undefined;
|
||||
} else if (!executablePath) {
|
||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||
executablePath = "/usr/bin/chromium";
|
||||
|
||||
// Install chromium in container via apt install
|
||||
if ( !commandExistsSync(executablePath)) {
|
||||
await new Promise((resolve, reject) => {
|
||||
log.info("Chromium", "Installing Chromium...");
|
||||
let child = childProcess.exec("apt update && apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk");
|
||||
|
||||
// On exit
|
||||
child.on("exit", (code) => {
|
||||
log.info("Chromium", "apt install chromium exited with code " + code);
|
||||
|
||||
if (code === 0) {
|
||||
log.info("Chromium", "Installed Chromium");
|
||||
let version = childProcess.execSync(executablePath + " --version").toString("utf8");
|
||||
log.info("Chromium", "Chromium version: " + version);
|
||||
resolve();
|
||||
} else if (code === 100) {
|
||||
reject(new Error("Installing Chromium, please wait..."));
|
||||
} else {
|
||||
reject(new Error("apt install chromium failed with code " + code));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
executablePath = findChrome(allowedList);
|
||||
}
|
||||
} else {
|
||||
// User specified a path
|
||||
// Check if the executablePath is in the list of allowed
|
||||
if (!await isAllowedChromeExecutable(executablePath)) {
|
||||
throw new Error("This Chromium executable path is not allowed by default. If you are sure this is safe, please add an environment variable UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC=1 to allow it.");
|
||||
}
|
||||
}
|
||||
return executablePath;
|
||||
}
|
||||
|
||||
function findChrome(executables) {
|
||||
// Use the last working executable, so we don't have to search for it again
|
||||
if (lastAutoDetectChromeExecutable) {
|
||||
if (commandExistsSync(lastAutoDetectChromeExecutable)) {
|
||||
return lastAutoDetectChromeExecutable;
|
||||
}
|
||||
}
|
||||
|
||||
for (let executable of executables) {
|
||||
if (commandExistsSync(executable)) {
|
||||
lastAutoDetectChromeExecutable = executable;
|
||||
return executable;
|
||||
}
|
||||
}
|
||||
throw new Error("Chromium not found, please specify Chromium executable path in the settings page.");
|
||||
}
|
||||
|
||||
async function resetChrome() {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
browser = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the chrome executable is valid and return the version
|
||||
* @param executablePath
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function testChrome(executablePath) {
|
||||
try {
|
||||
executablePath = await prepareChromeExecutable(executablePath);
|
||||
|
||||
log.info("Chromium", "Testing Chromium executable: " + executablePath);
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath,
|
||||
});
|
||||
const version = browser.version();
|
||||
await browser.close();
|
||||
return version;
|
||||
} catch (e) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: connect remote browser? https://playwright.dev/docs/api/class-browsertype#browser-type-connect
|
||||
*
|
||||
*/
|
||||
class RealBrowserMonitorType extends MonitorType {
|
||||
|
||||
name = "real-browser";
|
||||
|
||||
async check(monitor, heartbeat, server) {
|
||||
const browser = await getBrowser();
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
const res = await page.goto(monitor.url, {
|
||||
waitUntil: "networkidle",
|
||||
timeout: monitor.interval * 1000 * 0.8,
|
||||
});
|
||||
|
||||
let filename = jwt.sign(monitor.id, server.jwtSecret) + ".png";
|
||||
|
||||
await page.screenshot({
|
||||
path: path.join(Database.screenshotDir, filename),
|
||||
});
|
||||
|
||||
await context.close();
|
||||
|
||||
if (res.status() >= 200 && res.status() < 400) {
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = res.status();
|
||||
|
||||
const timing = res.request().timing();
|
||||
heartbeat.ping = timing.responseEnd;
|
||||
} else {
|
||||
throw new Error(res.status() + "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RealBrowserMonitorType,
|
||||
testChrome,
|
||||
resetChrome,
|
||||
};
|
@ -15,7 +15,7 @@ class DingDing extends NotificationProvider {
|
||||
msgtype: "markdown",
|
||||
markdown: {
|
||||
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
}
|
||||
};
|
||||
if (this.sendToDingDing(notification, params)) {
|
||||
|
@ -59,8 +59,8 @@ class Discord extends NotificationProvider {
|
||||
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
value: heartbeatJSON["localDateTime"],
|
||||
},
|
||||
{
|
||||
name: "Error",
|
||||
@ -94,8 +94,8 @@ class Discord extends NotificationProvider {
|
||||
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
value: heartbeatJSON["localDateTime"],
|
||||
},
|
||||
{
|
||||
name: "Ping",
|
||||
|
@ -35,8 +35,7 @@ class Feishu extends NotificationProvider {
|
||||
text:
|
||||
"[Down] " +
|
||||
heartbeatJSON["msg"] +
|
||||
"\nTime (UTC): " +
|
||||
heartbeatJSON["time"],
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
},
|
||||
],
|
||||
],
|
||||
@ -62,8 +61,7 @@ class Feishu extends NotificationProvider {
|
||||
text:
|
||||
"[Up] " +
|
||||
heartbeatJSON["msg"] +
|
||||
"\nTime (UTC): " +
|
||||
heartbeatJSON["time"],
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -11,7 +11,7 @@ class HomeAssistant extends NotificationProvider {
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
|
||||
`${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
|
||||
{
|
||||
title: "Uptime Kuma",
|
||||
message,
|
||||
|
@ -33,7 +33,10 @@ class Line extends NotificationProvider {
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
"text": "UptimeKuma Alert: [🔴 Down]\n" +
|
||||
"Name: " + monitorJSON["name"] + " \n" +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
}
|
||||
]
|
||||
};
|
||||
@ -44,7 +47,10 @@ class Line extends NotificationProvider {
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
"text": "UptimeKuma Alert: [✅ Up]\n" +
|
||||
"Name: " + monitorJSON["name"] + " \n" +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -24,12 +24,18 @@ class LineNotify extends NotificationProvider {
|
||||
await axios.post(lineAPIUrl, qs.stringify(testMessage), config);
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let downMessage = {
|
||||
"message": "\n[🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
"message": "\n[🔴 Down]\n" +
|
||||
"Name: " + monitorJSON["name"] + " \n" +
|
||||
heartbeatJSON["msg"] + "\n" +
|
||||
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lineAPIUrl, qs.stringify(downMessage), config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let upMessage = {
|
||||
"message": "\n[✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
"message": "\n[✅ Up]\n" +
|
||||
"Name: " + monitorJSON["name"] + " \n" +
|
||||
heartbeatJSON["msg"] + "\n" +
|
||||
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lineAPIUrl, qs.stringify(upMessage), config);
|
||||
}
|
||||
|
@ -28,7 +28,9 @@ class LunaSea extends NotificationProvider {
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
let downdata = {
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
"body": "[🔴 Down] " +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lunaseaurl, downdata);
|
||||
return okMsg;
|
||||
@ -37,7 +39,9 @@ class LunaSea extends NotificationProvider {
|
||||
if (heartbeatJSON["status"] === UP) {
|
||||
let updata = {
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
"body": "[✅ Up] " +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lunaseaurl, updata);
|
||||
return okMsg;
|
||||
|
@ -10,7 +10,7 @@ class Mattermost extends NotificationProvider {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let mattermostTestData = {
|
||||
username: mattermostUserName,
|
||||
@ -27,97 +27,79 @@ class Mattermost extends NotificationProvider {
|
||||
}
|
||||
|
||||
const mattermostIconEmoji = notification.mattermosticonemo;
|
||||
const mattermostIconUrl = notification.mattermosticonurl;
|
||||
let mattermostIconEmojiOnline = "";
|
||||
let mattermostIconEmojiOffline = "";
|
||||
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
let mattermostdowndata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: mattermostIconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON["name"] +
|
||||
" service went down.",
|
||||
color: "#FF0000",
|
||||
title:
|
||||
"❌ " +
|
||||
monitorJSON["name"] +
|
||||
" service went down. ❌",
|
||||
title_link: monitorJSON["url"],
|
||||
fields: [
|
||||
{
|
||||
short: true,
|
||||
title: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
short: true,
|
||||
title: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
short: false,
|
||||
title: "Error",
|
||||
value: heartbeatJSON["msg"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostdowndata
|
||||
);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let mattermostupdata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: mattermostIconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON["name"] +
|
||||
" service went up!",
|
||||
color: "#32CD32",
|
||||
title:
|
||||
"✅ " +
|
||||
monitorJSON["name"] +
|
||||
" service went up! ✅",
|
||||
title_link: monitorJSON["url"],
|
||||
fields: [
|
||||
{
|
||||
short: true,
|
||||
title: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
short: true,
|
||||
title: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
short: false,
|
||||
title: "Ping",
|
||||
value: heartbeatJSON["ping"] + "ms",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostupdata
|
||||
);
|
||||
return okMsg;
|
||||
if (mattermostIconEmoji && typeof mattermostIconEmoji === "string") {
|
||||
const emojiArray = mattermostIconEmoji.split(" ");
|
||||
if (emojiArray.length >= 2) {
|
||||
mattermostIconEmojiOnline = emojiArray[0];
|
||||
mattermostIconEmojiOffline = emojiArray[1];
|
||||
}
|
||||
}
|
||||
const mattermostIconUrl = notification.mattermosticonurl;
|
||||
let iconEmoji = mattermostIconEmoji;
|
||||
let statusField = {
|
||||
short: false,
|
||||
title: "Error",
|
||||
value: heartbeatJSON.msg,
|
||||
};
|
||||
let statusText = "unknown";
|
||||
let color = "#000000";
|
||||
if (heartbeatJSON.status === DOWN) {
|
||||
iconEmoji = mattermostIconEmojiOffline || mattermostIconEmoji;
|
||||
statusField = {
|
||||
short: false,
|
||||
title: "Error",
|
||||
value: heartbeatJSON.msg,
|
||||
};
|
||||
statusText = "down.";
|
||||
color = "#FF0000";
|
||||
} else if (heartbeatJSON.status === UP) {
|
||||
iconEmoji = mattermostIconEmojiOnline || mattermostIconEmoji;
|
||||
statusField = {
|
||||
short: false,
|
||||
title: "Ping",
|
||||
value: heartbeatJSON.ping + "ms",
|
||||
};
|
||||
statusText = "up!";
|
||||
color = "#32CD32";
|
||||
}
|
||||
|
||||
let mattermostdata = {
|
||||
username: monitorJSON.name + " " + mattermostUserName,
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: iconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON.name +
|
||||
" service went " +
|
||||
statusText,
|
||||
color: color,
|
||||
title:
|
||||
monitorJSON.name +
|
||||
" service went " +
|
||||
statusText,
|
||||
title_link: monitorJSON.url,
|
||||
fields: [
|
||||
statusField,
|
||||
{
|
||||
short: true,
|
||||
title: `Time (${heartbeatJSON["timezone"]})`,
|
||||
value: heartbeatJSON.localDateTime,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostdata
|
||||
);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Ntfy extends NotificationProvider {
|
||||
|
||||
@ -9,16 +10,54 @@ class Ntfy extends NotificationProvider {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
let headers = {};
|
||||
if (notification.ntfyusername) {
|
||||
if (notification.ntfyAuthenticationMethod === "usernamePassword") {
|
||||
headers = {
|
||||
"Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"),
|
||||
};
|
||||
} else if (notification.ntfyAuthenticationMethod === "accessToken") {
|
||||
headers = {
|
||||
"Authorization": "Bearer " + notification.ntfyaccesstoken,
|
||||
};
|
||||
}
|
||||
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let ntfyTestData = {
|
||||
"topic": notification.ntfytopic,
|
||||
"title": (monitorJSON?.name || notification.ntfytopic) + " [Uptime-Kuma]",
|
||||
"message": msg,
|
||||
"priority": notification.ntfyPriority,
|
||||
"tags": [ "test_tube" ],
|
||||
};
|
||||
await axios.post(`${notification.ntfyserverurl}`, ntfyTestData, { headers: headers });
|
||||
return okMsg;
|
||||
}
|
||||
let tags = [];
|
||||
let status = "unknown";
|
||||
let priority = notification.ntfyPriority || 4;
|
||||
if ("status" in heartbeatJSON) {
|
||||
if (heartbeatJSON.status === DOWN) {
|
||||
tags = [ "red_circle" ];
|
||||
status = "Down";
|
||||
// if priority is not 5, increase priority for down alerts
|
||||
priority = priority === 5 ? priority : priority + 1;
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
tags = [ "green_circle" ];
|
||||
status = "Up";
|
||||
}
|
||||
}
|
||||
let data = {
|
||||
"topic": notification.ntfytopic,
|
||||
"message": msg,
|
||||
"priority": notification.ntfyPriority || 4,
|
||||
"title": "Uptime-Kuma",
|
||||
"message": heartbeatJSON.msg,
|
||||
"priority": priority,
|
||||
"title": monitorJSON.name + " " + status + " [Uptime-Kuma]",
|
||||
"tags": tags,
|
||||
"actions": [
|
||||
{
|
||||
"action": "view",
|
||||
"label": "Open " + monitorJSON.name,
|
||||
"url": monitorJSON.url,
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (notification.ntfyIcon) {
|
||||
|
@ -15,7 +15,7 @@ class Opsgenie extends NotificationProvider {
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let opsgenieAlertsUrl;
|
||||
let priority = (notification.opsgeniePriority == "") ? 3 : notification.opsgeniePriority;
|
||||
let priority = (!notification.opsgeniePriority) ? 3 : notification.opsgeniePriority;
|
||||
const textMsg = "Uptime Kuma Alert";
|
||||
|
||||
try {
|
||||
|
@ -29,14 +29,18 @@ class Pushbullet extends NotificationProvider {
|
||||
let downData = {
|
||||
"type": "note",
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
"body": "[🔴 Down] " +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
};
|
||||
await axios.post(pushbulletUrl, downData, config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let upData = {
|
||||
"type": "note",
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
"body": "[✅ Up] " +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
};
|
||||
await axios.post(pushbulletUrl, upData, config);
|
||||
}
|
||||
|
@ -24,13 +24,16 @@ class Pushover extends NotificationProvider {
|
||||
if (notification.pushoverdevice) {
|
||||
data.device = notification.pushoverdevice;
|
||||
}
|
||||
if (notification.pushoverttl) {
|
||||
data.ttl = notification.pushoverttl;
|
||||
}
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
await axios.post(pushoverlink, data);
|
||||
return okMsg;
|
||||
} else {
|
||||
data.message += "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"];
|
||||
data.message += `\n<b>Time (${heartbeatJSON["timezone"]})</b>:${heartbeatJSON["localDateTime"]}`;
|
||||
await axios.post(pushoverlink, data);
|
||||
return okMsg;
|
||||
}
|
||||
|
@ -22,8 +22,6 @@ class RocketChat extends NotificationProvider {
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"channel": notification.rocketchannel,
|
||||
@ -31,7 +29,7 @@ class RocketChat extends NotificationProvider {
|
||||
"icon_emoji": notification.rocketiconemo,
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Uptime Kuma Alert *Time (UTC)*\n" + time,
|
||||
"title": `Uptime Kuma Alert *Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`,
|
||||
"text": "*Message*\n" + msg,
|
||||
}
|
||||
]
|
||||
|
@ -39,7 +39,6 @@ class Slack extends NotificationProvider {
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
const textMsg = "Uptime Kuma Alert";
|
||||
let data = {
|
||||
"text": `${textMsg}\n${msg}`,
|
||||
@ -65,7 +64,7 @@ class Slack extends NotificationProvider {
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Time (UTC)*\n" + time,
|
||||
"text": `*Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`,
|
||||
}],
|
||||
}
|
||||
],
|
||||
|
@ -91,7 +91,7 @@ class SMTP extends NotificationProvider {
|
||||
|
||||
let bodyTextContent = msg;
|
||||
if (heartbeatJSON) {
|
||||
bodyTextContent = `${msg}\nTime (UTC): ${heartbeatJSON["time"]}`;
|
||||
bodyTextContent = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
|
||||
}
|
||||
|
||||
// send mail with defined transport object
|
||||
|
@ -25,8 +25,11 @@ class Telegram extends NotificationProvider {
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
let msg = (error.response.data.description) ? error.response.data.description : "Error without description";
|
||||
throw new Error(msg);
|
||||
if (error.response && error.response.data && error.response.data.description) {
|
||||
throw new Error(error.response.data.description);
|
||||
} else {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
class Plugin {
|
||||
async load() {
|
||||
|
||||
}
|
||||
|
||||
async unload() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Plugin,
|
||||
};
|
@ -1,256 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const { log } = require("../src/util");
|
||||
const path = require("path");
|
||||
const axios = require("axios");
|
||||
const { Git } = require("./git");
|
||||
const childProcess = require("child_process");
|
||||
|
||||
class PluginsManager {
|
||||
|
||||
static disable = false;
|
||||
|
||||
/**
|
||||
* Plugin List
|
||||
* @type {PluginWrapper[]}
|
||||
*/
|
||||
pluginList = [];
|
||||
|
||||
/**
|
||||
* Plugins Dir
|
||||
*/
|
||||
pluginsDir;
|
||||
|
||||
server;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {UptimeKumaServer} server
|
||||
*/
|
||||
constructor(server) {
|
||||
this.server = server;
|
||||
|
||||
if (!PluginsManager.disable) {
|
||||
this.pluginsDir = "./data/plugins/";
|
||||
|
||||
if (! fs.existsSync(this.pluginsDir)) {
|
||||
fs.mkdirSync(this.pluginsDir, { recursive: true });
|
||||
}
|
||||
|
||||
log.debug("plugin", "Scanning plugin directory");
|
||||
let list = fs.readdirSync(this.pluginsDir);
|
||||
|
||||
this.pluginList = [];
|
||||
for (let item of list) {
|
||||
this.loadPlugin(item);
|
||||
}
|
||||
|
||||
} else {
|
||||
log.warn("PLUGIN", "Skip scanning plugin directory");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a Plugin
|
||||
*/
|
||||
async loadPlugin(name) {
|
||||
log.info("plugin", "Load " + name);
|
||||
let plugin = new PluginWrapper(this.server, this.pluginsDir + name);
|
||||
|
||||
try {
|
||||
await plugin.load();
|
||||
this.pluginList.push(plugin);
|
||||
} catch (e) {
|
||||
log.error("plugin", "Failed to load plugin: " + this.pluginsDir + name);
|
||||
log.error("plugin", "Reason: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a Plugin
|
||||
* @param {string} repoURL Git repo url
|
||||
* @param {string} name Directory name, also known as plugin unique name
|
||||
*/
|
||||
downloadPlugin(repoURL, name) {
|
||||
if (fs.existsSync(this.pluginsDir + name)) {
|
||||
log.info("plugin", "Plugin folder already exists? Removing...");
|
||||
fs.rmSync(this.pluginsDir + name, {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
log.info("plugin", "Installing plugin: " + name + " " + repoURL);
|
||||
let result = Git.clone(repoURL, this.pluginsDir, name);
|
||||
log.info("plugin", "Install result: " + result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a plugin
|
||||
* @param {string} name
|
||||
*/
|
||||
async removePlugin(name) {
|
||||
log.info("plugin", "Removing plugin: " + name);
|
||||
for (let plugin of this.pluginList) {
|
||||
if (plugin.info.name === name) {
|
||||
await plugin.unload();
|
||||
|
||||
// Delete the plugin directory
|
||||
fs.rmSync(this.pluginsDir + name, {
|
||||
recursive: true
|
||||
});
|
||||
|
||||
this.pluginList.splice(this.pluginList.indexOf(plugin), 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
log.warn("plugin", "Plugin not found: " + name);
|
||||
throw new Error("Plugin not found: " + name);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Update a plugin
|
||||
* Only available for plugins which were downloaded from the official list
|
||||
* @param pluginID
|
||||
*/
|
||||
updatePlugin(pluginID) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin list from server + local installed plugin list
|
||||
* Item will be merged if the `name` is the same.
|
||||
* @returns {Promise<[]>}
|
||||
*/
|
||||
async fetchPluginList() {
|
||||
let remotePluginList;
|
||||
try {
|
||||
const res = await axios.get("https://uptime.kuma.pet/c/plugins.json");
|
||||
remotePluginList = res.data.pluginList;
|
||||
} catch (e) {
|
||||
log.error("plugin", "Failed to fetch plugin list: " + e.message);
|
||||
remotePluginList = [];
|
||||
}
|
||||
|
||||
for (let plugin of this.pluginList) {
|
||||
let find = false;
|
||||
// Try to merge
|
||||
for (let remotePlugin of remotePluginList) {
|
||||
if (remotePlugin.name === plugin.info.name) {
|
||||
find = true;
|
||||
remotePlugin.installed = true;
|
||||
remotePlugin.name = plugin.info.name;
|
||||
remotePlugin.fullName = plugin.info.fullName;
|
||||
remotePlugin.description = plugin.info.description;
|
||||
remotePlugin.version = plugin.info.version;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Local plugin
|
||||
if (!find) {
|
||||
plugin.info.local = true;
|
||||
remotePluginList.push(plugin.info);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort Installed first, then sort by name
|
||||
return remotePluginList.sort((a, b) => {
|
||||
if (a.installed === b.installed) {
|
||||
if (a.fullName < b.fullName) {
|
||||
return -1;
|
||||
}
|
||||
if (a.fullName > b.fullName) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} else if (a.installed) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class PluginWrapper {
|
||||
|
||||
server = undefined;
|
||||
pluginDir = undefined;
|
||||
|
||||
/**
|
||||
* Must be an `new-able` class.
|
||||
* @type {function}
|
||||
*/
|
||||
pluginClass = undefined;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Plugin}
|
||||
*/
|
||||
object = undefined;
|
||||
info = {};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {UptimeKumaServer} server
|
||||
* @param {string} pluginDir
|
||||
*/
|
||||
constructor(server, pluginDir) {
|
||||
this.server = server;
|
||||
this.pluginDir = pluginDir;
|
||||
}
|
||||
|
||||
async load() {
|
||||
let indexFile = this.pluginDir + "/index.js";
|
||||
let packageJSON = this.pluginDir + "/package.json";
|
||||
|
||||
log.info("plugin", "Installing dependencies");
|
||||
|
||||
if (fs.existsSync(indexFile)) {
|
||||
// Install dependencies
|
||||
let result = childProcess.spawnSync("npm", [ "install" ], {
|
||||
cwd: this.pluginDir,
|
||||
env: {
|
||||
...process.env,
|
||||
PLAYWRIGHT_BROWSERS_PATH: "../../browsers", // Special handling for read-browser-monitor
|
||||
}
|
||||
});
|
||||
|
||||
if (result.stdout) {
|
||||
log.info("plugin", "Install dependencies result: " + result.stdout.toString("utf-8"));
|
||||
} else {
|
||||
log.warn("plugin", "Install dependencies result: no output");
|
||||
}
|
||||
|
||||
this.pluginClass = require(path.join(process.cwd(), indexFile));
|
||||
|
||||
let pluginClassType = typeof this.pluginClass;
|
||||
|
||||
if (pluginClassType === "function") {
|
||||
this.object = new this.pluginClass(this.server);
|
||||
await this.object.load();
|
||||
} else {
|
||||
throw new Error("Invalid plugin, it does not export a class");
|
||||
}
|
||||
|
||||
if (fs.existsSync(packageJSON)) {
|
||||
this.info = require(path.join(process.cwd(), packageJSON));
|
||||
} else {
|
||||
this.info.fullName = this.pluginDir;
|
||||
this.info.name = "[unknown]";
|
||||
this.info.version = "[unknown-version]";
|
||||
}
|
||||
|
||||
this.info.installed = true;
|
||||
log.info("plugin", `${this.info.fullName} v${this.info.version} loaded`);
|
||||
}
|
||||
}
|
||||
|
||||
async unload() {
|
||||
await this.object.unload();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PluginsManager,
|
||||
PluginWrapper
|
||||
};
|
@ -28,7 +28,7 @@ const monitorResponseTime = new PrometheusClient.Gauge({
|
||||
|
||||
const monitorStatus = new PrometheusClient.Gauge({
|
||||
name: "monitor_status",
|
||||
help: "Monitor Status (1 = UP, 0= DOWN)",
|
||||
help: "Monitor Status (1 = UP, 0= DOWN, 2= PENDING, 3= MAINTENANCE)",
|
||||
labelNames: commonLabels
|
||||
});
|
||||
|
||||
|
@ -19,6 +19,11 @@ const nodeVersion = parseInt(process.versions.node.split(".")[0]);
|
||||
const requiredVersion = 14;
|
||||
console.log(`Your Node.js version: ${nodeVersion}`);
|
||||
|
||||
// See more: https://github.com/louislam/uptime-kuma/issues/3138
|
||||
if (nodeVersion >= 20) {
|
||||
console.warn("\x1b[31m%s\x1b[0m", "Warning: Uptime Kuma is currently not stable on Node.js >= 20, please use Node.js 18.");
|
||||
}
|
||||
|
||||
if (nodeVersion < requiredVersion) {
|
||||
console.error(`Error: Your Node.js version is not supported, please upgrade to Node.js >= ${requiredVersion}.`);
|
||||
process.exit(-1);
|
||||
@ -142,8 +147,8 @@ const { apiKeySocketHandler } = require("./socket-handlers/api-key-socket-handle
|
||||
const { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
|
||||
const { Settings } = require("./settings");
|
||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||
const { pluginsHandler } = require("./socket-handlers/plugins-handler");
|
||||
const apicache = require("./modules/apicache");
|
||||
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@ -156,12 +161,6 @@ app.use(function (req, res, next) {
|
||||
next();
|
||||
});
|
||||
|
||||
/**
|
||||
* Use for decode the auth object
|
||||
* @type {null}
|
||||
*/
|
||||
let jwtSecret = null;
|
||||
|
||||
/**
|
||||
* Show Setup Page
|
||||
* @type {boolean}
|
||||
@ -172,7 +171,6 @@ let needSetup = false;
|
||||
Database.init(args);
|
||||
await initDatabase(testMode);
|
||||
await server.initAfterDatabaseReady();
|
||||
server.loadPlugins();
|
||||
server.entryPage = await Settings.get("entryPage");
|
||||
await StatusPage.loadDomainMappingList();
|
||||
|
||||
@ -281,7 +279,7 @@ let needSetup = false;
|
||||
log.info("auth", `Login by token. IP=${clientIP}`);
|
||||
|
||||
try {
|
||||
let decoded = jwt.verify(token, jwtSecret);
|
||||
let decoded = jwt.verify(token, server.jwtSecret);
|
||||
|
||||
log.info("auth", "Username from JWT: " + decoded.username);
|
||||
|
||||
@ -352,7 +350,7 @@ let needSetup = false;
|
||||
ok: true,
|
||||
token: jwt.sign({
|
||||
username: data.username,
|
||||
}, jwtSecret),
|
||||
}, server.jwtSecret),
|
||||
});
|
||||
}
|
||||
|
||||
@ -382,7 +380,7 @@ let needSetup = false;
|
||||
ok: true,
|
||||
token: jwt.sign({
|
||||
username: data.username,
|
||||
}, jwtSecret),
|
||||
}, server.jwtSecret),
|
||||
});
|
||||
} else {
|
||||
|
||||
@ -671,6 +669,7 @@ let needSetup = false;
|
||||
// Edit a monitor
|
||||
socket.on("editMonitor", async (monitor, callback) => {
|
||||
try {
|
||||
let removeGroupChildren = false;
|
||||
checkLogin(socket);
|
||||
|
||||
let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ]);
|
||||
@ -679,8 +678,22 @@ let needSetup = false;
|
||||
throw new Error("Permission denied.");
|
||||
}
|
||||
|
||||
// Check if Parent is Descendant (would cause endless loop)
|
||||
if (monitor.parent !== null) {
|
||||
const childIDs = await Monitor.getAllChildrenIDs(monitor.id);
|
||||
if (childIDs.includes(monitor.parent)) {
|
||||
throw new Error("Invalid Monitor Group");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove children if monitor type has changed (from group to non-group)
|
||||
if (bean.type === "group" && monitor.type !== bean.type) {
|
||||
removeGroupChildren = true;
|
||||
}
|
||||
|
||||
bean.name = monitor.name;
|
||||
bean.description = monitor.description;
|
||||
bean.parent = monitor.parent;
|
||||
bean.type = monitor.type;
|
||||
bean.url = monitor.url;
|
||||
bean.method = monitor.method;
|
||||
@ -739,9 +752,13 @@ let needSetup = false;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
if (removeGroupChildren) {
|
||||
await Monitor.unlinkAllChildren(monitor.id);
|
||||
}
|
||||
|
||||
await updateMonitorNotification(bean.id, monitor.notificationIDList);
|
||||
|
||||
if (bean.active) {
|
||||
if (bean.isActive()) {
|
||||
await restartMonitor(socket.userID, bean.id);
|
||||
}
|
||||
|
||||
@ -884,6 +901,8 @@ let needSetup = false;
|
||||
delete server.monitorList[monitorID];
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
socket.userID,
|
||||
@ -892,6 +911,10 @@ let needSetup = false;
|
||||
// Fix #2880
|
||||
apicache.clear();
|
||||
|
||||
const endTime = Date.now();
|
||||
|
||||
log.info("DB", `Delete Monitor completed in : ${endTime - startTime} ms`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully.",
|
||||
@ -1135,6 +1158,8 @@ let needSetup = false;
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
}
|
||||
|
||||
const previousChromeExecutable = await Settings.get("chromeExecutable");
|
||||
|
||||
await setSettings("general", data);
|
||||
server.entryPage = data.entryPage;
|
||||
|
||||
@ -1145,6 +1170,12 @@ let needSetup = false;
|
||||
await server.setTimezone(data.serverTimezone);
|
||||
}
|
||||
|
||||
// If Chrome Executable is changed, need to reset the browser
|
||||
if (previousChromeExecutable !== data.chromeExecutable) {
|
||||
log.info("settings", "Chrome executable is changed. Resetting Chrome...");
|
||||
await resetChrome();
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved"
|
||||
@ -1353,7 +1384,7 @@ let needSetup = false;
|
||||
accepted_statuscodes: monitorListData[i].accepted_statuscodes,
|
||||
dns_resolve_type: monitorListData[i].dns_resolve_type,
|
||||
dns_resolve_server: monitorListData[i].dns_resolve_server,
|
||||
notificationIDList: {},
|
||||
notificationIDList: monitorListData[i].notificationIDList,
|
||||
proxy_id: monitorListData[i].proxy_id || null,
|
||||
};
|
||||
|
||||
@ -1515,7 +1546,6 @@ let needSetup = false;
|
||||
maintenanceSocketHandler(socket);
|
||||
apiKeySocketHandler(socket);
|
||||
generalSocketHandler(socket, server);
|
||||
pluginsHandler(socket, server);
|
||||
|
||||
log.debug("server", "added all socket handlers");
|
||||
|
||||
@ -1559,7 +1589,7 @@ let needSetup = false;
|
||||
}
|
||||
});
|
||||
|
||||
initBackgroundJobs(args);
|
||||
await initBackgroundJobs();
|
||||
|
||||
// Start cloudflared at the end if configured
|
||||
await cloudflaredAutoStart(cloudflaredToken);
|
||||
@ -1685,7 +1715,7 @@ async function initDatabase(testMode = false) {
|
||||
needSetup = true;
|
||||
}
|
||||
|
||||
jwtSecret = jwtSecretBean.value;
|
||||
server.jwtSecret = jwtSecretBean.value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,6 +3,7 @@ const { Settings } = require("../settings");
|
||||
const { sendInfo } = require("../client");
|
||||
const { checkLogin } = require("../util-server");
|
||||
const GameResolver = require("gamedig/lib/GameResolver");
|
||||
const { testChrome } = require("../monitor-types/real-browser-monitor-type");
|
||||
|
||||
let gameResolver = new GameResolver();
|
||||
let gameList = null;
|
||||
@ -47,4 +48,18 @@ module.exports.generalSocketHandler = (socket, server) => {
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("testChrome", (executable, callback) => {
|
||||
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
|
||||
testChrome(executable).then((version) => {
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Found Chromium/Chrome. Version: " + version,
|
||||
});
|
||||
}).catch((e) => {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -186,7 +186,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
log.debug("maintenance", `Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||
|
||||
let monitors = await R.getAll("SELECT monitor.id, monitor.name FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [
|
||||
let monitors = await R.getAll("SELECT monitor.id FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [
|
||||
maintenanceID,
|
||||
]);
|
||||
|
||||
|
@ -1,69 +0,0 @@
|
||||
const { checkLogin } = require("../util-server");
|
||||
const { PluginsManager } = require("../plugins-manager");
|
||||
const { log } = require("../../src/util.js");
|
||||
|
||||
/**
|
||||
* Handlers for plugins
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @param {UptimeKumaServer} server
|
||||
*/
|
||||
module.exports.pluginsHandler = (socket, server) => {
|
||||
|
||||
const pluginManager = server.getPluginManager();
|
||||
|
||||
// Get Plugin List
|
||||
socket.on("getPluginList", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
log.debug("plugin", "PluginManager.disable: " + PluginsManager.disable);
|
||||
|
||||
if (PluginsManager.disable) {
|
||||
throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/");
|
||||
}
|
||||
|
||||
let pluginList = await pluginManager.fetchPluginList();
|
||||
callback({
|
||||
ok: true,
|
||||
pluginList,
|
||||
});
|
||||
} catch (error) {
|
||||
log.warn("plugin", "Error: " + error.message);
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("installPlugin", async (repoURL, name, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
pluginManager.downloadPlugin(repoURL, name);
|
||||
await pluginManager.loadPlugin(name);
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("uninstallPlugin", async (name, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await pluginManager.removePlugin(name);
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@ -10,7 +10,6 @@ const util = require("util");
|
||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||
const { Settings } = require("./settings");
|
||||
const dayjs = require("dayjs");
|
||||
const { PluginsManager } = require("./plugins-manager");
|
||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
|
||||
|
||||
/**
|
||||
@ -47,12 +46,6 @@ class UptimeKumaServer {
|
||||
*/
|
||||
indexHTML = "";
|
||||
|
||||
/**
|
||||
* Plugins Manager
|
||||
* @type {PluginsManager}
|
||||
*/
|
||||
pluginsManager = null;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {{}}
|
||||
@ -61,6 +54,12 @@ class UptimeKumaServer {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Use for decode the auth object
|
||||
* @type {null}
|
||||
*/
|
||||
jwtSecret = null;
|
||||
|
||||
static getInstance(args) {
|
||||
if (UptimeKumaServer.instance == null) {
|
||||
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
||||
@ -98,11 +97,17 @@ class UptimeKumaServer {
|
||||
}
|
||||
}
|
||||
|
||||
// Set Monitor Types
|
||||
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
|
||||
|
||||
this.io = new Server(this.httpServer);
|
||||
}
|
||||
|
||||
/** Initialise app after the database has been set up */
|
||||
async initAfterDatabaseReady() {
|
||||
// Static
|
||||
this.app.use("/screenshots", express.static(Database.screenshotDir));
|
||||
|
||||
await CacheableDnsHttpAgent.update();
|
||||
|
||||
process.env.TZ = await this.getTimezone();
|
||||
@ -289,46 +294,6 @@ class UptimeKumaServer {
|
||||
async stop() {
|
||||
|
||||
}
|
||||
|
||||
loadPlugins() {
|
||||
this.pluginsManager = new PluginsManager(this);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {PluginsManager}
|
||||
*/
|
||||
getPluginManager() {
|
||||
return this.pluginsManager;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {MonitorType} monitorType
|
||||
*/
|
||||
addMonitorType(monitorType) {
|
||||
if (monitorType instanceof MonitorType && monitorType.name) {
|
||||
if (monitorType.name in UptimeKumaServer.monitorTypeList) {
|
||||
log.error("", "Conflict Monitor Type name");
|
||||
}
|
||||
UptimeKumaServer.monitorTypeList[monitorType.name] = monitorType;
|
||||
} else {
|
||||
log.error("", "Invalid Monitor Type: " + monitorType.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {MonitorType} monitorType
|
||||
*/
|
||||
removeMonitorType(monitorType) {
|
||||
if (UptimeKumaServer.monitorTypeList[monitorType.name] === monitorType) {
|
||||
delete UptimeKumaServer.monitorTypeList[monitorType.name];
|
||||
} else {
|
||||
log.error("", "Remove MonitorType failed: " + monitorType.name);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@ -337,3 +302,4 @@ module.exports = {
|
||||
|
||||
// Must be at the end
|
||||
const { MonitorType } = require("./monitor-types/monitor-type");
|
||||
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");
|
||||
|
@ -342,7 +342,12 @@ exports.mysqlQuery = function (connectionString, query) {
|
||||
resolve("No Error, but the result is not an array. Type: " + typeof res);
|
||||
}
|
||||
}
|
||||
connection.destroy();
|
||||
|
||||
try {
|
||||
connection.end();
|
||||
} catch (_) {
|
||||
connection.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -408,12 +413,18 @@ exports.radius = function (
|
||||
exports.redisPingAsync = function (dsn) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = redis.createClient({
|
||||
url: dsn,
|
||||
url: dsn
|
||||
});
|
||||
client.on("error", (err) => {
|
||||
if (client.isOpen) {
|
||||
client.disconnect();
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
client.connect().then(() => {
|
||||
if (!client.isOpen) {
|
||||
client.emit("error", new Error("connection isn't open"));
|
||||
}
|
||||
client.ping().then((res, err) => {
|
||||
if (client.isOpen) {
|
||||
client.disconnect();
|
||||
@ -423,7 +434,7 @@ exports.redisPingAsync = function (dsn) {
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
});
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -519,12 +530,16 @@ const parseCertificateInfo = function (info) {
|
||||
|
||||
// Move up the chain until loop is encountered
|
||||
if (link.issuerCertificate == null) {
|
||||
link.certType = (i === 0) ? "self-signed" : "root CA";
|
||||
break;
|
||||
} else if (link.issuerCertificate.fingerprint in existingList) {
|
||||
// a root CA certificate is typically "signed by itself" (=> "self signed certificate") and thus the "issuerCertificate" is a reference to itself.
|
||||
log.debug("cert", `[Last] ${link.issuerCertificate.fingerprint}`);
|
||||
link.certType = (i === 0) ? "self-signed" : "root CA";
|
||||
link.issuerCertificate = null;
|
||||
break;
|
||||
} else {
|
||||
link.certType = (i === 0) ? "server" : "intermediate CA";
|
||||
link = link.issuerCertificate;
|
||||
}
|
||||
|
||||
|
@ -266,6 +266,11 @@ optgroup {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
|
||||
.form-select:disabled {
|
||||
color: rgba($dark-font-color, 0.7);
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
@ -1,6 +1,12 @@
|
||||
@import "vars.scss";
|
||||
@import "node_modules/vue-multiselect/dist/vue-multiselect";
|
||||
|
||||
.multiselect {
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__tags {
|
||||
border-radius: 1.5rem;
|
||||
border: 1px solid #ced4da;
|
||||
@ -14,10 +20,12 @@
|
||||
|
||||
.multiselect__option--highlight {
|
||||
background: $primary !important;
|
||||
color: $dark-font-color2 !important;
|
||||
}
|
||||
|
||||
.multiselect__option--highlight::after {
|
||||
background: $primary !important;
|
||||
color: $dark-font-color2 !important;
|
||||
}
|
||||
|
||||
.multiselect__tag {
|
||||
|
@ -48,15 +48,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
id="monitor-submit-btn" class="btn btn-primary" type="submit"
|
||||
:disabled="processing"
|
||||
>
|
||||
{{ $t("Generate") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
id="monitor-submit-btn" class="btn btn-primary" type="submit"
|
||||
:disabled="processing"
|
||||
>
|
||||
{{ $t("Generate") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
299
src/components/BadgeGeneratorDialog.vue
Normal file
299
src/components/BadgeGeneratorDialog.vue
Normal file
@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<div ref="BadgeGeneratorModal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
{{ $t("Badge Generator", [monitor.name]) }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">{{ $t("Badge Type") }}</label>
|
||||
<select id="type" v-model="badge.type" class="form-select">
|
||||
<option value="status">status</option>
|
||||
<option value="uptime">uptime</option>
|
||||
<option value="ping">ping</option>
|
||||
<option value="avg-response">avg-response</option>
|
||||
<option value="cert-exp">cert-exp</option>
|
||||
<option value="response">response</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('duration') " class="mb-3">
|
||||
<label for="duration" class="form-label">{{ $t("Badge Duration") }}</label>
|
||||
<input id="duration" v-model="badge.duration" type="number" min="0" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('label') " class="mb-3">
|
||||
<label for="label" class="form-label">{{ $t("Badge Label") }}</label>
|
||||
<input id="label" v-model="badge.label" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('prefix') " class="mb-3">
|
||||
<label for="prefix" class="form-label">{{ $t("Badge Prefix") }}</label>
|
||||
<input id="prefix" v-model="badge.prefix" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('suffix') " class="mb-3">
|
||||
<label for="suffix" class="form-label">{{ $t("Badge Suffix") }}</label>
|
||||
<input id="suffix" v-model="badge.suffix" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelColor') " class="mb-3">
|
||||
<label for="labelColor" class="form-label">{{ $t("Badge Label Color") }}</label>
|
||||
<input id="labelColor" v-model="badge.labelColor" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('color') " class="mb-3">
|
||||
<label for="color" class="form-label">{{ $t("Badge Color") }}</label>
|
||||
<input id="color" v-model="badge.color" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelPrefix') " class="mb-3">
|
||||
<label for="labelPrefix" class="form-label">{{ $t("Badge Label Prefix") }}</label>
|
||||
<input id="labelPrefix" v-model="badge.labelPrefix" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelSuffix') " class="mb-3">
|
||||
<label for="labelSuffix" class="form-label">{{ $t("Badge Label Suffix") }}</label>
|
||||
<input id="labelSuffix" v-model="badge.labelSuffix" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('upColor') " class="mb-3">
|
||||
<label for="upColor" class="form-label">{{ $t("Badge Up Color") }}</label>
|
||||
<input id="upColor" v-model="badge.upColor" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downColor') " class="mb-3">
|
||||
<label for="downColor" class="form-label">{{ $t("Badge Down Color") }}</label>
|
||||
<input id="downColor" v-model="badge.downColor" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('pendingColor') " class="mb-3">
|
||||
<label for="pendingColor" class="form-label">{{ $t("Badge Pending Color") }}</label>
|
||||
<input id="pendingColor" v-model="badge.pendingColor" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('maintenanceColor') " class="mb-3">
|
||||
<label for="maintenanceColor" class="form-label">{{ $t("Badge Maintenance Color") }}</label>
|
||||
<input id="maintenanceColor" v-model="badge.maintenanceColor" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnColor') " class="mb-3">
|
||||
<label for="warnColor" class="form-label">{{ $t("Badge Warn Color") }}</label>
|
||||
<input id="warnColor" v-model="badge.warnColor" type="number" min="0" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnDays') " class="mb-3">
|
||||
<label for="warnDays" class="form-label">{{ $t("Badge Warn Days") }}</label>
|
||||
<input id="warnDays" v-model="badge.warnDays" type="number" min="0" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downDays') " class="mb-3">
|
||||
<label for="downDays" class="form-label">{{ $t("Badge Down Days") }}</label>
|
||||
<input id="downDays" v-model="badge.downDays" type="number" min="0" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="style" class="form-label">{{ $t("Badge Style") }}</label>
|
||||
<select id="style" v-model="badge.style" class="form-select">
|
||||
<option value="plastic">plastic</option>
|
||||
<option value="flat">flat</option>
|
||||
<option value="flat-square">flat-square</option>
|
||||
<option value="for-the-badge">for-the-badge</option>
|
||||
<option value="social">social</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">{{ $t("Badge value (For Testing only.)") }}</label>
|
||||
<input id="value" v-model="badge.value" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="push-url" class="form-label">{{ $t("Badge URL") }}</label>
|
||||
<CopyableInput id="push-url" v-model="badgeURL" type="url" disabled="disabled" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal">
|
||||
{{ $t("Close") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap";
|
||||
import CopyableInput from "./CopyableInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CopyableInput
|
||||
},
|
||||
props: {},
|
||||
emits: [],
|
||||
data() {
|
||||
return {
|
||||
model: null,
|
||||
processing: false,
|
||||
monitor: {
|
||||
id: null,
|
||||
name: null,
|
||||
},
|
||||
badge: {
|
||||
type: "status",
|
||||
duration: null,
|
||||
label: null,
|
||||
prefix: null,
|
||||
suffix: null,
|
||||
labelColor: null,
|
||||
color: null,
|
||||
labelPrefix: null,
|
||||
labelSuffix: null,
|
||||
upColor: null,
|
||||
downColor: null,
|
||||
pendingColor: null,
|
||||
maintenanceColor: null,
|
||||
warnColor: null,
|
||||
warnDays: null,
|
||||
downDays: null,
|
||||
style: "flat",
|
||||
value: null,
|
||||
},
|
||||
parameters: {
|
||||
status: [
|
||||
"upLabel",
|
||||
"downLabel",
|
||||
"pendingLabel",
|
||||
"maintenanceLabel",
|
||||
"upColor",
|
||||
"downColor",
|
||||
"pendingColor",
|
||||
"maintenanceColor",
|
||||
],
|
||||
uptime: [
|
||||
"duration",
|
||||
"labelPrefix",
|
||||
"labelSuffix",
|
||||
"prefix",
|
||||
"suffix",
|
||||
"color",
|
||||
"labelColor",
|
||||
],
|
||||
ping: [
|
||||
"duration",
|
||||
"labelPrefix",
|
||||
"labelSuffix",
|
||||
"prefix",
|
||||
"suffix",
|
||||
"color",
|
||||
"labelColor",
|
||||
],
|
||||
"avg-response": [
|
||||
"duration",
|
||||
"labelPrefix",
|
||||
"labelSuffix",
|
||||
"prefix",
|
||||
"suffix",
|
||||
"color",
|
||||
"labelColor",
|
||||
],
|
||||
"cert-exp": [
|
||||
"labelPrefix",
|
||||
"labelSuffix",
|
||||
"prefix",
|
||||
"suffix",
|
||||
"upColor",
|
||||
"warnColor",
|
||||
"downColor",
|
||||
"warnDays",
|
||||
"downDays",
|
||||
"labelColor",
|
||||
],
|
||||
response: [
|
||||
"labelPrefix",
|
||||
"labelSuffix",
|
||||
"prefix",
|
||||
"suffix",
|
||||
"color",
|
||||
"labelColor",
|
||||
],
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
badgeURL() {
|
||||
if (!this.monitor.id || !this.badge.type) {
|
||||
return;
|
||||
}
|
||||
let badgeURL = this.$root.baseURL + "/api/badge/" + this.monitor.id + "/" + this.badge.type;
|
||||
|
||||
let parameterList = {};
|
||||
|
||||
for (let parameter of this.parameters[this.badge.type] || []) {
|
||||
if (parameter === "duration" && this.badge.duration) {
|
||||
badgeURL += "/" + this.badge.duration;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.badge[parameter]) {
|
||||
parameterList[parameter] = this.badge[parameter];
|
||||
}
|
||||
}
|
||||
|
||||
for (let parameter of [ "label", "style", "value" ]) {
|
||||
if (parameter === "style" && this.badge.style === "flat") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.badge[parameter]) {
|
||||
parameterList[parameter] = this.badge[parameter];
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(parameterList).length > 0) {
|
||||
return badgeURL + "?" + new URLSearchParams(parameterList);
|
||||
}
|
||||
|
||||
return badgeURL;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.BadgeGeneratorModal = new Modal(this.$refs.BadgeGeneratorModal);
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Setting monitor
|
||||
* @param {number} monitorId ID of monitor
|
||||
* @param {string} monitorName Name of monitor
|
||||
*/
|
||||
show(monitorId, monitorName) {
|
||||
this.monitor = {
|
||||
id: monitorId,
|
||||
name: monitorName,
|
||||
};
|
||||
|
||||
this.BadgeGeneratorModal.show();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.dark {
|
||||
.modal-dialog .form-text, .modal-dialog p {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -19,43 +19,18 @@
|
||||
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
||||
</div>
|
||||
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" :title="item.description">
|
||||
<div class="row">
|
||||
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="info">
|
||||
<Uptime :monitor="item" type="24" :pill="true" />
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="tags">
|
||||
<Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
||||
<div class="col-12 bottom-style">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<MonitorListItem v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item" :isSearch="searchText !== ''" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||
import Tag from "../components/Tag.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import MonitorListItem from "../components/MonitorListItem.vue";
|
||||
import { getMonitorRelativeURL } from "../util.ts";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Uptime,
|
||||
HeartbeatBar,
|
||||
Tag,
|
||||
MonitorListItem,
|
||||
},
|
||||
props: {
|
||||
/** Should the scrollbar be shown */
|
||||
@ -91,6 +66,20 @@ export default {
|
||||
sortedMonitorList() {
|
||||
let result = Object.values(this.$root.monitorList);
|
||||
|
||||
// Simple filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
if (this.searchText !== "") {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
result = result.filter(monitor => {
|
||||
return monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||
});
|
||||
} else {
|
||||
result = result.filter(monitor => monitor.parent === null);
|
||||
}
|
||||
|
||||
// Filter result by active state, weight and alphabetical
|
||||
result.sort((m1, m2) => {
|
||||
|
||||
if (m1.active !== m2.active) {
|
||||
@ -116,17 +105,6 @@ export default {
|
||||
return m1.name.localeCompare(m2.name);
|
||||
});
|
||||
|
||||
// Simple filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
if (this.searchText !== "") {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
result = result.filter(monitor => {
|
||||
return monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
204
src/components/MonitorListItem.vue
Normal file
204
src/components/MonitorListItem.vue
Normal file
@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div>
|
||||
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }">
|
||||
<div class="row">
|
||||
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="info" :style="depthMargin">
|
||||
<Uptime :monitor="monitor" type="24" :pill="true" />
|
||||
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed">
|
||||
<font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" />
|
||||
</span>
|
||||
{{ monitorName }}
|
||||
</div>
|
||||
<div class="tags">
|
||||
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||
<HeartbeatBar size="small" :monitor-id="monitor.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
||||
<div class="col-12 bottom-style">
|
||||
<HeartbeatBar size="small" :monitor-id="monitor.id" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<transition name="slide-fade-up">
|
||||
<div v-if="!isCollapsed" class="childs">
|
||||
<MonitorListItem v-for="(item, index) in sortedChildMonitorList" :key="index" :monitor="item" :isSearch="isSearch" :depth="depth + 1" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||
import Tag from "../components/Tag.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import { getMonitorRelativeURL } from "../util.ts";
|
||||
|
||||
export default {
|
||||
name: "MonitorListItem",
|
||||
components: {
|
||||
Uptime,
|
||||
HeartbeatBar,
|
||||
Tag,
|
||||
},
|
||||
props: {
|
||||
/** Monitor this represents */
|
||||
monitor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
/** If the user is currently searching */
|
||||
isSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** How many ancestors are above this monitor */
|
||||
depth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortedChildMonitorList() {
|
||||
let result = Object.values(this.$root.monitorList);
|
||||
|
||||
result = result.filter(childMonitor => childMonitor.parent === this.monitor.id);
|
||||
|
||||
result.sort((m1, m2) => {
|
||||
|
||||
if (m1.active !== m2.active) {
|
||||
if (m1.active === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (m2.active === 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (m1.weight !== m2.weight) {
|
||||
if (m1.weight > m2.weight) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (m1.weight < m2.weight) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return m1.name.localeCompare(m2.name);
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
hasChildren() {
|
||||
return this.sortedChildMonitorList.length > 0;
|
||||
},
|
||||
depthMargin() {
|
||||
return {
|
||||
marginLeft: `${31 * this.depth}px`,
|
||||
};
|
||||
},
|
||||
monitorName() {
|
||||
if (this.isSearch) {
|
||||
return this.monitor.pathName;
|
||||
} else {
|
||||
return this.monitor.name;
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
|
||||
// Always unfold if monitor is accessed directly
|
||||
if (this.monitor.childrenIDs.includes(parseInt(this.$route.params.id))) {
|
||||
this.isCollapsed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set collapsed value based on local storage
|
||||
let storage = window.localStorage.getItem("monitorCollapsed");
|
||||
if (storage === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let storageObject = JSON.parse(storage);
|
||||
if (storageObject[`monitor_${this.monitor.id}`] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isCollapsed = storageObject[`monitor_${this.monitor.id}`];
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Changes the collapsed value of the current monitor and saves it to local storage
|
||||
*/
|
||||
changeCollapsed() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
|
||||
// Save collapsed value into local storage
|
||||
let storage = window.localStorage.getItem("monitorCollapsed");
|
||||
let storageObject = {};
|
||||
if (storage !== null) {
|
||||
storageObject = JSON.parse(storage);
|
||||
}
|
||||
storageObject[`monitor_${this.monitor.id}`] = this.isCollapsed;
|
||||
|
||||
window.localStorage.setItem("monitorCollapsed", JSON.stringify(storageObject));
|
||||
},
|
||||
/**
|
||||
* Get URL of monitor
|
||||
* @param {number} id ID of monitor
|
||||
* @returns {string} Relative URL of monitor
|
||||
*/
|
||||
monitorURL(id) {
|
||||
return getMonitorRelativeURL(id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.small-padding {
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
|
||||
.collapse-padding {
|
||||
padding-left: 8px !important;
|
||||
padding-right: 2px !important;
|
||||
}
|
||||
|
||||
// .monitor-item {
|
||||
// width: 100%;
|
||||
// }
|
||||
|
||||
.tags {
|
||||
margin-top: 4px;
|
||||
padding-left: 67px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.animated {
|
||||
transition: all 0.2s $easing-in;
|
||||
}
|
||||
|
||||
</style>
|
123
src/components/MonitorSettingDialog.vue
Normal file
123
src/components/MonitorSettingDialog.vue
Normal file
@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div ref="MonitorSettingDialog" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
{{ $t("Monitor Setting", [monitor.name]) }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="my-3 form-check">
|
||||
<input id="show-clickable-link" v-model="monitor.isClickAble" class="form-check-input" type="checkbox" @click="toggleLink(monitor.group_index, monitor.monitor_index)" />
|
||||
<label class="form-check-label" for="show-clickable-link">
|
||||
{{ $t("Show Clickable Link") }}
|
||||
</label>
|
||||
<div class="form-text">
|
||||
{{ $t("Show Clickable Link Description") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-primary btn-add-group me-2"
|
||||
@click="$refs.badgeGeneratorDialog.show(monitor.id, monitor.name)"
|
||||
>
|
||||
<font-awesome-icon icon="certificate" />
|
||||
{{ $t("Open Badge Generator") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal">
|
||||
{{ $t("Close") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BadgeGeneratorDialog ref="badgeGeneratorDialog" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap";
|
||||
import BadgeGeneratorDialog from "./BadgeGeneratorDialog.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BadgeGeneratorDialog
|
||||
},
|
||||
props: {},
|
||||
emits: [],
|
||||
data() {
|
||||
return {
|
||||
monitor: {
|
||||
id: null,
|
||||
name: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {},
|
||||
|
||||
mounted() {
|
||||
this.MonitorSettingDialog = new Modal(this.$refs.MonitorSettingDialog);
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Setting monitor
|
||||
* @param {Object} group Data of monitor
|
||||
* @param {Object} monitor Data of monitor
|
||||
*/
|
||||
show(group, monitor) {
|
||||
this.monitor = {
|
||||
id: monitor.element.id,
|
||||
name: monitor.element.name,
|
||||
monitor_index: monitor.index,
|
||||
group_index: group.index,
|
||||
isClickAble: this.showLink(monitor),
|
||||
};
|
||||
|
||||
this.MonitorSettingDialog.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the value of sendUrl
|
||||
* @param {number} groupIndex Index of group monitor is member of
|
||||
* @param {number} index Index of monitor within group
|
||||
*/
|
||||
toggleLink(groupIndex, index) {
|
||||
this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl = !this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl;
|
||||
},
|
||||
|
||||
/**
|
||||
* Should a link to the monitor be shown?
|
||||
* Attempts to guess if a link should be shown based upon if
|
||||
* sendUrl is set and if the URL is default or not.
|
||||
* @param {Object} monitor Monitor to check
|
||||
* @param {boolean} [ignoreSendUrl=false] Should the presence of the sendUrl
|
||||
* property be ignored. This will only work in edit mode.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
showLink(monitor, ignoreSendUrl = false) {
|
||||
// We must check if there are any elements in monitorList to
|
||||
// prevent undefined errors if it hasn't been loaded yet
|
||||
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
|
||||
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword";
|
||||
}
|
||||
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.dark {
|
||||
.modal-dialog .form-text, .modal-dialog p {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -131,6 +131,7 @@ export default {
|
||||
"OneBot": "OneBot",
|
||||
"Opsgenie": "Opsgenie",
|
||||
"PagerDuty": "PagerDuty",
|
||||
"PagerTree": "PagerTree",
|
||||
"pushbullet": "Pushbullet",
|
||||
"PushByTechulus": "Push by Techulus",
|
||||
"pushover": "Pushover",
|
||||
|
@ -11,16 +11,16 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="chart-wrapper" :class="{ loading : loading}">
|
||||
<LineChart :chart-data="chartData" :options="chartOptions" />
|
||||
<Line :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
|
||||
import "chartjs-adapter-dayjs";
|
||||
import "chartjs-adapter-dayjs-4";
|
||||
import dayjs from "dayjs";
|
||||
import { LineChart } from "vue-chart-3";
|
||||
import { Line } from "vue-chartjs";
|
||||
import { useToast } from "vue-toastification";
|
||||
import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts";
|
||||
|
||||
@ -29,7 +29,7 @@ const toast = useToast();
|
||||
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
|
||||
|
||||
export default {
|
||||
components: { LineChart },
|
||||
components: { Line },
|
||||
props: {
|
||||
/** ID of monitor */
|
||||
monitorId: {
|
||||
@ -104,8 +104,10 @@ export default {
|
||||
}
|
||||
},
|
||||
ticks: {
|
||||
sampleSize: 3,
|
||||
maxRotation: 0,
|
||||
autoSkipPadding: 30,
|
||||
padding: 3,
|
||||
},
|
||||
grid: {
|
||||
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)",
|
||||
@ -197,6 +199,7 @@ export default {
|
||||
borderColor: "#5CDD8B",
|
||||
backgroundColor: "#5CDD8B38",
|
||||
yAxisID: "y",
|
||||
label: "ping",
|
||||
},
|
||||
{
|
||||
// Bar Chart
|
||||
@ -208,6 +211,8 @@ export default {
|
||||
barThickness: "flex",
|
||||
barPercentage: 1,
|
||||
categoryPercentage: 1,
|
||||
inflateAmount: 0.05,
|
||||
label: "status",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -1,102 +0,0 @@
|
||||
<template>
|
||||
<div v-if="! (!plugin.installed && plugin.local)" class="plugin-item pt-4 pb-2">
|
||||
<div class="info">
|
||||
<h5>{{ plugin.fullName }}</h5>
|
||||
<p class="description">
|
||||
{{ plugin.description }}
|
||||
</p>
|
||||
<span class="version">{{ $t("Version") }}: {{ plugin.version }} <a v-if="plugin.repo" :href="plugin.repo" target="_blank">Repo</a></span>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button v-if="status === 'installing'" class="btn btn-primary" disabled>{{ $t("installing") }}</button>
|
||||
<button v-else-if="status === 'uninstalling'" class="btn btn-danger" disabled>{{ $t("uninstalling") }}</button>
|
||||
<button v-else-if="plugin.installed || status === 'installed'" class="btn btn-danger" @click="deleteConfirm">{{ $t("uninstall") }}</button>
|
||||
<button v-else class="btn btn-primary" @click="install">{{ $t("install") }}</button>
|
||||
</div>
|
||||
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="uninstall">
|
||||
{{ $t("confirmUninstallPlugin") }}
|
||||
</Confirm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirm from "./Confirm.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
},
|
||||
props: {
|
||||
plugin: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
status: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Show confirmation for deleting a tag
|
||||
*/
|
||||
deleteConfirm() {
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
install() {
|
||||
this.status = "installing";
|
||||
|
||||
this.$root.getSocket().emit("installPlugin", this.plugin.repo, this.plugin.name, (res) => {
|
||||
if (res.ok) {
|
||||
this.status = "";
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.plugin.installed = true;
|
||||
} else {
|
||||
this.$root.toastRes(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
uninstall() {
|
||||
this.status = "uninstalling";
|
||||
|
||||
this.$root.getSocket().emit("uninstallPlugin", this.plugin.name, (res) => {
|
||||
if (res.ok) {
|
||||
this.status = "";
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.plugin.installed = false;
|
||||
} else {
|
||||
this.$root.toastRes(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.plugin-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
|
||||
.info {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 13px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -49,16 +49,15 @@
|
||||
{{ monitor.element.name }}
|
||||
</a>
|
||||
<p v-else class="item-name"> {{ monitor.element.name }} </p>
|
||||
|
||||
<span
|
||||
v-if="showLink(monitor, true)"
|
||||
title="Toggle Clickable Link"
|
||||
title="Setting"
|
||||
>
|
||||
<font-awesome-icon
|
||||
v-if="editMode"
|
||||
:class="{'link-active': monitor.element.sendUrl, 'btn-link': true}"
|
||||
icon="link" class="action me-3"
|
||||
|
||||
@click="toggleLink(group.index, monitor.index)"
|
||||
:class="{'link-active': true, 'btn-link': true}"
|
||||
icon="cog" class="action me-3"
|
||||
@click="$refs.monitorSettingDialog.show(group, monitor)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@ -77,9 +76,11 @@
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
<MonitorSettingDialog ref="monitorSettingDialog" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MonitorSettingDialog from "./MonitorSettingDialog.vue";
|
||||
import Draggable from "vuedraggable";
|
||||
import HeartbeatBar from "./HeartbeatBar.vue";
|
||||
import Uptime from "./Uptime.vue";
|
||||
@ -87,6 +88,7 @@ import Tag from "./Tag.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MonitorSettingDialog,
|
||||
Draggable,
|
||||
HeartbeatBar,
|
||||
Uptime,
|
||||
@ -135,15 +137,6 @@ export default {
|
||||
this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the value of sendUrl
|
||||
* @param {number} groupIndex Index of group monitor is member of
|
||||
* @param {number} index Index of monitor within group
|
||||
*/
|
||||
toggleLink(groupIndex, index) {
|
||||
this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl = !this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl;
|
||||
},
|
||||
|
||||
/**
|
||||
* Should a link to the monitor be shown?
|
||||
* Attempts to guess if a link should be shown based upon if
|
||||
|
@ -6,7 +6,7 @@
|
||||
'm-2': size == 'normal',
|
||||
'px-2': size == 'sm',
|
||||
'py-0': size == 'sm',
|
||||
'm-1': size == 'sm',
|
||||
'mx-1': size == 'sm',
|
||||
}"
|
||||
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }"
|
||||
>
|
||||
|
@ -76,11 +76,24 @@
|
||||
</button>
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-if="allMonitorList.length > 0" class="pt-3 px-3">
|
||||
<div v-if="allMonitorList.length > 0" class="pt-3">
|
||||
<label class="form-label">{{ $t("Add a monitor") }}:</label>
|
||||
<select v-model="selectedAddMonitor" class="form-control">
|
||||
<option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option>
|
||||
</select>
|
||||
<VueMultiselect
|
||||
v-model="selectedAddMonitor"
|
||||
:options="allMonitorList"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:placeholder="$t('Add a monitor')"
|
||||
label="name"
|
||||
trackBy="name"
|
||||
class="mt-1"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<div class="d-inline-flex">
|
||||
<span>{{ option.name }} <Tag v-for="monitorTag in option.tags" :key="monitorTag" :item="monitorTag" :size="'sm'" /></span>
|
||||
</div>
|
||||
</template>
|
||||
</VueMultiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -107,6 +120,7 @@
|
||||
<script>
|
||||
import { Modal } from "bootstrap";
|
||||
import Confirm from "./Confirm.vue";
|
||||
import Tag from "./Tag.vue";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import { colorOptions } from "../util-frontend";
|
||||
import { useToast } from "vue-toastification";
|
||||
@ -117,6 +131,7 @@ export default {
|
||||
components: {
|
||||
VueMultiselect,
|
||||
Confirm,
|
||||
Tag,
|
||||
},
|
||||
props: {
|
||||
updated: {
|
||||
|
@ -16,17 +16,29 @@
|
||||
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="ntfy-username" class="form-label">{{ $t("Username") }} ({{ $t("Optional") }})</label>
|
||||
<label for="authentication-method" class="form-label">{{ $t("ntfyAuthenticationMethod") }}</label>
|
||||
<select id="authentication-method" v-model="$parent.notification.ntfyAuthenticationMethod" class="form-select">
|
||||
<option v-for="(name, type) in authenticationMethods" :key="type" :value="type">{{ name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'usernamePassword'" class="mb-3">
|
||||
<label for="ntfy-username" class="form-label">{{ $t("Username") }}</label>
|
||||
<div class="input-group mb-3">
|
||||
<input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="ntfy-password" class="form-label">{{ $t("Password") }} ({{ $t("Optional") }})</label>
|
||||
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'usernamePassword'" class="mb-3">
|
||||
<label for="ntfy-password" class="form-label">{{ $t("Password") }}</label>
|
||||
<div class="input-group mb-3">
|
||||
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'accessToken'" class="mb-3">
|
||||
<label for="ntfy-access-token" class="form-label">{{ $t("Access Token") }}</label>
|
||||
<div class="input-group mb-3">
|
||||
<HiddenInput id="ntfy-access-token" v-model="$parent.notification.ntfyaccesstoken"></HiddenInput>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="ntfy-icon" class="form-label">{{ $t("IconUrl") }}</label>
|
||||
<input id="ntfy-icon" v-model="$parent.notification.ntfyIcon" type="text" class="form-control">
|
||||
@ -40,11 +52,29 @@ export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
computed: {
|
||||
authenticationMethods() {
|
||||
return {
|
||||
none: this.$t("None"),
|
||||
usernamePassword: this.$t("ntfyUsernameAndPassword"),
|
||||
accessToken: this.$t("Access Token")
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (typeof this.$parent.notification.ntfyPriority === "undefined") {
|
||||
this.$parent.notification.ntfyserverurl = "https://ntfy.sh";
|
||||
this.$parent.notification.ntfyPriority = 5;
|
||||
}
|
||||
|
||||
// Handling notifications that added before 1.22.0
|
||||
if (typeof this.$parent.notification.ntfyAuthenticationMethod === "undefined") {
|
||||
if (!this.$parent.notification.ntfyusername) {
|
||||
this.$parent.notification.ntfyAuthenticationMethod = "none";
|
||||
} else {
|
||||
this.$parent.notification.ntfyAuthenticationMethod = "usernamePassword";
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -42,6 +42,8 @@
|
||||
<option value="vibrate">{{ $t("pushoversounds vibrate") }}</option>
|
||||
<option value="none">{{ $t("pushoversounds none") }}</option>
|
||||
</select>
|
||||
<label for="pushover-ttl" class="form-label">{{ $t("pushoverMessageTtl") }}</label>
|
||||
<input id="pushover-ttl" v-model="$parent.notification.pushoverttl" type="number" min="0" step="1" class="form-control">
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||
|
@ -190,6 +190,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chrome Executable -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="primaryBaseURL">
|
||||
{{ $t("chromeExecutable") }}
|
||||
</label>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
id="primaryBaseURL"
|
||||
v-model="settings.chromeExecutable"
|
||||
class="form-control"
|
||||
name="primaryBaseURL"
|
||||
:placeholder="$t('chromeExecutableAutoDetect')"
|
||||
/>
|
||||
<button class="btn btn-outline-primary" type="button" @click="testChrome">
|
||||
{{ $t("Test") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
{{ $t("chromeExecutableDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
@ -241,6 +265,12 @@ export default {
|
||||
autoGetPrimaryBaseURL() {
|
||||
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
||||
},
|
||||
|
||||
testChrome() {
|
||||
this.$root.getSocket().emit("testChrome", this.settings.chromeExecutable, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -1,57 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mt-3">{{ remotePluginListMsg }}</div>
|
||||
<PluginItem v-for="plugin in remotePluginList" :key="plugin.id" :plugin="plugin" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PluginItem from "../PluginItem.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PluginItem
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
remotePluginList: [],
|
||||
remotePluginListMsg: "",
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
pluginList() {
|
||||
return this.$parent.$parent.$parent.pluginList;
|
||||
},
|
||||
settings() {
|
||||
return this.$parent.$parent.$parent.settings;
|
||||
},
|
||||
saveSettings() {
|
||||
return this.$parent.$parent.$parent.saveSettings;
|
||||
},
|
||||
settingsLoaded() {
|
||||
return this.$parent.$parent.$parent.settingsLoaded;
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.loadList();
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadList() {
|
||||
this.remotePluginListMsg = this.$t("Loading") + "...";
|
||||
|
||||
this.$root.getSocket().emit("getPluginList", (res) => {
|
||||
if (res.ok) {
|
||||
this.remotePluginList = res.pluginList;
|
||||
this.remotePluginListMsg = "";
|
||||
} else {
|
||||
this.remotePluginListMsg = this.$t("loadingError") + " " + res.msg;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,21 +1,18 @@
|
||||
<template>
|
||||
<div class="my-4">
|
||||
<div class="mx-4 pt-1 my-3">
|
||||
<div class="mx-0 mx-lg-4 pt-1 mb-4">
|
||||
<button class="btn btn-primary" @click.stop="addTag"><font-awesome-icon icon="plus" /> {{ $t("Add New Tag") }}</button>
|
||||
</div>
|
||||
|
||||
<div class="tags-list my-3">
|
||||
<div v-for="(tag, index) in tagsList" :key="tag.id" class="d-flex align-items-center mx-4 py-1 tags-list-row" :disabled="processing" @click="editTag(index)">
|
||||
<div class="col-5 ps-1">
|
||||
<div v-for="(tag, index) in tagsList" :key="tag.id" class="d-flex align-items-center mx-0 mx-lg-4 py-1 tags-list-row" :disabled="processing" @click="editTag(index)">
|
||||
<div class="col-10 col-sm-5">
|
||||
<Tag :item="tag" />
|
||||
</div>
|
||||
<div class="col-5 px-1">
|
||||
<div class="col-5 px-1 d-none d-sm-block">
|
||||
<div>{{ monitorsByTag(tag.id).length }} {{ $tc("Monitor", monitorsByTag(tag.id).length) }}</div>
|
||||
</div>
|
||||
<div class="col-2 pe-3 d-flex justify-content-end">
|
||||
<button type="button" class="btn ms-2 py-1">
|
||||
<font-awesome-icon class="" icon="edit" />
|
||||
</button>
|
||||
<div class="col-2 pe-2 pe-lg-3 d-flex justify-content-end">
|
||||
<button type="button" class="btn-rm-tag btn btn-outline-danger ms-2 py-1" :disabled="processing" @click.stop="deleteConfirm(index)">
|
||||
<font-awesome-icon class="" icon="trash" />
|
||||
</button>
|
||||
@ -156,8 +153,8 @@ export default {
|
||||
@import "../../assets/vars.scss";
|
||||
|
||||
.btn-rm-tag {
|
||||
padding-left: 11px;
|
||||
padding-right: 11px;
|
||||
padding-left: 9px;
|
||||
padding-right: 9px;
|
||||
}
|
||||
|
||||
.tags-list .tags-list-row {
|
||||
|
@ -49,6 +49,7 @@ import {
|
||||
faFilter,
|
||||
faInfoCircle,
|
||||
faClone,
|
||||
faCertificate,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(
|
||||
@ -95,6 +96,7 @@ library.add(
|
||||
faFilter,
|
||||
faInfoCircle,
|
||||
faClone,
|
||||
faCertificate,
|
||||
);
|
||||
|
||||
export { FontAwesomeIcon };
|
||||
|
@ -683,6 +683,6 @@
|
||||
"backupDescription2": "ملحوظة",
|
||||
"languageName": "العربية",
|
||||
"Game": "الألعاب",
|
||||
"List": "قائمة",
|
||||
"List": "القائمة",
|
||||
"statusMaintenance": "الصيانة"
|
||||
}
|
||||
|
@ -178,7 +178,7 @@
|
||||
"Degraded Service": "Всички услуги са недостъпни",
|
||||
"Add Group": "Добави група",
|
||||
"Add a monitor": "Добави монитор",
|
||||
"Edit Status Page": "Редактиране Статус страница",
|
||||
"Edit Status Page": "Редактиране на статус страницата",
|
||||
"Go to Dashboard": "Към Таблото",
|
||||
"telegram": "Telegram",
|
||||
"webhook": "Уеб кука",
|
||||
@ -200,7 +200,7 @@
|
||||
"mattermost": "Mattermost",
|
||||
"Status Page": "Статус страница",
|
||||
"Status Pages": "Статус страници",
|
||||
"Primary Base URL": "Основен базов URL адрес",
|
||||
"Primary Base URL": "Базов URL адрес",
|
||||
"Push URL": "Генериран Push URL адрес",
|
||||
"needPushEvery": "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди.",
|
||||
"pushOptionalParams": "Допълнителни, но не задължителни параметри: {0}",
|
||||
@ -591,7 +591,7 @@
|
||||
"All Status Pages": "Всички статус страници",
|
||||
"Select status pages...": "Изберете статус страници…",
|
||||
"recurringIntervalMessage": "Изпълнявай ежедневно | Изпълнявай всеки {0} дни",
|
||||
"affectedMonitorsDescription": "Изберете монитори, засегнати от текущата поддръжка",
|
||||
"affectedMonitorsDescription": "Изберете монитори, попадащи в обсега на текущата поддръжка",
|
||||
"affectedStatusPages": "Покажи това съобщение за поддръжка на избрани статус страници",
|
||||
"atLeastOneMonitor": "Изберете поне един засегнат монитор",
|
||||
"deleteMaintenanceMsg": "Сигурни ли сте, че желаете да изтриете тази поддръжка?",
|
||||
@ -652,7 +652,7 @@
|
||||
"dnsCacheDescription": "Възможно е да не работи в IPv6 среда - деактивирайте, ако срещнете проблеми.",
|
||||
"Single Maintenance Window": "Единичен времеви интервал за поддръжка",
|
||||
"Maintenance Time Window of a Day": "Времеви интервал от деня за поддръжка",
|
||||
"Effective Date Range": "Интервал от дни на влизане в сила",
|
||||
"Effective Date Range": "Ефективен интервал от дни (по желание)",
|
||||
"Schedule Maintenance": "Планирай поддръжка",
|
||||
"Date and Time": "Дата и час",
|
||||
"DateTime Range": "Изтрий времеви интервал",
|
||||
@ -707,7 +707,7 @@
|
||||
"telegramSendSilently": "Изпрати тихо",
|
||||
"Clone Monitor": "Клониране на монитор",
|
||||
"Clone": "Клонирай",
|
||||
"cloneOf": "Клонинг на {0}",
|
||||
"cloneOf": "Клониран {0}",
|
||||
"Expiry": "Валиден до",
|
||||
"Expiry date": "Дата на изтичане",
|
||||
"Add Another": "Добави друг",
|
||||
@ -738,5 +738,51 @@
|
||||
"Add New Tag": "Добави нов етикет",
|
||||
"lunaseaTarget": "Цел",
|
||||
"lunaseaDeviceID": "ID на устройството",
|
||||
"lunaseaUserID": "ID на потребител"
|
||||
"lunaseaUserID": "ID на потребител",
|
||||
"twilioAccountSID": "Профил SID",
|
||||
"twilioAuthToken": "Удостоверяващ токен",
|
||||
"twilioFromNumber": "От номер",
|
||||
"twilioToNumber": "Към номер",
|
||||
"sameAsServerTimezone": "Kато часовата зона на сървъра",
|
||||
"startDateTime": "Старт Дата/Час",
|
||||
"endDateTime": "Край Дата/Час",
|
||||
"cronSchedule": "График: ",
|
||||
"invalidCronExpression": "Невалиден \"Cron\" израз: {0}",
|
||||
"cronExpression": "Израз тип \"Cron\"",
|
||||
"statusPageRefreshIn": "Обновяване след: {0}",
|
||||
"ntfyUsernameAndPassword": "Потребителско име и парола",
|
||||
"ntfyAuthenticationMethod": "Метод за удостоверяване",
|
||||
"pushoverMessageTtl": "TTL на съобщението (секунди)",
|
||||
"Open Badge Generator": "Отвори генератора на баджове",
|
||||
"Badge Generator": "Генератор на баджове на {0}",
|
||||
"Badge Type": "Тип бадж",
|
||||
"Badge Duration": "Продължителност на баджа",
|
||||
"Badge Prefix": "Префикс на баджа",
|
||||
"Badge Label Color": "Цвят на етикета на баджа",
|
||||
"Badge Color": "Цвят на баджа",
|
||||
"Badge Label Suffix": "Суфикс на етикета на значката",
|
||||
"Badge Up Color": "Цвят на баджа за достъпен",
|
||||
"Badge Down Color": "Цвят на баджа за недостъпен",
|
||||
"Badge Maintenance Color": "Цвят на баджа за поддръжка",
|
||||
"Badge Warn Color": "Цвят на баджа за предупреждение",
|
||||
"Badge Warn Days": "Дни за показване на баджа",
|
||||
"Badge Style": "Стил на баджа",
|
||||
"Badge value (For Testing only.)": "Стойност на баджа (само за тест.)",
|
||||
"Badge URL": "URL адрес на баджа",
|
||||
"Monitor Setting": "Настройка на монитор {0}",
|
||||
"Show Clickable Link": "Покажи връзка, която може да се кликне",
|
||||
"Show Clickable Link Description": "Ако е отбелязано, всеки който има достъп до тази статус страница, ще може да достъпва URL адреса на монитора.",
|
||||
"Badge Label": "Етикет на баджа",
|
||||
"Badge Suffix": "Суфикс на баджа",
|
||||
"Badge Label Prefix": "Префикс на етикета на значката",
|
||||
"Badge Pending Color": "Цвят на баджа за изчакващ",
|
||||
"Badge Down Days": "Колко дни баджът да не се показва",
|
||||
"Group": "Група",
|
||||
"Monitor Group": "Монитор група",
|
||||
"Cannot connect to the socket server": "Не може да се свърже със сокет сървъра",
|
||||
"Reconnecting...": "Повторно свързване...",
|
||||
"Edit Maintenance": "Редактиране на поддръжка",
|
||||
"Home": "Главна страница",
|
||||
"noGroupMonitorMsg": "Не е налично. Първо създайте групов монитор.",
|
||||
"Close": "Затвори"
|
||||
}
|
||||
|
28
src/lang/ca.json
Normal file
28
src/lang/ca.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"Settings": "Paràmetres",
|
||||
"Dashboard": "Tauler",
|
||||
"Help": "Ajuda",
|
||||
"New Update": "Nova actualització",
|
||||
"Language": "Idioma",
|
||||
"Appearance": "Aparença",
|
||||
"Theme": "Tema",
|
||||
"General": "General",
|
||||
"Game": "Joc",
|
||||
"Version": "Versió",
|
||||
"Check Update On GitHub": "Comprovar actualitzacions a GitHub",
|
||||
"List": "Llista",
|
||||
"Home": "Inici",
|
||||
"Add": "Afegir",
|
||||
"Add New Monitor": "Afegir nou monitor",
|
||||
"Quick Stats": "Estadístiques ràpides",
|
||||
"Up": "Funcional",
|
||||
"Down": "Caigut",
|
||||
"Pending": "Pendent",
|
||||
"Maintenance": "Manteniment",
|
||||
"Unknown": "Desconegut",
|
||||
"Cannot connect to the socket server": "No es pot connectar al servidor socket",
|
||||
"Reconnecting...": "S'està tornant a connectar...",
|
||||
"languageName": "Català",
|
||||
"Primary Base URL": "URL Base Primària",
|
||||
"statusMaintenance": "Manteniment"
|
||||
}
|
46
src/lang/ckb.json
Normal file
46
src/lang/ckb.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"languageName": "کوردی",
|
||||
"Settings": "ڕێکخستنەکان",
|
||||
"Help": "یارمەتی",
|
||||
"New Update": "وەشانی نوێ",
|
||||
"Language": "زمان",
|
||||
"Appearance": "ڕووکار",
|
||||
"Theme": "شێوەی ڕووکار",
|
||||
"General": "گشتی",
|
||||
"Game": "یاری",
|
||||
"Version": "وەشان",
|
||||
"Check Update On GitHub": "سەیری وەشانی نوێ بکە لە Github",
|
||||
"List": "لیست",
|
||||
"Add": "زیادکردن",
|
||||
"Quick Stats": "ئاماری خێرا",
|
||||
"Up": "سەروو",
|
||||
"Down": "خواروو",
|
||||
"Pending": "هەڵپەسێردراو",
|
||||
"statusMaintenance": "چاکردنەوە",
|
||||
"Maintenance": "چاکردنەوە",
|
||||
"Unknown": "نەزانراو",
|
||||
"Passive Monitor Type": "جۆری مۆنیتەری پاسیڤ",
|
||||
"Specific Monitor Type": "جۆری مۆنیتەری تایبەت",
|
||||
"markdownSupported": "ڕستەسازی مارکداون پشتگیری دەکرێت",
|
||||
"pauseDashboardHome": "وچان",
|
||||
"Pause": "وچان",
|
||||
"Name": "ناو",
|
||||
"Status": "دۆخ",
|
||||
"Message": "پەیام",
|
||||
"No important events": "هیچ ڕووداوێکی گرنگ نییە",
|
||||
"Resume": "دەستپێکردنەوە",
|
||||
"Edit": "بژارکردن",
|
||||
"Delete": "سڕینەوە",
|
||||
"Uptime": "کاتی کارکردن",
|
||||
"Cert Exp.": "بەسەرچوونی بڕوانامەی SSL.",
|
||||
"day": "ڕۆژ | ڕۆژەکان",
|
||||
"-day": "-ڕۆژ",
|
||||
"hour": "کاتژمێر",
|
||||
"Dashboard": "داشبۆرد",
|
||||
"Primary Base URL": "بەستەری بنچینەیی سەرەکی",
|
||||
"Add New Monitor": "مۆنیتەرێکی نوێ زیاد بکە",
|
||||
"General Monitor Type": "جۆری مۆنیتەری گشتی",
|
||||
"DateTime": "رێکەوت",
|
||||
"Current": "هەنووکە",
|
||||
"Monitor": "مۆنیتەر | مۆنیتەرەکان"
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"languageName": "Czech",
|
||||
"languageName": "Čeština",
|
||||
"checkEverySecond": "Kontrolovat každých {0} sekund",
|
||||
"retryCheckEverySecond": "Opakovat každých {0} sekund",
|
||||
"resendEveryXTimes": "Znovu zaslat {0}krát",
|
||||
@ -134,7 +134,7 @@
|
||||
"Remember me": "Zapamatovat si mě",
|
||||
"Login": "Přihlášení",
|
||||
"No Monitors, please": "Žádné dohledy, prosím",
|
||||
"add one": "přidat jeden",
|
||||
"add one": "začněte přidáním nového",
|
||||
"Notification Type": "Typ oznámení",
|
||||
"Email": "E-mail",
|
||||
"Test": "Test",
|
||||
@ -518,7 +518,7 @@
|
||||
"PushDeer Key": "PushDeer klíč",
|
||||
"Footer Text": "Text v patičce",
|
||||
"Show Powered By": "Zobrazit \"Poskytuje\"",
|
||||
"Domain Names": "Názvy domén",
|
||||
"Domain Names": "Doménová jména",
|
||||
"signedInDisp": "Přihlášen jako {0}",
|
||||
"signedInDispDisabled": "Ověření je vypnuté.",
|
||||
"RadiusSecret": "Tajemství Radius",
|
||||
@ -546,7 +546,7 @@
|
||||
"pushoversounds cashregister": "Pokladna",
|
||||
"pushoversounds classical": "Classical",
|
||||
"pushoversounds cosmic": "Kosmický",
|
||||
"pushoversounds falling": "Falling",
|
||||
"pushoversounds falling": "Padající",
|
||||
"pushoversounds gamelan": "Gamelan",
|
||||
"pushoversounds incoming": "Příchozí",
|
||||
"pushoversounds intermission": "Přestávka",
|
||||
@ -669,7 +669,7 @@
|
||||
"Free Mobile User Identifier": "Identifikátor uživatele Free Mobile",
|
||||
"Free Mobile API Key": "API klíč Free Mobile",
|
||||
"Enable TLS": "Povolit TLS",
|
||||
"Proto Service Name": "Proto Service Name",
|
||||
"Proto Service Name": "Jméno Proto Service",
|
||||
"Proto Method": "Proto metoda",
|
||||
"Proto Content": "Proto obsah",
|
||||
"Economy": "Úsporná",
|
||||
@ -705,9 +705,9 @@
|
||||
"telegramProtectContent": "Ochrana přeposílání/ukládání",
|
||||
"telegramSendSilently": "Odeslat potichu",
|
||||
"telegramSendSilentlyDescription": "Zprávu odešle tiše. Uživatelé obdrží oznámení bez zvuku.",
|
||||
"Clone": "Klonovat",
|
||||
"cloneOf": "Klonovat {0}",
|
||||
"Clone Monitor": "Klonovat dohled",
|
||||
"Clone": "Duplikovat",
|
||||
"cloneOf": "Kopie {0}",
|
||||
"Clone Monitor": "Duplikovat dohled",
|
||||
"API Keys": "API klíče",
|
||||
"Expiry": "Platnost",
|
||||
"Don't expire": "Nevyprší",
|
||||
@ -749,5 +749,40 @@
|
||||
"cronSchedule": "Plán: ",
|
||||
"invalidCronExpression": "Neplatný cron výraz: {0}",
|
||||
"startDateTime": "Počáteční datum/čas",
|
||||
"endDateTime": "Datum/čas konce"
|
||||
"endDateTime": "Datum/čas konce",
|
||||
"ntfyAuthenticationMethod": "Způsob ověření",
|
||||
"ntfyUsernameAndPassword": "Uživatelské jméno a heslo",
|
||||
"pushoverMessageTtl": "Zpráva TTL (Sekund)",
|
||||
"Show Clickable Link": "Zobrazit klikatelný odkaz",
|
||||
"Show Clickable Link Description": "Pokud je zaškrtnuto, všichni, kdo mají přístup k této stavové stránce, mají přístup k adrese URL monitoru.",
|
||||
"Open Badge Generator": "Otevřít generátor odznaků",
|
||||
"Badge Type": "Typ odznaku",
|
||||
"Badge Duration": "Platnost odznaku",
|
||||
"Badge Label": "Štítek odznaku",
|
||||
"Badge Prefix": "Prefix odznaku",
|
||||
"Monitor Setting": "{0}'s Nastavení dohledu",
|
||||
"Badge Generator": "Generátor odznaků pro {0}",
|
||||
"Badge Label Color": "Barva štítku odznaku",
|
||||
"Badge Color": "Barva odznaku",
|
||||
"Badge Style": "Styl odznaku",
|
||||
"Badge Label Suffix": "Přípona štítku odznaku",
|
||||
"Badge URL": "URL odznaku",
|
||||
"Badge Suffix": "Přípona odznaku",
|
||||
"Badge Label Prefix": "Prefix štítku odznaku",
|
||||
"Badge Up Color": "Barva odznaku při Běží",
|
||||
"Badge Down Color": "Barva odznaku při Nedostupné",
|
||||
"Badge Pending Color": "Barva odznaku při Pauze",
|
||||
"Badge Maintenance Color": "Barva odznaku při Údržbě",
|
||||
"Badge Warn Color": "Barva odznaku při Upozornění",
|
||||
"Reconnecting...": "Obnovení spojení...",
|
||||
"Cannot connect to the socket server": "Nelze se připojit k soketovému serveru",
|
||||
"Edit Maintenance": "Upravit Údržbu",
|
||||
"Home": "Hlavní stránka",
|
||||
"Badge Down Days": "Odznak nedostupných dní",
|
||||
"Group": "Skupina",
|
||||
"Monitor Group": "Sledovaná skupina",
|
||||
"noGroupMonitorMsg": "Není k dispozici. Nejprve vytvořte skupin dohledů.",
|
||||
"Close": "Zavřít",
|
||||
"Badge value (For Testing only.)": "Hodnota odznaku (pouze pro testování)",
|
||||
"Badge Warn Days": "Odznak dní s upozorněním"
|
||||
}
|
||||
|
@ -37,7 +37,7 @@
|
||||
"checkEverySecond": "Tjek hvert {0} sekund",
|
||||
"Response": "Respons",
|
||||
"Ping": "Ping",
|
||||
"Monitor Type": "Overvåger Type",
|
||||
"Monitor Type": "Overvåger type",
|
||||
"Keyword": "Nøgleord",
|
||||
"Friendly Name": "Visningsnavn",
|
||||
"URL": "URL",
|
||||
@ -144,7 +144,7 @@
|
||||
"retryCheckEverySecond": "Prøv igen hvert {0} sekund.",
|
||||
"importHandleDescription": "Vælg 'Spring over eksisterende', hvis du vil springe over hver overvåger eller underretning med samme navn. 'Overskriv' sletter alle eksisterende overvågere og underretninger.",
|
||||
"confirmImportMsg": "Er du sikker på at importere sikkerhedskopien? Sørg for, at du har valgt den rigtige importindstilling.",
|
||||
"Heartbeat Retry Interval": "Hjerteslag Gentagelsesinterval",
|
||||
"Heartbeat Retry Interval": "Hjerteslag gentagelsesinterval",
|
||||
"Import Backup": "Importer Backup",
|
||||
"Export Backup": "Eksporter Backup",
|
||||
"Skip existing": "Spring over eksisterende",
|
||||
@ -166,14 +166,14 @@
|
||||
"Purple": "Lilla",
|
||||
"Pink": "Pink",
|
||||
"Search...": "Søg…",
|
||||
"Avg. Ping": "Gns. Ping",
|
||||
"Avg. Response": "Gns. Respons",
|
||||
"Avg. Ping": "Gns. ping",
|
||||
"Avg. Response": "Gns. respons",
|
||||
"Entry Page": "Entry Side",
|
||||
"statusPageNothing": "Intet her, tilføj venligst en Gruppe eller en Overvåger.",
|
||||
"No Services": "Ingen Tjenester",
|
||||
"All Systems Operational": "Alle Systemer i Drift",
|
||||
"Partially Degraded Service": "Delvist Forringet Service",
|
||||
"Degraded Service": "Forringet Service",
|
||||
"Partially Degraded Service": "Delvist forringet service",
|
||||
"Degraded Service": "Forringet service",
|
||||
"Add Group": "Tilføj Gruppe",
|
||||
"Add a monitor": "Tilføj en Overvåger",
|
||||
"Edit Status Page": "Rediger Statusside",
|
||||
@ -314,7 +314,7 @@
|
||||
"Steam API Key": "Steam API-nøgle",
|
||||
"Shrink Database": "Krymp Database",
|
||||
"Pick a RR-Type...": "Vælg en RR-Type…",
|
||||
"Pick Accepted Status Codes...": "Vælg Accepterede Statuskoder...",
|
||||
"Pick Accepted Status Codes...": "Vælg accepterede statuskoder…",
|
||||
"Default": "Standard",
|
||||
"HTTP Options": "HTTP Valgmuligheder",
|
||||
"Create Incident": "Opret Annoncering",
|
||||
@ -447,7 +447,7 @@
|
||||
"Docker Hosts": "Docker Hosts",
|
||||
"loadingError": "Kan ikke hente dataene, prøv igen senere.",
|
||||
"Custom": "Brugerdefineret",
|
||||
"Monitor": "Monitor | Monitors",
|
||||
"Monitor": "Overvåger | Overvågere",
|
||||
"Specific Monitor Type": "Specifik monitor-type",
|
||||
"topic": "Emne",
|
||||
"Fingerprint:": "Fingerprint:",
|
||||
@ -580,5 +580,7 @@
|
||||
"Expiry date": "Udløbsdato",
|
||||
"Expires": "Udløber",
|
||||
"deleteAPIKeyMsg": "Er du sikker på du vil slette denne API nøgle?",
|
||||
"pagertreeDoNothing": "Gør intet"
|
||||
"pagertreeDoNothing": "Gør intet",
|
||||
"Start of maintenance": "Start på vedligeholdelse",
|
||||
"Add New Tag": "Tilføj nyt tag"
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
"Version": "Version",
|
||||
"Check Update On GitHub": "Auf GitHub nach Updates suchen",
|
||||
"List": "Liste",
|
||||
"Home": "Home",
|
||||
"Add": "Hinzufügen",
|
||||
"Add New Monitor": "Neuen Monitor hinzufügen",
|
||||
"Quick Stats": "Übersicht",
|
||||
@ -17,6 +18,8 @@
|
||||
"Down": "Inaktiv",
|
||||
"Pending": "Ausstehend",
|
||||
"Unknown": "Unbekannt",
|
||||
"Cannot connect to the socket server": "Es kann keine Verbindung zum Socket-Server hergestellt werden",
|
||||
"Reconnecting...": "Die Verbindung wird wiederhergestellt...",
|
||||
"Pause": "Pausieren",
|
||||
"pauseDashboardHome": "Pausiert",
|
||||
"Name": "Name",
|
||||
@ -259,6 +262,7 @@
|
||||
"More info on:": "Mehr Infos auf: {0}",
|
||||
"pushoverDesc1": "Notfallpriorität (2) hat standardmässig 30 Sekunden Auszeit zwischen den Versuchen und läuft nach 1 Stunde ab.",
|
||||
"pushoverDesc2": "Fülle das Geräte Feld aus, wenn du Benachrichtigungen an verschiedene Geräte senden möchtest.",
|
||||
"pushoverMessageTtl": "Message TTL (Sekunden)",
|
||||
"SMS Type": "SMS Typ",
|
||||
"octopushTypePremium": "Premium (Schnell - zur Benachrichtigung empfohlen)",
|
||||
"octopushTypeLowCost": "Kostengünstig (Langsam - manchmal vom Betreiber gesperrt)",
|
||||
@ -736,6 +740,8 @@
|
||||
"lunaseaTarget": "Ziel",
|
||||
"lunaseaDeviceID": "Geräte-ID",
|
||||
"lunaseaUserID": "Benutzer-ID",
|
||||
"ntfyAuthenticationMethod": "Authentifizierungsmethode",
|
||||
"ntfyUsernameAndPassword": "Benutzername und Passwort",
|
||||
"twilioAccountSID": "Account SID",
|
||||
"twilioFromNumber": "Absender",
|
||||
"twilioToNumber": "Empfänger",
|
||||
@ -746,5 +752,34 @@
|
||||
"endDateTime": "Ende Datum/Uhrzeit",
|
||||
"cronExpression": "Cron-Ausdruck",
|
||||
"cronSchedule": "Zeitplan: ",
|
||||
"invalidCronExpression": "Ungültiger Cron-Ausdruck: {0}"
|
||||
"invalidCronExpression": "Ungültiger Cron-Ausdruck: {0}",
|
||||
"Open Badge Generator": "Open Badge Generator",
|
||||
"Badge Generator": "{0}'s Badge Generator",
|
||||
"Badge Type": "Badge Typ",
|
||||
"Badge Duration": "Badge Dauer",
|
||||
"Badge Label": "Badge Label",
|
||||
"Badge Prefix": "Badge Präfix",
|
||||
"Badge Suffix": "Badge Suffix",
|
||||
"Badge Label Color": "Badge Label Farbe",
|
||||
"Badge Color": "Badge Farbe",
|
||||
"Badge Label Prefix": "Badge Label Präfix",
|
||||
"Badge Up Color": "Badge Up Farbe",
|
||||
"Badge Maintenance Color": "Badge Wartung Farbe",
|
||||
"Badge Warn Color": "Badge Warnung Farbe",
|
||||
"Badge Warn Days": "Badge Warnung Tage",
|
||||
"Badge Style": "Badge Stil",
|
||||
"Badge URL": "Badge URL",
|
||||
"Badge Pending Color": "Badge Pending Farbe",
|
||||
"Badge Down Days": "Badge Down Tage",
|
||||
"Monitor Setting": "{0}'s Monitor Einstellung",
|
||||
"Show Clickable Link": "Klickbaren Link anzeigen",
|
||||
"Badge Label Suffix": "Badge Label Suffix",
|
||||
"Badge value (For Testing only.)": "Badge Wert (nur für Tests)",
|
||||
"Show Clickable Link Description": "Wenn diese Option aktiviert ist, kann jeder, der Zugriff auf diese Statusseite hat, auf die Monitor URL zugreifen.",
|
||||
"Badge Down Color": "Badge Down Farbe",
|
||||
"Edit Maintenance": "Wartung bearbeiten",
|
||||
"Group": "Gruppe",
|
||||
"Monitor Group": "Monitor Gruppe",
|
||||
"noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
|
||||
"Close": "Schliessen"
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
"Version": "Version",
|
||||
"Check Update On GitHub": "Auf GitHub nach Updates suchen",
|
||||
"List": "Liste",
|
||||
"Home": "Home",
|
||||
"Add": "Hinzufügen",
|
||||
"Add New Monitor": "Neuen Monitor hinzufügen",
|
||||
"Quick Stats": "Übersicht",
|
||||
@ -17,6 +18,8 @@
|
||||
"Down": "Inaktiv",
|
||||
"Pending": "Ausstehend",
|
||||
"Unknown": "Unbekannt",
|
||||
"Cannot connect to the socket server": "Es kann keine Verbindung zum Socket-Server hergestellt werden",
|
||||
"Reconnecting...": "Die Verbindung wird wiederhergestellt...",
|
||||
"Pause": "Pausieren",
|
||||
"pauseDashboardHome": "Pausiert",
|
||||
"Name": "Name",
|
||||
@ -259,6 +262,7 @@
|
||||
"More info on:": "Mehr Infos auf: {0}",
|
||||
"pushoverDesc1": "Notfallpriorität (2) hat standardmäßig 30 Sekunden Auszeit zwischen den Versuchen und läuft nach 1 Stunde ab.",
|
||||
"pushoverDesc2": "Fülle das Geräte Feld aus, wenn du Benachrichtigungen an verschiedene Geräte senden möchtest.",
|
||||
"pushoverMessageTtl": "Message TTL (Sekunden)",
|
||||
"SMS Type": "SMS Typ",
|
||||
"octopushTypePremium": "Premium (Schnell - zur Benachrichtigung empfohlen)",
|
||||
"octopushTypeLowCost": "Kostengünstig (Langsam - manchmal vom Betreiber gesperrt)",
|
||||
@ -626,6 +630,7 @@
|
||||
"lastDay4": "4. letzter Tag im Monat",
|
||||
"No Maintenance": "Keine Wartung",
|
||||
"Schedule Maintenance": "Wartung planen",
|
||||
"Edit Maintenance": "Wartung bearbeiten",
|
||||
"pauseMaintenanceMsg": "Möchtest du wirklich pausieren?",
|
||||
"maintenanceStatus-under-maintenance": "Unter Wartung",
|
||||
"maintenanceStatus-inactive": "Inaktiv",
|
||||
@ -739,6 +744,8 @@
|
||||
"lunaseaDeviceID": "Geräte-ID",
|
||||
"lunaseaTarget": "Ziel",
|
||||
"lunaseaUserID": "Benutzer-ID",
|
||||
"ntfyAuthenticationMethod": "Authentifizierungsmethode",
|
||||
"ntfyUsernameAndPassword": "Benutzername und Passwort",
|
||||
"twilioAccountSID": "Account SID",
|
||||
"twilioFromNumber": "Absender",
|
||||
"twilioToNumber": "Empfänger",
|
||||
@ -749,5 +756,33 @@
|
||||
"endDateTime": "Ende Datum/Uhrzeit",
|
||||
"cronExpression": "Cron-Ausdruck",
|
||||
"cronSchedule": "Zeitplan: ",
|
||||
"invalidCronExpression": "Ungültiger Cron-Ausdruck: {0}"
|
||||
"invalidCronExpression": "Ungültiger Cron-Ausdruck: {0}",
|
||||
"Show Clickable Link": "Klickbaren Link anzeigen",
|
||||
"Open Badge Generator": "Open Badge Generator",
|
||||
"Badge Generator": "{0}'s Badge Generator",
|
||||
"Badge Type": "Badge Typ",
|
||||
"Badge Duration": "Badge Dauer",
|
||||
"Badge Label": "Badge Label",
|
||||
"Show Clickable Link Description": "Wenn diese Option aktiviert ist, kann jeder, der Zugriff auf diese Statusseite hat, auf die Monitor-URL zugreifen.",
|
||||
"Badge Label Color": "Badge Label Farbe",
|
||||
"Badge Color": "Badge Farbe",
|
||||
"Badge Label Prefix": "Badge Label Präfix",
|
||||
"Badge Label Suffix": "Badge Label Suffix",
|
||||
"Badge Maintenance Color": "Badge Wartung Farbe",
|
||||
"Badge Warn Color": "Badge Warnung Farbe",
|
||||
"Badge Style": "Badge Stil",
|
||||
"Badge value (For Testing only.)": "Badge Wert (nur für Tests)",
|
||||
"Badge URL": "Badge URL",
|
||||
"Badge Up Color": "Badge Up Farbe",
|
||||
"Badge Down Color": "Badge Down Farbe",
|
||||
"Badge Pending Color": "Badge Pending Farbe",
|
||||
"Badge Down Days": "Badge Down Tage",
|
||||
"Monitor Setting": "{0}'s Monitor Einstellung",
|
||||
"Badge Prefix": "Badge Präfix",
|
||||
"Badge Suffix": "Badge Suffix",
|
||||
"Badge Warn Days": "Badge Warnung Tage",
|
||||
"Group": "Gruppe",
|
||||
"Monitor Group": "Monitor Gruppe",
|
||||
"noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
|
||||
"Close": "Schließen"
|
||||
}
|
||||
|
@ -695,5 +695,7 @@
|
||||
"Learn More": "Μάθετε περισσότερα",
|
||||
"Free Mobile User Identifier": "Free Mobile User Identifier",
|
||||
"Free Mobile API Key": "Free Mobile API Key",
|
||||
"smseaglePriority": "Προτεραιότητα μηνύματος (0-9, προεπιλογή = 0)"
|
||||
"smseaglePriority": "Προτεραιότητα μηνύματος (0-9, προεπιλογή = 0)",
|
||||
"statusPageRefreshIn": "Ανανέωση σε {0}",
|
||||
"Add New Tag": "Πρόσθεσε νέα ετικέτα"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"Version": "Version",
|
||||
"Check Update On GitHub": "Check Update On GitHub",
|
||||
"List": "List",
|
||||
"Home": "Home",
|
||||
"Add": "Add",
|
||||
"Add New Monitor": "Add New Monitor",
|
||||
"Quick Stats": "Quick Stats",
|
||||
@ -22,6 +23,8 @@
|
||||
"statusMaintenance": "Maintenance",
|
||||
"Maintenance": "Maintenance",
|
||||
"Unknown": "Unknown",
|
||||
"Cannot connect to the socket server": "Cannot connect to the socket server",
|
||||
"Reconnecting...": "Reconnecting...",
|
||||
"General Monitor Type": "General Monitor Type",
|
||||
"Passive Monitor Type": "Passive Monitor Type",
|
||||
"Specific Monitor Type": "Specific Monitor Type",
|
||||
@ -433,11 +436,15 @@
|
||||
"Enable DNS Cache": "Enable DNS Cache",
|
||||
"Enable": "Enable",
|
||||
"Disable": "Disable",
|
||||
"chromeExecutable": "Chrome/Chromium Executable",
|
||||
"chromeExecutableAutoDetect": "Auto Detect",
|
||||
"chromeExecutableDescription": "For Docker users, if Chromium is not yet installed, it may take a few minutes to install and display the test result. It takes 1GB of disk space.",
|
||||
"dnsCacheDescription": "It may be not working in some IPv6 environments, disable it if you encounter any issues.",
|
||||
"Single Maintenance Window": "Single Maintenance Window",
|
||||
"Maintenance Time Window of a Day": "Maintenance Time Window of a Day",
|
||||
"Effective Date Range": "Effective Date Range (Optional)",
|
||||
"Schedule Maintenance": "Schedule Maintenance",
|
||||
"Edit Maintenance": "Edit Maintenance",
|
||||
"Date and Time": "Date and Time",
|
||||
"DateTime Range": "DateTime Range",
|
||||
"loadingError": "Cannot fetch the data, please try again later.",
|
||||
@ -558,6 +565,7 @@
|
||||
"More info on:": "More info on: {0}",
|
||||
"pushoverDesc1": "Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.",
|
||||
"pushoverDesc2": "If you want to send notifications to different devices, fill out Device field.",
|
||||
"pushoverMessageTtl": "Message TTL (Seconds)",
|
||||
"SMS Type": "SMS Type",
|
||||
"octopushTypePremium": "Premium (Fast - recommended for alerting)",
|
||||
"octopushTypeLowCost": "Low Cost (Slow - sometimes blocked by operator)",
|
||||
@ -716,8 +724,38 @@
|
||||
"lunaseaTarget": "Target",
|
||||
"lunaseaDeviceID": "Device ID",
|
||||
"lunaseaUserID": "User ID",
|
||||
"ntfyAuthenticationMethod": "Authentication Method",
|
||||
"ntfyUsernameAndPassword": "Username and Password",
|
||||
"twilioAccountSID": "Account SID",
|
||||
"twilioAuthToken": "Auth Token",
|
||||
"twilioFromNumber": "From Number",
|
||||
"twilioToNumber": "To Number"
|
||||
"twilioToNumber": "To Number",
|
||||
"Monitor Setting": "{0}'s Monitor Setting",
|
||||
"Show Clickable Link": "Show Clickable Link",
|
||||
"Show Clickable Link Description": "If checked everyone who have access to this status page can have access to monitor URL.",
|
||||
"Open Badge Generator": "Open Badge Generator",
|
||||
"Badge Generator": "{0}'s Badge Generator",
|
||||
"Badge Type": "Badge Type",
|
||||
"Badge Duration": "Badge Duration",
|
||||
"Badge Label": "Badge Label",
|
||||
"Badge Prefix": "Badge Prefix",
|
||||
"Badge Suffix": "Badge Suffix",
|
||||
"Badge Label Color": "Badge Label Color",
|
||||
"Badge Color": "Badge Color",
|
||||
"Badge Label Prefix": "Badge Label Prefix",
|
||||
"Badge Label Suffix": "Badge Label Suffix",
|
||||
"Badge Up Color": "Badge Up Color",
|
||||
"Badge Down Color": "Badge Down Color",
|
||||
"Badge Pending Color": "Badge Pending Color",
|
||||
"Badge Maintenance Color": "Badge Maintenance Color",
|
||||
"Badge Warn Color": "Badge Warn Color",
|
||||
"Badge Warn Days": "Badge Warn Days",
|
||||
"Badge Down Days": "Badge Down Days",
|
||||
"Badge Style": "Badge Style",
|
||||
"Badge value (For Testing only.)": "Badge value (For Testing only.)",
|
||||
"Badge URL": "Badge URL",
|
||||
"Group": "Group",
|
||||
"Monitor Group": "Monitor Group",
|
||||
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
|
||||
"Close": "Close"
|
||||
}
|
||||
|
@ -748,5 +748,10 @@
|
||||
"cronExpression": "Expresión Cron",
|
||||
"cronSchedule": "Cronograma: ",
|
||||
"invalidCronExpression": "Expresión Cron invalida:{0}",
|
||||
"statusPageRefreshIn": "Reinicio en: {0}"
|
||||
"statusPageRefreshIn": "Reinicio en: {0}",
|
||||
"twilioAuthToken": "Token de Autentificación",
|
||||
"ntfyUsernameAndPassword": "Nombre de Usuario y Contraseña",
|
||||
"ntfyAuthenticationMethod": "Método de Autentificación",
|
||||
"Cannot connect to the socket server": "No se puede conectar al servidor socket",
|
||||
"Reconnecting...": "Reconectando..."
|
||||
}
|
||||
|
@ -74,7 +74,7 @@
|
||||
"Heartbeat Retry Interval": "Pultsu errepikatze interbaloak",
|
||||
"Advanced": "Aurreratua",
|
||||
"Upside Down Mode": "Alderantzizkako modua",
|
||||
"Max. Redirects": "Berbideratze max.",
|
||||
"Max. Redirects": "Birbideratze max.",
|
||||
"Accepted Status Codes": "Onartutako egoera kodeak",
|
||||
"Push URL": "Push URLa",
|
||||
"needPushEvery": "URL hau {0} segunduro deitu beharko zenuke.",
|
||||
@ -159,7 +159,7 @@
|
||||
"Token": "Tokena",
|
||||
"Show URI": "Erakutsi URIa",
|
||||
"Tags": "Etiketak",
|
||||
"Add New below or Select...": "Gehitu beste bat behean edo hautatu...",
|
||||
"Add New below or Select...": "Gehitu beste bat behean edo hautatu…",
|
||||
"Tag with this name already exist.": "Izen hau duen etiketa dagoeneko badago.",
|
||||
"Tag with this value already exist.": "Balio hau duen etiketa dagoeneko badago.",
|
||||
"color": "kolorea",
|
||||
@ -172,7 +172,7 @@
|
||||
"Indigo": "Indigo",
|
||||
"Purple": "Morea",
|
||||
"Pink": "Arrosa",
|
||||
"Search...": "Bilatu...",
|
||||
"Search...": "Bilatu…",
|
||||
"Avg. Ping": "Batazbesteko Pinga",
|
||||
"Avg. Response": "Batazbesteko erantzuna",
|
||||
"Entry Page": "Sarrera orria",
|
||||
@ -218,7 +218,7 @@
|
||||
"wayToGetDiscordURL": "You can get this by going to Server Settings -> Integrations -> Create Webhook",
|
||||
"Bot Display Name": "Bot Display Name",
|
||||
"Prefix Custom Message": "Prefix Custom Message",
|
||||
"Hello @everyone is...": "Hello {'@'}everyone is...",
|
||||
"Hello @everyone is...": "Kaixo {'@'}edonor da…",
|
||||
"teams": "Microsoft Teams",
|
||||
"Webhook URL": "Webhook URL",
|
||||
"wayToGetTeamsURL": "You can learn how to create a webhook URL {0}.",
|
||||
@ -325,7 +325,7 @@
|
||||
"Steam API Key": "Steam API Giltza",
|
||||
"Shrink Database": "Shrink Datubasea",
|
||||
"Pick a RR-Type...": "Pick a RR-Type...",
|
||||
"Pick Accepted Status Codes...": "Hautatu onartutako egoera kodeak...",
|
||||
"Pick Accepted Status Codes...": "Hautatu onartutako egoera kodeak…",
|
||||
"Default": "Lehenetsia",
|
||||
"HTTP Options": "HTTP Aukerak",
|
||||
"Create Incident": "Sortu inzidentzia",
|
||||
@ -527,7 +527,7 @@
|
||||
"There might be a typing error in the address.": "Idazketa-akats bat egon daiteke helbidean.",
|
||||
"What you can try:": "Probatu dezakezuna:",
|
||||
"Retype the address.": "Berridatzi helbidea.",
|
||||
"Go back to the previous page.": "Itzuli aurreko orrialdera",
|
||||
"Go back to the previous page.": "Itzuli aurreko orrialdera.",
|
||||
"Coming Soon": "Laster",
|
||||
"wayToGetClickSendSMSToken": "API erabiltzailea and API giltza hemendik lortu ditzakezu: {0} .",
|
||||
"Connection String": "Konexio katea",
|
||||
@ -537,5 +537,39 @@
|
||||
"ntfy Topic": "ntfy Topic",
|
||||
"Domain": "Domeinua",
|
||||
"Workstation": "Lan gunea",
|
||||
"disableCloudflaredNoAuthMsg": "Ez Auth moduan zaude, pasahitza ez da beharrezkoa."
|
||||
"disableCloudflaredNoAuthMsg": "Ez Auth moduan zaude, pasahitza ez da beharrezkoa.",
|
||||
"maintenanceStatus-ended": "Bukatuta",
|
||||
"maintenanceStatus-unknown": "Ezezaguna",
|
||||
"Enable": "Gaitu",
|
||||
"Strategy": "Estrategia",
|
||||
"General Monitor Type": "Monitorizazio mota orokorra",
|
||||
"Select status pages...": "Hautatu egoera orriak…",
|
||||
"Server Address": "Zerbitzari helbidea",
|
||||
"Learn More": "Ikasi gehiago",
|
||||
"weekdayShortTue": "Ast",
|
||||
"weekdayShortWed": "Asz",
|
||||
"Disable": "Desgaitu",
|
||||
"warningTimezone": "Zerbitzariaren orduzona erabiltzen ari da",
|
||||
"weekdayShortThu": "Og",
|
||||
"weekdayShortMon": "Asl",
|
||||
"Base URL": "Oinarri URLa",
|
||||
"high": "altua",
|
||||
"Economy": "Ekonomia",
|
||||
"Help": "Laguntza",
|
||||
"Game": "Jokoa",
|
||||
"statusMaintenance": "Mantenuan",
|
||||
"Maintenance": "Mantenua",
|
||||
"Passive Monitor Type": "Monitorizazio mota pasiboa",
|
||||
"Specific Monitor Type": "Zehaztutako monitorizazio mota",
|
||||
"markdownSupported": "Markdown sintaxia onartzen du",
|
||||
"Monitor": "Monitorizazio | Monitorizazioak",
|
||||
"resendDisabled": "Berbidaltzea desgaituta",
|
||||
"weekdayShortFri": "Ost",
|
||||
"weekdayShortSat": "Lar",
|
||||
"weekdayShortSun": "Iga",
|
||||
"dayOfWeek": "Asteko eguna",
|
||||
"dayOfMonth": "Hilabeteko eguna",
|
||||
"lastDay": "Azken eguna",
|
||||
"lastDay1": "Hilabeteko azken eguna",
|
||||
"Resend Notification if Down X times consecutively": "Bidali jakinarazpena X aldiz jarraian erortzen bada"
|
||||
}
|
||||
|
@ -173,7 +173,7 @@
|
||||
"Entry Page": "صفحه ورودی",
|
||||
"statusPageNothing": "چیزی اینجا نیست، لطفا یک گروه و یا یک مانیتور اضافه کنید.",
|
||||
"No Services": "هیچ سرویسی موجود نیست",
|
||||
"All Systems Operational": "تمامی سیستمها عملیاتی هستند",
|
||||
"All Systems Operational": "تمامی سیستمها فعال هستند",
|
||||
"Partially Degraded Service": "افت نسبی کیفیت سرویس",
|
||||
"Degraded Service": "افت کامل کیفیت سرویس",
|
||||
"Add Group": "اضافه کردن گروه",
|
||||
@ -323,7 +323,7 @@
|
||||
"Customize": "شخصی سازی",
|
||||
"Custom Footer": "فوتر اختصاصی",
|
||||
"No Proxy": "بدون پروکسی",
|
||||
"Authentication": "احراز هویت",
|
||||
"Authentication": "اعتبارسنجی",
|
||||
"steamApiKeyDescription": "برای مانیتورینگ یک سرور استیم، شما نیاز به یک \"Steam Web-API key\" دارید. برای دریافت کلید میتوانید از اینجا اقدام کنید: ",
|
||||
"No Monitors": "بدون مانیتور",
|
||||
"Untitled Group": "دسته بنده نشده",
|
||||
@ -677,7 +677,7 @@
|
||||
"Access Token": "توکن دسترسی",
|
||||
"smtp": "ایمیل (SMTP)",
|
||||
"Device": "دستگاه",
|
||||
"Proxy server has authentication": "پروکسی سرور دارای احراز هویت",
|
||||
"Proxy server has authentication": "پروکسی سرور دارای اعتبارسنجی است",
|
||||
"Add New Tag": "اضافه کردن تگ جدید",
|
||||
"Custom": "غیره",
|
||||
"default": "پیش فرض",
|
||||
@ -718,5 +718,40 @@
|
||||
"endDateTime": "ساعت/روز پایان",
|
||||
"cronSchedule": "برنامه زمانی: ",
|
||||
"invalidCronExpression": "حالت کرون نامعتبر است: {0}",
|
||||
"cronExpression": "حالت کرون"
|
||||
"cronExpression": "حالت کرون",
|
||||
"ntfyAuthenticationMethod": "روش اعتبارسنجی",
|
||||
"ntfyUsernameAndPassword": "نام کاربری و رمز عبور",
|
||||
"pushoverMessageTtl": "TTL پیام (ثانیه)",
|
||||
"Show Clickable Link": "نمایش لینک های قابل کلیک",
|
||||
"Open Badge Generator": "باز کردن نشان ساز (Badge Generator)",
|
||||
"Badge Generator": "نشان ساز (Badge Generator) {0}",
|
||||
"Badge Type": "نوع نشان",
|
||||
"Badge Duration": "مدت نشان",
|
||||
"Badge Label": "برچسب نشان",
|
||||
"Badge Prefix": "پیشوند نشان",
|
||||
"Badge Suffix": "پسوند نشان",
|
||||
"Badge Label Color": "رنگ برچسب نشان",
|
||||
"Badge Color": "رنگ نشان",
|
||||
"Badge Label Prefix": "پیشوند برچسب نشان",
|
||||
"Badge Label Suffix": "پسوند برچسب نشان",
|
||||
"Badge Down Color": "رنگ نشان زمانی که مانیتور دچار قطعی و Down شده است",
|
||||
"Badge Maintenance Color": "رنگ نشان برای زمانی که مانیتور در حالت نگهداری است",
|
||||
"Badge Warn Color": "رنگ نشان زمانی که مانیتور در حالت هشدار است",
|
||||
"Badge Down Days": "روز هایی که مانیتور دچار قطعی شده است",
|
||||
"Badge Style": "حالت نشان",
|
||||
"Badge value (For Testing only.)": "مقدار نشان (فقط برای تست.)",
|
||||
"Badge URL": "آدرس نشان",
|
||||
"Monitor Setting": "تنظیمات مانتیور {0}",
|
||||
"Show Clickable Link Description": "اگر انتخاب شود، همه کسانی که به این صفحه وضعیت دسترسی دارند میتوانند به صفحه مانیتور نیز دسترسی داشته باشند.",
|
||||
"Badge Up Color": "رنگ نشان زمانی که مانیتور بدون مشکل و بالا است",
|
||||
"Badge Pending Color": "رنگ نشان زمانی که مانیتور در حال انتظار است",
|
||||
"Badge Warn Days": "روزهایی که مانیتور در حالت هشدار است",
|
||||
"noGroupMonitorMsg": "موجود نیست. ابتدا یک گروه مانیتور جدید ایجاد کنید.",
|
||||
"Home": "خانه",
|
||||
"Edit Maintenance": "ویرایش تعمیر و نگهداری",
|
||||
"Cannot connect to the socket server": "عدم امکان ارتباط با سوکت سرور",
|
||||
"Reconnecting...": "ارتباط مجدد...",
|
||||
"Monitor Group": "گروه مانیتور",
|
||||
"Group": "گروه",
|
||||
"Close": "بستن"
|
||||
}
|
||||
|
@ -73,7 +73,7 @@
|
||||
"Delete": "Supprimer",
|
||||
"Current": "Actuellement",
|
||||
"Uptime": "Disponibilité",
|
||||
"Cert Exp.": "Expiration SSL",
|
||||
"Cert Exp.": "Expiration Cert SSL",
|
||||
"day": "jour | jours",
|
||||
"-day": "-jour",
|
||||
"hour": "heure",
|
||||
@ -88,8 +88,8 @@
|
||||
"Port": "Port",
|
||||
"Heartbeat Interval": "Intervalle de vérification",
|
||||
"Retries": "Essais",
|
||||
"Heartbeat Retry Interval": "Réessayer l'intervalle de vérification",
|
||||
"Resend Notification if Down X times consecutively": "Renvoyer la notification si en panne X fois consécutivement",
|
||||
"Heartbeat Retry Interval": "Intervalle de ré-essaie",
|
||||
"Resend Notification if Down X times consecutively": "Renvoyer la notification si hors ligne X fois consécutivement",
|
||||
"Advanced": "Avancé",
|
||||
"Upside Down Mode": "Mode inversé",
|
||||
"Max. Redirects": "Nombre maximum de redirections",
|
||||
@ -329,7 +329,7 @@
|
||||
"Body": "Corps",
|
||||
"Headers": "En-têtes",
|
||||
"PushUrl": "URL Push",
|
||||
"HeadersInvalidFormat": "Les en-têtes de la requête ne sont pas dans un format JSON valide : ",
|
||||
"HeadersInvalidFormat": "Les en-têtes de la requête ne sont pas dans un format JSON valide : ",
|
||||
"BodyInvalidFormat": "Le corps de la requête n'est pas dans un format JSON valide : ",
|
||||
"Monitor History": "Historique de la sonde",
|
||||
"clearDataOlderThan": "Conserver l'historique des données de la sonde durant {0} jours.",
|
||||
@ -338,7 +338,7 @@
|
||||
"One record": "Un enregistrement",
|
||||
"steamApiKeyDescription": "Pour surveiller un serveur Steam, vous avez besoin d'une clé Steam Web-API. Vous pouvez enregistrer votre clé ici : ",
|
||||
"Current User": "Utilisateur actuel",
|
||||
"topic": "Topic",
|
||||
"topic": "Sujet",
|
||||
"topicExplanation": "Topic MQTT à surveiller",
|
||||
"successMessage": "Message de réussite",
|
||||
"successMessageExplanation": "Message MQTT qui sera considéré comme un succès",
|
||||
@ -699,7 +699,7 @@
|
||||
"Edit Tag": "Modifier l'étiquette",
|
||||
"Body Encoding": "Encodage du corps",
|
||||
"telegramMessageThreadID": "(Facultatif) ID du fil de message",
|
||||
"telegramMessageThreadIDDescription": "(Facultatif) Identifiant unique pour le fil de discussion cible (sujet) du forum; pour les supergroupes du forum uniquement",
|
||||
"telegramMessageThreadIDDescription": "(Facultatif) Identifiant unique pour le fil de discussion ciblé (sujet) du forum; pour les supergroupes du forum uniquement",
|
||||
"telegramProtectContent": "Protéger le transfert/l'enregistrement",
|
||||
"telegramProtectContentDescription": "S'il est activé, les messages du robot dans Telegram seront protégés contre le transfert et l'enregistrement.",
|
||||
"telegramSendSilently": "Envoyer silencieusement",
|
||||
@ -749,5 +749,40 @@
|
||||
"endDateTime": "Date/heure de fin",
|
||||
"cronExpression": "Expression cron",
|
||||
"cronSchedule": "Calendrier : ",
|
||||
"invalidCronExpression": "Expression Cron non valide : {0}"
|
||||
"invalidCronExpression": "Expression Cron non valide : {0}",
|
||||
"ntfyUsernameAndPassword": "Nom d'utilisateur et mot de passe",
|
||||
"ntfyAuthenticationMethod": "Méthode d'authentification",
|
||||
"pushoverMessageTtl": "TTL Message (Secondes)",
|
||||
"Show Clickable Link": "Afficher le lien cliquable",
|
||||
"Show Clickable Link Description": "Si cette case est cochée, tous ceux qui ont accès à cette page d'état peuvent accéder à l'URL du moniteur.",
|
||||
"Open Badge Generator": "Ouvrir le générateur de badges",
|
||||
"Badge Type": "Type de badge",
|
||||
"Badge Duration": "Durée du badge",
|
||||
"Badge Prefix": "Préfixe de badge",
|
||||
"Badge Suffix": "Suffixe de badge",
|
||||
"Badge Label Color": "Couleur de l'étiquette du badge",
|
||||
"Badge Color": "Couleur du badge",
|
||||
"Badge Label Prefix": "Préfixe d'étiquette de badge",
|
||||
"Badge Label Suffix": "Suffixe d'étiquette de badge",
|
||||
"Badge Up Color": "Couleur du badge en ligne",
|
||||
"Badge Down Color": "Couleur du badge hors ligne",
|
||||
"Badge Pending Color": "Couleur du badge en attente",
|
||||
"Badge Maintenance Color": "Couleur du badge maintenance",
|
||||
"Badge Warn Color": "Couleur du badge d'avertissement",
|
||||
"Badge Warn Days": "Jours d'avertissement de badge",
|
||||
"Badge Style": "Style de badge",
|
||||
"Badge value (For Testing only.)": "Valeur du badge (Pour les tests uniquement.)",
|
||||
"Monitor Setting": "Réglage de la sonde {0}",
|
||||
"Badge Generator": "Générateur de badges {0}",
|
||||
"Badge Label": "Étiquette de badge",
|
||||
"Badge URL": "URL du badge",
|
||||
"Cannot connect to the socket server": "Impossible de se connecter au serveur de socket",
|
||||
"Reconnecting...": "Reconnexion...",
|
||||
"Edit Maintenance": "Modifier la maintenance",
|
||||
"Monitor Group": "Groupe de sonde | Groupe de sondes",
|
||||
"Badge Down Days": "Badge hors ligne",
|
||||
"Group": "Groupe",
|
||||
"Home": "Accueil",
|
||||
"noGroupMonitorMsg": "Pas disponible. Créez d'abord une sonde de groupe.",
|
||||
"Close": "Fermer"
|
||||
}
|
||||
|
23
src/lang/gl.json
Normal file
23
src/lang/gl.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"Settings": "Axustes",
|
||||
"Dashboard": "Panel",
|
||||
"Help": "Axuda",
|
||||
"General": "Xeral",
|
||||
"List": "Lista",
|
||||
"Home": "Casa",
|
||||
"Add": "Engadir",
|
||||
"Up": "Arriba",
|
||||
"Pending": "Pendente",
|
||||
"statusMaintenance": "Mantemento",
|
||||
"Maintenance": "Mantemento",
|
||||
"Unknown": "Descoñecido",
|
||||
"Reconnecting...": "Reconectando...",
|
||||
"pauseDashboardHome": "Pausa",
|
||||
"Pause": "Pausa",
|
||||
"Name": "Nome",
|
||||
"Status": "Estado",
|
||||
"DateTime": "DataHora",
|
||||
"Message": "Mensaxe",
|
||||
"languageName": "Galego",
|
||||
"Down": "Abaixo"
|
||||
}
|
@ -724,5 +724,22 @@
|
||||
"Edit Tag": "עריכת תגית",
|
||||
"Learn More": "לקריאה נוספת",
|
||||
"telegramSendSilently": "שליחה שקטה",
|
||||
"telegramSendSilentlyDescription": "שליחת הודעות שקטה. משתמשים יקבלו ההתראה ללא צליל."
|
||||
"telegramSendSilentlyDescription": "שליחת הודעות שקטה. משתמשים יקבלו ההתראה ללא צליל.",
|
||||
"Add New Tag": "הוסף תג חדש",
|
||||
"Home": "ראשי",
|
||||
"sameAsServerTimezone": "אותו איזור זמן כמו השרת",
|
||||
"cronSchedule": "לו\"ז: ",
|
||||
"twilioToNumber": "למספר",
|
||||
"startDateTime": "תאריך\\זמן התחלה",
|
||||
"pagertreeSilent": "שקט",
|
||||
"Reconnecting...": "מתחבר מחדש...",
|
||||
"statusPageRefreshIn": "רענון תוך: {0}",
|
||||
"Edit Maintenance": "ערוך תחזוקה",
|
||||
"pagertreeUrgency": "דחיפות",
|
||||
"pagertreeLow": "נמוכה",
|
||||
"pagertreeMedium": "בינונית",
|
||||
"pagertreeHigh": "גבוהה",
|
||||
"pagertreeCritical": "קריטי",
|
||||
"pagertreeResolve": "הגדרה אוטומטית",
|
||||
"ntfyUsernameAndPassword": "שם משתמש וסיסמא"
|
||||
}
|
||||
|
43
src/lang/hi.json
Normal file
43
src/lang/hi.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"Dashboard": "डैशबोर्ड",
|
||||
"Help": "मदद",
|
||||
"New Update": "नया अपडेट",
|
||||
"Language": "भाषा",
|
||||
"Appearance": "अपीयरेंस",
|
||||
"Theme": "थीम",
|
||||
"Game": "गेम",
|
||||
"languageName": "हिंदी",
|
||||
"Settings": "सेटिंग्स",
|
||||
"General": "जनरल",
|
||||
"List": "सूची",
|
||||
"Add": "जोड़ें",
|
||||
"Add New Monitor": "नया मॉनिटर जोड़ें",
|
||||
"Pending": "लंबित",
|
||||
"statusMaintenance": "रखरखाव",
|
||||
"Maintenance": "रखरखाव",
|
||||
"Unknown": "अज्ञात",
|
||||
"Cannot connect to the socket server": "सॉकेट सर्वर से कनेक्ट नहीं हो सकता",
|
||||
"pauseDashboardHome": "विराम",
|
||||
"Resume": "फिर से शुरू करें",
|
||||
"Delete": "हटाएं",
|
||||
"Current": "मौजूदा",
|
||||
"Up": "चालू",
|
||||
"General Monitor Type": "सामान्य मॉनिटर प्रकार",
|
||||
"Specific Monitor Type": "विशिष्ट मॉनिटर प्रकार",
|
||||
"Pause": "विराम",
|
||||
"Name": "नाम",
|
||||
"Message": "संदेश",
|
||||
"No important events": "कोई महत्वपूर्ण घटनाक्रम नहीं",
|
||||
"Edit": "परिवर्तन",
|
||||
"Ping": "पिंग",
|
||||
"Monitor Type": "मॉनिटर प्रकार",
|
||||
"Keyword": "कीवर्ड",
|
||||
"Friendly Name": "दोस्ताना नाम",
|
||||
"Version": "संस्करण",
|
||||
"Home": "घर",
|
||||
"Quick Stats": "शीघ्र आँकड़े",
|
||||
"Reconnecting...": "पुनः कनेक्ट किया जा रहा है...",
|
||||
"Down": "बंद",
|
||||
"Passive Monitor Type": "निष्क्रिय मॉनिटर प्रकार",
|
||||
"Status": "स्थिति"
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"languageName": "日本語",
|
||||
"checkEverySecond": "{0}秒ごとにチェックします",
|
||||
"retriesDescription": "サービスがダウンとしてマークされ、通知が送信されるまでの最大リトライ数",
|
||||
"retriesDescription": "サービスが完全に停止したと判断し、通知を送信する前に再接続を試みる最大回数",
|
||||
"ignoreTLSError": "HTTPS ウェブサイトの TLS/SSL エラーを無視する",
|
||||
"upsideDownModeDescription": "ステータスの扱いを逆にします。サービスに到達可能な場合は、DOWNとなる。",
|
||||
"maxRedirectDescription": "フォローするリダイレクトの最大数。リダイレクトを無効にするには0を設定する。",
|
||||
"upsideDownModeDescription": "稼働ステータスを反転して扱います。サービスに接続可能な場合は、停止として扱います。",
|
||||
"maxRedirectDescription": "必要な場合にリダイレクトする最大回数です。リダイレクトを無効にしたい場合は、0に設定してください。",
|
||||
"acceptedStatusCodesDescription": "成功した応答とみなされるステータスコードを選択する。",
|
||||
"passwordNotMatchMsg": "繰り返しのパスワードが一致しません。",
|
||||
"notificationDescription": "監視を機能させるには、監視に通知を割り当ててください。",
|
||||
@ -21,15 +21,15 @@
|
||||
"Language": "言語",
|
||||
"Appearance": "外観",
|
||||
"Theme": "テーマ",
|
||||
"General": "全般的",
|
||||
"General": "全般",
|
||||
"Version": "バージョン",
|
||||
"Check Update On GitHub": "GitHubでアップデートを確認する",
|
||||
"List": "一覧",
|
||||
"Add": "追加",
|
||||
"Add New Monitor": "監視の追加",
|
||||
"Quick Stats": "統計",
|
||||
"Up": "Up",
|
||||
"Down": "Down",
|
||||
"Up": "正常",
|
||||
"Down": "停止",
|
||||
"Pending": "中止",
|
||||
"Unknown": "不明",
|
||||
"Pause": "一時停止",
|
||||
@ -42,12 +42,12 @@
|
||||
"Edit": "編集",
|
||||
"Delete": "削除",
|
||||
"Current": "現在",
|
||||
"Uptime": "起動時間",
|
||||
"Uptime": "稼働時間",
|
||||
"Cert Exp.": "証明書有効期限",
|
||||
"day": "日 | 日間",
|
||||
"-day": "-日",
|
||||
"hour": "時間",
|
||||
"-hour": "-時間",
|
||||
"-hour": "時間",
|
||||
"Response": "レスポンス",
|
||||
"Ping": "Ping",
|
||||
"Monitor Type": "監視タイプ",
|
||||
@ -57,19 +57,19 @@
|
||||
"Hostname": "ホスト名",
|
||||
"Port": "ポート",
|
||||
"Heartbeat Interval": "監視間隔",
|
||||
"Retries": "Retries",
|
||||
"Advanced": "Advanced",
|
||||
"Upside Down Mode": "Upside Down Mode",
|
||||
"Retries": "再試行回数",
|
||||
"Advanced": "詳細設定",
|
||||
"Upside Down Mode": "反転モード",
|
||||
"Max. Redirects": "最大リダイレクト数",
|
||||
"Accepted Status Codes": "正常なステータスコード",
|
||||
"Save": "保存",
|
||||
"Notifications": "通知",
|
||||
"Not available, please setup.": "利用できません。設定してください。",
|
||||
"Not available, please setup.": "利用できません。設定が必要です。",
|
||||
"Setup Notification": "通知設定",
|
||||
"Light": "Light",
|
||||
"Dark": "Dark",
|
||||
"Auto": "Auto",
|
||||
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar",
|
||||
"Light": "ライト",
|
||||
"Dark": "ダーク",
|
||||
"Auto": "自動",
|
||||
"Theme - Heartbeat Bar": "テーマ - 監視バー",
|
||||
"Normal": "通常",
|
||||
"Bottom": "下部",
|
||||
"None": "なし",
|
||||
@ -120,7 +120,7 @@
|
||||
"Also apply to existing monitors": "既存のモニターにも適用する",
|
||||
"Export": "エクスポート",
|
||||
"Import": "インポート",
|
||||
"backupDescription": "すべての監視と通知方法をJSONファイルにできます。",
|
||||
"backupDescription": "すべての監視と通知設定をJSONファイルとしてバックアップすることができます。",
|
||||
"backupDescription2": "※ 履歴と統計のデータはバックアップされません。",
|
||||
"backupDescription3": "通知に使用するトークンなどの機密データも含まれています。注意して扱ってください。",
|
||||
"alertNoFile": "インポートするファイルを選択してください。",
|
||||
@ -171,7 +171,7 @@
|
||||
"Shrink Database": "データベースの縮小",
|
||||
"Start": "始める",
|
||||
"Retry": "リトライ",
|
||||
"Please read": "読んでください",
|
||||
"Please read": "次のリンクを参考にしてください",
|
||||
"Orange": "橙",
|
||||
"Gateway Type": "ゲートウェイの種類",
|
||||
"Game": "ゲーム",
|
||||
@ -240,7 +240,7 @@
|
||||
"Unpin": "ピンを外す",
|
||||
"Switch to Light Theme": "ライトテーマに切り替える",
|
||||
"Hide Tags": "タグを隠す",
|
||||
"Description": "概要",
|
||||
"Description": "メモ",
|
||||
"Untitled Group": "名前の無いグループ",
|
||||
"Services": "サービス",
|
||||
"Discard": "破棄",
|
||||
@ -258,7 +258,7 @@
|
||||
"proxyDescription": "プロキシはモニターに割り当てられていないと機能しません。",
|
||||
"setAsDefaultProxyDescription": "このプロキシは、新しいモニターに対してデフォルトで有効になっています。モニターごとに個別にプロキシを無効にすることができます。",
|
||||
"Remove Token": "Tokenを削除",
|
||||
"Stop": "止める",
|
||||
"Stop": "停止",
|
||||
"Add New Status Page": "新しいステータスページを追加",
|
||||
"Next": "次へ",
|
||||
"No Proxy": "プロキシなし",
|
||||
@ -500,7 +500,7 @@
|
||||
"default: notify all devices": "デフォルト:すべてのデバイスに通知する",
|
||||
"Trigger type:": "トリガータイプ:",
|
||||
"Event data:": "イベントデータ:",
|
||||
"backupOutdatedWarning": "非推奨:多くの機能が追加され、このバックアップ機能は少しメンテナンスされていないため、完全なバックアップの生成や復元はできません。",
|
||||
"backupOutdatedWarning": "非推奨: 多くの機能に変更があり、バックアップ機能の開発が一部滞っているため、完全なバックアップの作成や復元ができません。",
|
||||
"backupRecommend": "代わりにボリュームまたはデータフォルダ(./data/)を直接バックアップしてください。",
|
||||
"recurringInterval": "インターバル",
|
||||
"Recurring": "繰り返し",
|
||||
@ -512,5 +512,9 @@
|
||||
"Device Token": "デバイストークン",
|
||||
"recurringIntervalMessage": "毎日1回実行する|{0} 日に1回実行する",
|
||||
"Add New Tag": "新しいタグを追加",
|
||||
"statusPageMaintenanceEndDate": "終了日"
|
||||
"statusPageMaintenanceEndDate": "終了日",
|
||||
"Body Encoding": "ボディエンコード",
|
||||
"Learn More": "さらに詳しく",
|
||||
"infiniteRetention": "保持期間を無制限にしたい場合は、0に設定してください。",
|
||||
"Display Timezone": "表示タイムゾーン"
|
||||
}
|
||||
|
@ -748,5 +748,6 @@
|
||||
"lunaseaTarget": "대상",
|
||||
"lunaseaDeviceID": "기기 ID",
|
||||
"statusPageRefreshIn": "{0} 후 새로고침",
|
||||
"telegramMessageThreadIDDescription": "포럼의 대상 메시지 쓰레드(주제)에 대한 선택적 고유 식별인, 포럼 관리자 그룹에만 해당"
|
||||
"telegramMessageThreadIDDescription": "포럼의 대상 메시지 쓰레드(주제)에 대한 선택적 고유 식별인, 포럼 관리자 그룹에만 해당",
|
||||
"pagertreeSilent": "없음"
|
||||
}
|
||||
|
28
src/lang/ms.json
Normal file
28
src/lang/ms.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"Help": "Bantuan",
|
||||
"New Update": "Kemaskini baharu",
|
||||
"Appearance": "Penampilan",
|
||||
"Theme": "Tema",
|
||||
"General": "Umum",
|
||||
"Game": "Permainan",
|
||||
"Primary Base URL": "URL Pangkalan Utama",
|
||||
"Version": "Versi",
|
||||
"Add": "Menambah",
|
||||
"Quick Stats": "Statistik ringkas",
|
||||
"Up": "Dalam talian",
|
||||
"Down": "Luar talian",
|
||||
"Pending": "Belum selesai",
|
||||
"statusMaintenance": "Membaiki",
|
||||
"Maintenance": "Membaiki",
|
||||
"Unknown": "Tidak ketahui",
|
||||
"General Monitor Type": "Jenis monitor umum",
|
||||
"Check Update On GitHub": "Semak kemas kini dalam GitHub",
|
||||
"List": "Senarai",
|
||||
"Specific Monitor Type": "Jenis monitor spesifik",
|
||||
"markdownSupported": "Sintaks markdown disokong",
|
||||
"languageName": "Bahasa inggeris",
|
||||
"Dashboard": "Papan pemuka",
|
||||
"Language": "Bahasa",
|
||||
"Add New Monitor": "Tambah monitor baharu",
|
||||
"Passive Monitor Type": "Jenis monitor pasif"
|
||||
}
|
@ -536,11 +536,11 @@
|
||||
"pushoversounds cosmic": "Kosmiczny",
|
||||
"pushoversounds falling": "Spadek",
|
||||
"pushoversounds gamelan": "Gamelan",
|
||||
"pushoversounds incoming": "Incoming",
|
||||
"pushoversounds intermission": "Intermission",
|
||||
"pushoversounds incoming": "Przychodzące",
|
||||
"pushoversounds intermission": "Przerwa",
|
||||
"pushoversounds magic": "Magia",
|
||||
"pushoversounds mechanical": "Mechaniczny",
|
||||
"pushoversounds pianobar": "Piano Bar",
|
||||
"pushoversounds pianobar": "fortepianowy klawisz",
|
||||
"pushoversounds siren": "Syrena",
|
||||
"pushoversounds spacealarm": "Alarm kosmiczny",
|
||||
"pushoversounds tugboat": "Holownik",
|
||||
@ -608,7 +608,7 @@
|
||||
"backupRecommend": "Zamiast tego należy wykonać bezpośrednią kopię zapasową woluminu lub folderu danych (./data/).",
|
||||
"Optional": "Opcjonalne",
|
||||
"squadcast": "Squadcast",
|
||||
"SendKey": "SendKey",
|
||||
"SendKey": "Przycisk Wyślij",
|
||||
"SMSManager API Docs": "Dokumentacja API SMSManager ",
|
||||
"Gateway Type": "Typ bramy",
|
||||
"SMSManager": "SMSManager",
|
||||
@ -663,7 +663,7 @@
|
||||
"IconUrl": "URL ikony",
|
||||
"Enable DNS Cache": "Włącz pamięć podręczną DNS",
|
||||
"Single Maintenance Window": "Pojedyncze okno konserwacji",
|
||||
"Effective Date Range": "Zakres dat obowiązywania",
|
||||
"Effective Date Range": "Zakres dat obowiązywania (opcjonalnie)",
|
||||
"Schedule Maintenance": "Planowanie konserwacji",
|
||||
"DateTime Range": "Zakres czasowy",
|
||||
"Maintenance Time Window of a Day": "Okno czasowe konserwacji na dzień",
|
||||
@ -743,5 +743,21 @@
|
||||
"statusPageRefreshIn": "Odświeżenie w ciągu: {0}",
|
||||
"lunaseaDeviceID": "ID urządzenia",
|
||||
"lunaseaUserID": "ID użytkownika",
|
||||
"Add New Tag": "Dodaj nowy tag"
|
||||
"Add New Tag": "Dodaj nowy tag",
|
||||
"startDateTime": "Data/godzina rozpoczęcia",
|
||||
"cronSchedule": "Harmonogram: ",
|
||||
"invalidCronExpression": "Nieprawidłowe sformułowanie Cron: {0}",
|
||||
"sameAsServerTimezone": "Tak jak strefa czasowa serwera",
|
||||
"endDateTime": "Data/godzina zakończenia",
|
||||
"cronExpression": "Wyrażenie Cron",
|
||||
"ntfyAuthenticationMethod": "Metoda Uwierzytelnienia",
|
||||
"ntfyUsernameAndPassword": "Nazwa użytkownika i hasło",
|
||||
"noGroupMonitorMsg": "Niedostępna. Stwórz najpierw grupę monitorów.",
|
||||
"Close": "Zamknij",
|
||||
"pushoverMessageTtl": "TTL wiadomości (sekundy)",
|
||||
"Home": "Strona główna",
|
||||
"Group": "Grupa",
|
||||
"Monitor Group": "Grupa monitora",
|
||||
"Reconnecting...": "Ponowne łączenie...",
|
||||
"Cannot connect to the socket server": "Nie można połączyć się z serwerem gniazda"
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"languageName": "Português (Brasileiro)",
|
||||
"languageName": "Português (Brasil)",
|
||||
"checkEverySecond": "Verificar a cada {0} segundos",
|
||||
"retryCheckEverySecond": "Tentar novamente a cada {0} segundos",
|
||||
"retriesDescription": "Máximo de tentativas antes que o serviço seja marcado como inativo e uma notificação seja enviada",
|
||||
"ignoreTLSError": "Ignorar erros TLS/SSL para sites HTTPS",
|
||||
"upsideDownModeDescription": "Inverta o status de cabeça para baixo. Se o serviço estiver acessível, ele está OFFLINE.",
|
||||
"upsideDownModeDescription": "Inverta o status. Se o serviço estiver acessível, ele está DESLIGADO.",
|
||||
"maxRedirectDescription": "Número máximo de redirecionamentos a seguir. Defina como 0 para desativar redirecionamentos.",
|
||||
"acceptedStatusCodesDescription": "Selecione os códigos de status que são considerados uma resposta bem-sucedida.",
|
||||
"passwordNotMatchMsg": "A senha repetida não corresponde.",
|
||||
@ -27,7 +27,7 @@
|
||||
"confirmEnableTwoFAMsg": "Tem certeza de que deseja habilitar 2FA?",
|
||||
"confirmDisableTwoFAMsg": "Tem certeza de que deseja desativar 2FA?",
|
||||
"Settings": "Configurações",
|
||||
"Dashboard": "Dashboard",
|
||||
"Dashboard": "Painel",
|
||||
"New Update": "Nova Atualização",
|
||||
"Language": "Linguagem",
|
||||
"Appearance": "Aparência",
|
||||
@ -39,8 +39,8 @@
|
||||
"Add": "Adicionar",
|
||||
"Add New Monitor": "Adicionar novo monitor",
|
||||
"Quick Stats": "Estatísticas rápidas",
|
||||
"Up": "On",
|
||||
"Down": "Off",
|
||||
"Up": "Ligado",
|
||||
"Down": "Desligado",
|
||||
"Pending": "Pendente",
|
||||
"Unknown": "Desconhecido",
|
||||
"Pause": "Pausar",
|
||||
@ -49,12 +49,12 @@
|
||||
"DateTime": "Data hora",
|
||||
"Message": "Mensagem",
|
||||
"No important events": "Nenhum evento importante",
|
||||
"Resume": "Resumo",
|
||||
"Resume": "Retomar",
|
||||
"Edit": "Editar",
|
||||
"Delete": "Deletar",
|
||||
"Delete": "Apagar",
|
||||
"Current": "Atual",
|
||||
"Uptime": "Tempo de atividade",
|
||||
"Cert Exp.": "Cert Exp.",
|
||||
"Cert Exp.": "Expiração Do Certificado",
|
||||
"day": "dia | dias",
|
||||
"-day": "-dia",
|
||||
"hour": "hora",
|
||||
@ -71,9 +71,9 @@
|
||||
"Retries": "Novas tentativas",
|
||||
"Heartbeat Retry Interval": "Intervalo de repetição de Heartbeat",
|
||||
"Advanced": "Avançado",
|
||||
"Upside Down Mode": "Modo de cabeça para baixo",
|
||||
"Upside Down Mode": "Modo Invertido",
|
||||
"Max. Redirects": "Redirecionamentos Máx",
|
||||
"Accepted Status Codes": "Status Code Aceitáveis",
|
||||
"Accepted Status Codes": "Códigos HTTP Aceitáveis",
|
||||
"Save": "Salvar",
|
||||
"Notifications": "Notificações",
|
||||
"Not available, please setup.": "Não disponível, por favor configure.",
|
||||
@ -131,7 +131,7 @@
|
||||
"Create": "Criar",
|
||||
"Clear Data": "Limpar Dados",
|
||||
"Events": "Eventos",
|
||||
"Heartbeats": "Heartbeats",
|
||||
"Heartbeats": "Batimentos Cardíacos",
|
||||
"Auto Get": "Obter Automático",
|
||||
"backupDescription": "Você pode fazer backup de todos os monitores e todas as notificações em um arquivo JSON.",
|
||||
"backupDescription2": "OBS: Os dados do histórico e do evento não estão incluídos.",
|
||||
@ -187,17 +187,17 @@
|
||||
"Select status pages...": "Selecionar status pages…",
|
||||
"Game": "Jogo",
|
||||
"Passive Monitor Type": "Tipo de monitoramento passivo",
|
||||
"Specific Monitor Type": "Especificar tipo de monitoramento",
|
||||
"Specific Monitor Type": "Tipo de monitoramento específico",
|
||||
"Monitor": "Monitoramento | Monitoramentos",
|
||||
"needPushEvery": "Você deve chamar esta URL a cada {0} segundos.",
|
||||
"Push URL": "Push URL",
|
||||
"Push URL": "URL de push",
|
||||
"Custom": "Personalizado",
|
||||
"here": "aqui",
|
||||
"Required": "Requerido",
|
||||
"webhookJsonDesc": "{0} é bom para qualquer servidor HTTP moderno, como Express.js",
|
||||
"webhookAdditionalHeadersTitle": "Cabeçalhos Adicionais",
|
||||
"webhookAdditionalHeadersDesc": "Define cabeçalhos adicionais enviados com o webhook.",
|
||||
"Webhook URL": "Webhook URL",
|
||||
"Webhook URL": "URL Do Webhook",
|
||||
"Priority": "Prioridade",
|
||||
"Read more": "Ver mais",
|
||||
"appriseInstalled": "Apprise está instalado.",
|
||||
@ -270,15 +270,319 @@
|
||||
"All Status Pages": "Todas as Status Pages",
|
||||
"Method": "Método",
|
||||
"General Monitor Type": "Tipo de monitoramento geral",
|
||||
"markdownSupported": "Sintaxe Markdown suportada",
|
||||
"emojiCheatSheet": "Folha de dicas de emojis: {0}",
|
||||
"topic": "Tema",
|
||||
"markdownSupported": "Markdown suportado",
|
||||
"emojiCheatSheet": "Dicas de Emojis",
|
||||
"topic": "Tópico",
|
||||
"topicExplanation": "Tópico MQTT para monitorar",
|
||||
"successMessageExplanation": "Mensagem MQTT que será considerada como sucesso",
|
||||
"Content Type": "Tipo de Conteúdo",
|
||||
"Content Type": "Tipo do Conteúdo",
|
||||
"Shrink Database": "Encolher Banco de Dados",
|
||||
"Content": "Conteúdo",
|
||||
"Pick a RR-Type...": "Escolha um tipo RR…",
|
||||
"Pick Accepted Status Codes...": "Escolha Códigos de Status Aceitos…",
|
||||
"Pick Affected Monitors...": "Escolher Monitores Afetados…"
|
||||
"Pick a RR-Type...": "Selecione um RR-Type…",
|
||||
"Pick Accepted Status Codes...": "Selecione Os Códigos de Status Aceitos…",
|
||||
"Pick Affected Monitors...": "Selecione os Monitores Afetados…",
|
||||
"Channel Name": "Nome Do Canal",
|
||||
"Don't know how to get the token? Please read the guide:": "Não sabe com pegar o token? Por favor, leia o guia:",
|
||||
"smtpDkimheaderFieldNames": "Chaves Do Cabeçalho para assinar (Opcional)",
|
||||
"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.": "A conexão atual pode ser perdida se você estiver se conectando pelo túnel da Cloudflare. Você tem certeza que deseja pará-lo? Digite a sua senha para confirmar.",
|
||||
"shrinkDatabaseDescription": "Acionar a limpeza do banco de dados para o SQLite. Se o seu banco de dados foi criado depois de 1.10.0, a limpeza automática(AUTO_VACUUM) já é habilitada por padrão e essa ação não é necessária.",
|
||||
"Powered by": "Fornecido por",
|
||||
"deleteProxyMsg": "Você tem certeza que deseja deletar este proxy para todos os monitores?",
|
||||
"proxyDescription": "Os proxies devem ser atribuídos a um monitor para funcionar.",
|
||||
"Certificate Chain": "Cadeia De Certificados",
|
||||
"Domain Name Expiry Notification": "Notificação De Expiração Do Nome Do Domínio",
|
||||
"Proxy": "Proxy",
|
||||
"wayToGetTelegramChatID": "Você pode pegar o Chat ID enviando uma mensagem marcando o bot no grupo e indo nessa URL para ver o chat_id:",
|
||||
"wayToGetLineNotifyToken": "Você pode pegar o token de acesso de {0}",
|
||||
"disableCloudflaredNoAuthMsg": "Você está no modo sem autenticação, a senha não é necessária.",
|
||||
"Frontend Version do not match backend version!": "Versão do frontend é diferente da versão do backend!",
|
||||
"strategyManual": "Ativar/Desativar Manualmente",
|
||||
"weekdayShortThu": "Qui",
|
||||
"Basic Settings": "Configurações Básicas",
|
||||
"User ID": "ID Do Usuário",
|
||||
"Line Developers Console": "Linha Do Terminal De Desenvolvimento",
|
||||
"lineDevConsoleTo": "Linha Do Terminal De Desenvolvimento- {0}",
|
||||
"smseagleToken": "Token De Acesso Da API",
|
||||
"Notification Service": "Serviço De Notificação",
|
||||
"default: notify all devices": "padrão: notificar todos os dispositivos",
|
||||
"Trigger type:": "Tipo Do Acionamento:",
|
||||
"Then choose an action, for example switch the scene to where an RGB light is red.": "",
|
||||
"Enable": "Habilitado",
|
||||
"Disable": "Desabilitado",
|
||||
"IconUrl": "URL Do Ícone",
|
||||
"Enable DNS Cache": "Habilitar Cache Do DNS",
|
||||
"Single Maintenance Window": "Janela Única De Manutenção",
|
||||
"dnsCacheDescription": "Pode não funcionar em alguns ambientes com IPv6, desabita caso encontre qualquer problema.",
|
||||
"Messaging API": "API Da Mensageira",
|
||||
"Icon URL": "URL Do Ícone",
|
||||
"Clone Monitor": "Clonar Monitoramento",
|
||||
"Clone": "Clonar",
|
||||
"cloneOf": "Clone do {0}",
|
||||
"deleteMaintenanceMsg": "Você tem certeza que deseja apagar essa manutenção?",
|
||||
"sameAsServerTimezone": "O mesmo do servidor de fuso-horário",
|
||||
"startDateTime": "Início Data/Horário",
|
||||
"endDateTime": "Fim Data/Horário",
|
||||
"cronExpression": "Expressão Do Cron",
|
||||
"cronSchedule": "Agendar: ",
|
||||
"invalidCronExpression": "Expressão Cron inválida: {0}",
|
||||
"Display Timezone": "Mostrar Fuso-horário",
|
||||
"Server Timezone": "Servidor De Fuso-horário",
|
||||
"statusPageMaintenanceEndDate": "Fim",
|
||||
"Schedule Maintenance": "Agendar Manutenção",
|
||||
"Date and Time": "Data E Horário",
|
||||
"DateTime Range": "Intervalo De Tempo",
|
||||
"Maintenance Time Window of a Day": "Janela de tempo de manutenção de um dia",
|
||||
"uninstalling": "Desinstalando",
|
||||
"confirmUninstallPlugin": "Você tem certeza were quer desinstalar esse plugin?",
|
||||
"notificationRegional": "Região",
|
||||
"dnsPortDescription": "Porta do servidor DNS. O padrão é 53. Você pode mudar a porta em qualquer momento.",
|
||||
"affectedMonitorsDescription": "Selecione os monitores afetados pela manutenção atual",
|
||||
"Icon Emoji": "Ícone Do Emoji",
|
||||
"wayToGetKookBotToken": "Criar aplicação e pegar o token do bot em {0}",
|
||||
"Notification Sound": "Som De Notificação",
|
||||
"More info on:": "Mais informações em: {0}",
|
||||
"SMS Type": "Tipo Do SMS",
|
||||
"Internal Room Id": "ID Interno Da Sala",
|
||||
"Platform": "Plataforma",
|
||||
"serwersmsAPIPassword": "Senha Da API",
|
||||
"serwersmsPhoneNumber": "Número Do Telefone",
|
||||
"documentation": "documentação",
|
||||
"smtpDkimDomain": "Nome Do Domínio",
|
||||
"smtpDkimKeySelector": "Chave Selecionadora",
|
||||
"smtpDkimPrivateKey": "Chave Privada",
|
||||
"smtpDkimHashAlgo": "Algoritmo Hash (Opcional)",
|
||||
"smtpDkimskipFields": "Chaves Do Cabeçalho para não assinar (Opcional)",
|
||||
"alertaEnvironment": "Ambiente",
|
||||
"alertaRecoverState": "Estado De Recuperação",
|
||||
"smseagleEncoding": "Enviar como Unicode",
|
||||
"onebotGroupMessage": "Grupo",
|
||||
"onebotPrivateMessage": "Privado",
|
||||
"onebotUserOrGroupId": "ID do Grupo/Usuário",
|
||||
"No Maintenance": "Sem Manutenção",
|
||||
"telegramProtectContentDescription": "Se ativado, a mensagens do bot do Telegram serão protegidas contra encaminhamentos e salvamento.",
|
||||
"telegramProtectContent": "Proteger Contra Encaminhamento/Salvamento",
|
||||
"affectedStatusPages": "Mostrar essa mensagem de manutenção nas páginas de status selecionadas",
|
||||
"loadingError": "Não foi possível pegar os dados, por favor tente novamente.",
|
||||
"Bot Display Name": "Nome Visível Do Bot",
|
||||
"Access Token": "Token De Acesso",
|
||||
"Unpin": "Desfixar",
|
||||
"telegramSendSilently": "Enviar Silenciosamente",
|
||||
"telegramSendSilentlyDescription": "Enviar a mensagem silenciosamente. Os usuários não receberam uma notificação com som.",
|
||||
"YOUR BOT TOKEN HERE": "O SEU TOKEN DO BOT VAI AQUI",
|
||||
"warningTimezone": "Está usando os servidores de fuso-horários",
|
||||
"dayOfWeek": "Dia Da Semana",
|
||||
"dayOfMonth": "Dia Do Mês",
|
||||
"lastDay": "Último Dia",
|
||||
"lastDay1": "Último Dia Do Mês",
|
||||
"lastDay2": "Penúltimo Dia Do Mês",
|
||||
"lastDay3": "Antepenúltimo Dia Do Mês",
|
||||
"lastDay4": "Quarto Último Dia Do Mês",
|
||||
"weekdayShortMon": "Seg",
|
||||
"weekdayShortTue": "Ter",
|
||||
"weekdayShortWed": "Qua",
|
||||
"weekdayShortFri": "Sex",
|
||||
"weekdayShortSat": "Sab",
|
||||
"weekdayShortSun": "Dom",
|
||||
"wayToGetTeamsURL": "Você pode aprender a como criar a URL do webhook {0}.",
|
||||
"Hello @everyone is...": "Olá {'@'}everyone é…",
|
||||
"Number": "Número",
|
||||
"install": "Instalar",
|
||||
"installing": "Instalando",
|
||||
"uninstall": "Desinstalar",
|
||||
"Ignore TLS Error": "Ignorar Erro De TLS",
|
||||
"Discord Webhook URL": "URL Do Webhook Do Discord",
|
||||
"emailCustomSubject": "Assunto Personalizado",
|
||||
"Prefix Custom Message": "Prefixo Personalizado Da Mensagem",
|
||||
"wayToGetZohoCliqURL": "Você pode aprender a como criar uma URL de Webhook {0}.",
|
||||
"Channel access token": "Canal do token de acesso",
|
||||
"promosmsPassword": "Senha Da API",
|
||||
"promosmsLogin": "Nome Do Login Da API",
|
||||
"atLeastOneMonitor": "Selecione pelo menos um monitoramento afetado",
|
||||
"apiCredentials": "Credenciais Da API",
|
||||
"For safety, must use secret key": "Para segurança deve se usar uma chave secreta",
|
||||
"Device Token": "Token Do Dispositivo",
|
||||
"Retry": "Tentar Novamente",
|
||||
"Topic": "Tópico",
|
||||
"Setup Proxy": "Configuração Do Proxy",
|
||||
"Proxy Protocol": "Protocolo Do Proxy",
|
||||
"Proxy Server": "Servidor Proxy",
|
||||
"Proxy server has authentication": "O servidor proxy tem autenticação",
|
||||
"aboutWebhooks": "Mais informações sobre Webhooks em: {0}",
|
||||
"Integration Key": "Chave De Integração",
|
||||
"Integration URL": "URL De Integração",
|
||||
"do nothing": "fazendo nada",
|
||||
"onebotSafetyTips": "Por segurança deve adicionar o token de acesso",
|
||||
"Subject:": "Assunto:",
|
||||
"Valid To:": "Válido para:",
|
||||
"For example: nginx, Apache and Traefik.": "Por exemplo: Nginx, Apache e Traefik.",
|
||||
"Please read": "Por favor, leia",
|
||||
"RadiusCallingStationIdDescription": "Identificador do dispositivo de chamada",
|
||||
"certificationExpiryDescription": "O monitoramento por HTTPS envia a notificação quando o certificado TLS expirar em:",
|
||||
"or": "ou",
|
||||
"Effective Date Range": "Intervalo Efetivo De Data (Opcional)",
|
||||
"recurringIntervalMessage": "Rodar diariamente | Rodar a cada {0} dias",
|
||||
"Status:": "Status: {0}",
|
||||
"smtpDkimSettings": "Configurações DKIM",
|
||||
"alertaApiKey": "Chave Da API",
|
||||
"alertaAlertState": "Estado Do Alerta",
|
||||
"statusPageRefreshIn": "Atualizando em: {0}",
|
||||
"Untitled Group": "Grupo Sem Título",
|
||||
"primary": "primário",
|
||||
"setAsDefaultProxyDescription": "Este proxy será habilitado por padrão em todos os novos monitores. Você pode desabilitar o proxy individualmente para cada monitor.",
|
||||
"Valid": "Válido",
|
||||
"Invalid": "Inválido",
|
||||
"User": "Usuário",
|
||||
"Installed": "Instalado",
|
||||
"Not installed": "Não instalado",
|
||||
"enableProxyDescription": "Este proxy não afetará as solicitações do monitor até que seja ativado. Você pode controlar temporariamente a desativação do proxy de todos os monitores pelo status de ativação.",
|
||||
"Not running": "Desabilitado",
|
||||
"Remove Token": "Remover Token",
|
||||
"Start": "Iniciar",
|
||||
"Stop": "Parar",
|
||||
"Add New Status Page": "Adicionar Nova Página De Status",
|
||||
"Accept characters:": "Caracteres aceitos:",
|
||||
"Running": "Habilitado",
|
||||
"startOrEndWithOnly": "Apenas iniciar ou parar com {0}",
|
||||
"No consecutive dashes": "Sem traços consecutivos",
|
||||
"Next": "Próximo",
|
||||
"No Proxy": "Sem Proxy",
|
||||
"Authentication": "Autenticação",
|
||||
"HTTP Basic Auth": "Autenticação Básica No HTTP",
|
||||
"New Status Page": "Nova Página De Status",
|
||||
"Page Not Found": "Página Não Encontrada",
|
||||
"Reverse Proxy": "Proxy Reverso",
|
||||
"About": "Sobre",
|
||||
"Message:": "Mensagem:",
|
||||
"HTTP Headers": "Cabeçalhos HTTP",
|
||||
"Trust Proxy": "Proxy Confiável",
|
||||
"Other Software": "Outros Programas",
|
||||
"Days Remaining:": "Dias Restantes:",
|
||||
"No status pages": "Sem página de status",
|
||||
"Date Created": "Data De Criação",
|
||||
"Backup": "Cópia de Segurança",
|
||||
"wayToGetCloudflaredURL": "(Baixe o CloudFlareD de {0})",
|
||||
"cloudflareWebsite": "Site Da CloudaFlare",
|
||||
"Issuer:": "Emissor:",
|
||||
"Fingerprint:": "Impressão Digital:",
|
||||
"Footer Text": "Texto Do Rodapé",
|
||||
"Domain Names": "Nome Dos Domínios",
|
||||
"signedInDispDisabled": "Autenticação Desabilitada.",
|
||||
"RadiusSecretDescription": "Compartilhe o Segredo entre o cliente e o servidor",
|
||||
"Certificate Expiry Notification": "Notificação De Certificado Expirado",
|
||||
"The resource is no longer available.": "O recurso não está mais disponível.",
|
||||
"There might be a typing error in the address.": "Pode ter um erro de digitação no endereço.",
|
||||
"Retype the address.": "Digitar novamente o endereço.",
|
||||
"Go back to the previous page.": "Voltar para a página anterior.",
|
||||
"Query": "Query",
|
||||
"settingsCertificateExpiry": "O Certificado TLS Expira",
|
||||
"Connection Type": "Tipo Da Conexão",
|
||||
"signedInDisp": "Assinado como {0}",
|
||||
"RadiusCallingStationId": "ID Da Estação De Chamada",
|
||||
"RadiusCalledStationIdDescription": "Identificador do dispositivo de chamada",
|
||||
"Coming Soon": "Em Breve",
|
||||
"Connection String": "String De Conexão",
|
||||
"Docker Daemon": "Daemon Do Docker",
|
||||
"Show Powered By": "Mostrar Distribuído Por",
|
||||
"RadiusSecret": "Segredo Radius",
|
||||
"RadiusCalledStationId": "ID Da Estação Chamada",
|
||||
"deleteDockerHostMsg": "Você tem certeza que quer deletar esse host do Docker para todos os monitores?",
|
||||
"tcp": "TCP / HTTP",
|
||||
"Docker Container": "Container Docker",
|
||||
"Container Name / ID": "Nome / ID do Container",
|
||||
"Domain": "Domínio",
|
||||
"Workstation": "Estação De Trabalho",
|
||||
"Packet Size": "Tamanho Do Pacote",
|
||||
"Bot Token": "Token do Bot",
|
||||
"wayToGetTelegramToken": "Você pode pegar o token de {0}.",
|
||||
"chatIDNotFound": "Chat ID não encontrado; por favor envia uma mensagem para o bot primeiro",
|
||||
"Chat ID": "Chat ID",
|
||||
"Docker Hosts": "Hosts Do Docker",
|
||||
"Docker Host": "Host Do Docker",
|
||||
"Examples": "Exemplos",
|
||||
"maintenanceStatus-under-maintenance": "Em Manutenção",
|
||||
"Long-Lived Access Token": "Token De Acesso De Longa Duração",
|
||||
"Home Assistant URL": "URL Do Home Assinant",
|
||||
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "O token de acessos de longa duração pode ser criado clicando no nome do seu perfil, com o botão esquerdo, ir até o final da lista e clicar em Criar Token. ",
|
||||
"Event type:": "Tipo Do Evento:",
|
||||
"Event data:": "Dados Do Evento:",
|
||||
"Frontend Version": "Versão Do Frontend",
|
||||
"backupRecommend": "Por favor faça uma cópia do volume ou da pasta com dados(./data/) diretamente ao invés.",
|
||||
"Optional": "Opcional",
|
||||
"recurringInterval": "Intervalo",
|
||||
"Recurring": "Recorrente",
|
||||
"pauseMaintenanceMsg": "Você tem certeza que quer pausar?",
|
||||
"maintenanceStatus-inactive": "Inativo",
|
||||
"maintenanceStatus-scheduled": "Agendado",
|
||||
"maintenanceStatus-ended": "Terminando",
|
||||
"maintenanceStatus-unknown": "Desconhecido",
|
||||
"enableGRPCTls": "Permita para enviar requisições gRPC com conexões TLS",
|
||||
"confirmDeleteTagMsg": "Você tem certeza que deseja apagar essa tag? Monitores associados a essa tag não serão apagados.",
|
||||
"grpcMethodDescription": "O nome do método é convertido para o formato cammelCase, exemplos: enviarBomDia, verificar, etc.",
|
||||
"infiniteRetention": "Defina como 0 para um tempo infinito de retenção.",
|
||||
"octopushLegacyHint": "Você usa a versão legada do Octopush (2011-2020) ou a nova versão?",
|
||||
"Example:": "Exemplo: {0}",
|
||||
"Read more:": "Leia mais em: {0}",
|
||||
"promosmsAllowLongSMS": "Permitir SMS grandes",
|
||||
"Android": "Android",
|
||||
"Huawei": "Huawei",
|
||||
"smseagleTo": "Números Dos Telefones",
|
||||
"smseaglePriority": "Prioridade da mensagem (0-9, padrão=0)",
|
||||
"dataRetentionTimeError": "O período de retenção tem que ser maior ou igual a 0",
|
||||
"User Key": "Chave Do Usuário",
|
||||
"Device": "Dispositivo",
|
||||
"Message Title": "Título Da Mensagem",
|
||||
"defaultNotificationName": "Minha {notification} Alerta({number})",
|
||||
"light": "claro",
|
||||
"socket": "Soquete",
|
||||
"Add New Tag": "Adicionar Nova Tag",
|
||||
"API Username": "Usuário Da API",
|
||||
"API Key": "Chave Da API",
|
||||
"Show update if available": "Mostrar atualização se disponível",
|
||||
"Also check beta release": "Também verificar lançamentos em beta",
|
||||
"Using a Reverse Proxy?": "Está usando um Proxy Reverso?",
|
||||
"Check how to config it for WebSocket": "Verifique como configurar para o WebSocket",
|
||||
"Steam Game Server": "Servidor De Jogo Da Steam",
|
||||
"Most likely causes:": "Causas mais prováveis:",
|
||||
"What you can try:": "O que você pode tentar:",
|
||||
"apiKey-active": "Ativa",
|
||||
"Expiry": "Expiração",
|
||||
"endpoint": "endpoint",
|
||||
"pagertreeIntegrationUrl": "URL de Integração",
|
||||
"pagertreeUrgency": "Urgência",
|
||||
"telegramMessageThreadID": "(Opcional) Message Thread ID",
|
||||
"Edit Tag": "Editar Etiqueta",
|
||||
"Server Address": "Endereço do Servidor",
|
||||
"Learn More": "Aprender Mais",
|
||||
"needSignalAPI": "Você precisa de um cliente Signal com API REST.",
|
||||
"Generate": "Gerar",
|
||||
"deleteAPIKeyMsg": "Você tem certeza de que quer apagar essa chave de API?",
|
||||
"plugin": "Plugin | Plugins",
|
||||
"Expiry date": "Data de expiração",
|
||||
"Don't expire": "Não expira",
|
||||
"Continue": "Continuar",
|
||||
"Add Another": "Adicionar Outro",
|
||||
"Key Added": "Chave Adicionada",
|
||||
"Add API Key": "Adicionar chave de API",
|
||||
"No API Keys": "Sem chaves de API",
|
||||
"apiKey-expired": "Expirada",
|
||||
"apiKey-inactive": "Inativa",
|
||||
"Expires": "Expira",
|
||||
"disableAPIKeyMsg": "Você tem certeza de que quer desativar essa chave de API?",
|
||||
"smtp": "Email (SMTP)",
|
||||
"secureOptionTLS": "TLS (465)",
|
||||
"From Email": "Email De",
|
||||
"smtpCC": "CC",
|
||||
"smtpBCC": "CCO",
|
||||
"To Email": "Email Para",
|
||||
"Recipients": "Destinatários",
|
||||
"Google Analytics ID": "ID Google Analytics",
|
||||
"Post": "Post",
|
||||
"Slug": "Slug",
|
||||
"The slug is already taken. Please choose another slug.": "Esse slug já foi utilizado. Por favor escolha outro slug.",
|
||||
"Setup Docker Host": "Configurar Host Docker",
|
||||
"trustProxyDescription": "Confiar nos cabeçalhos 'X-Forwarded-*'. Se você quer obter o endereço IP do cliente correto no seu Uptime Kuma que está por trás de um proxy como Nginx ou Apache, você deve ativar isso.",
|
||||
"Automations can optionally be triggered in Home Assistant:": "Automações podem opcionalmente ser disparadas no Home Assistant:",
|
||||
"secureOptionNone": "Nenhum / STARTTLS (25, 587)",
|
||||
"apiKeyAddedMsg": "Sua chave de API foi adicionada. Por favor anote essa chave, ela não será mostrada novamente.",
|
||||
"Show Clickable Link": "Mostrar Link Clicável"
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
"upsideDownModeDescription": "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.",
|
||||
"maxRedirectDescription": "Максимальное количество перенаправлений. Поставьте 0, чтобы отключить перенаправления.",
|
||||
"acceptedStatusCodesDescription": "Выберите коды статусов для определения доступности сервиса.",
|
||||
"passwordNotMatchMsg": "Повтор пароля не совпадает.",
|
||||
"passwordNotMatchMsg": "Введёные пароли не совпадают",
|
||||
"notificationDescription": "Привяжите уведомления к мониторам.",
|
||||
"keywordDescription": "Поиск слова в чистом HTML или в JSON-ответе (чувствительно к регистру).",
|
||||
"pauseDashboardHome": "Пауза",
|
||||
@ -43,7 +43,7 @@
|
||||
"Delete": "Удалить",
|
||||
"Current": "Текущий",
|
||||
"Uptime": "Аптайм",
|
||||
"Cert Exp.": "Сертификат истекает.",
|
||||
"Cert Exp.": "Сертификат истекает",
|
||||
"day": "день | дней",
|
||||
"-day": "-дней",
|
||||
"hour": "час",
|
||||
@ -69,7 +69,7 @@
|
||||
"Light": "Светлая",
|
||||
"Dark": "Тёмная",
|
||||
"Auto": "Авто",
|
||||
"Theme - Heartbeat Bar": "Тема - Полоса частоты опроса",
|
||||
"Theme - Heartbeat Bar": "Полоса частоты опроса",
|
||||
"Normal": "Обычный",
|
||||
"Bottom": "Снизу",
|
||||
"None": "Отсутствует",
|
||||
@ -160,7 +160,7 @@
|
||||
"Tag with this name already exist.": "Такой тег уже существует.",
|
||||
"Tag with this value already exist.": "Тег с таким значением уже существует.",
|
||||
"color": "цвет",
|
||||
"value (optional)": "значение (опционально)",
|
||||
"value (optional)": "значение (необязательно)",
|
||||
"Gray": "Серый",
|
||||
"Red": "Красный",
|
||||
"Orange": "Оранжевый",
|
||||
@ -175,9 +175,9 @@
|
||||
"Entry Page": "Главная страница",
|
||||
"statusPageNothing": "Здесь пусто. Добавьте группу или монитор.",
|
||||
"No Services": "Нет сервисов",
|
||||
"All Systems Operational": "Все системы работают в штатном режиме",
|
||||
"Partially Degraded Service": "Сервисы работают частично",
|
||||
"Degraded Service": "Все сервисы не работают",
|
||||
"All Systems Operational": "Все системы работают",
|
||||
"Partially Degraded Service": "Частичная работа сервисов",
|
||||
"Degraded Service": "Отказ всех сервисов",
|
||||
"Add Group": "Добавить группу",
|
||||
"Add a monitor": "Добавить монитор",
|
||||
"Edit Status Page": "Редактировать",
|
||||
@ -212,7 +212,7 @@
|
||||
"pushOptionalParams": "Опциональные параметры: {0}",
|
||||
"defaultNotificationName": "Моё уведомление {notification} ({number})",
|
||||
"here": "здесь",
|
||||
"Required": "Требуется",
|
||||
"Required": "Обязательно",
|
||||
"Bot Token": "Токен бота",
|
||||
"wayToGetTelegramToken": "Вы можете взять токен здесь - {0}.",
|
||||
"Chat ID": "ID чата",
|
||||
@ -296,7 +296,7 @@
|
||||
"promosmsPhoneNumber": "Номер телефона (для получателей из Польши можно пропустить код региона)",
|
||||
"promosmsSMSSender": "Имя отправителя SMS: Зарегистрированное или одно из имён по умолчанию: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
|
||||
"Feishu WebHookUrl": "Feishu WebHookURL",
|
||||
"matrixHomeserverURL": "URL сервера (вместе с http(s):// и опционально порт)",
|
||||
"matrixHomeserverURL": "URL сервера (вместе с http(s):// и по желанию порт)",
|
||||
"Internal Room Id": "Внутренний ID комнаты",
|
||||
"matrixDesc1": "Внутренний ID комнаты можно найти в Подробностях в параметрах канала вашего Matrix клиента. Он должен выглядеть примерно как !QMdRCpUIfLwsfjxye6:home.server.",
|
||||
"matrixDesc2": "Рекомендуется создать нового пользователя и не использовать токен доступа личного пользователя Matrix, т.к. это влечёт за собой полный доступ к аккаунту и к комнатам, в которых вы состоите. Вместо этого создайте нового пользователя и пригласите его только в ту комнату, в которой вы хотите получать уведомления. Токен доступа можно получить, выполнив команду {0}",
|
||||
@ -335,9 +335,9 @@
|
||||
"Current User": "Текущий пользователь",
|
||||
"About": "О программе",
|
||||
"Description": "Описание",
|
||||
"Powered by": "Работает на основе скрипта от",
|
||||
"Powered by": "Работает на",
|
||||
"shrinkDatabaseDescription": "Включает VACUUM для базы данных SQLite. Если ваша база данных была создана на версии 1.10.0 и более, AUTO_VACUUM уже включен и это действие не требуется.",
|
||||
"deleteStatusPageMsg": "Вы действительно хотите удалить эту страницу статуса сервисов?",
|
||||
"deleteStatusPageMsg": "Вы действительно хотите удалить эту страницу статуса?",
|
||||
"Style": "Стиль",
|
||||
"info": "ИНФО",
|
||||
"warning": "ВНИМАНИЕ",
|
||||
@ -367,7 +367,7 @@
|
||||
"Pick Accepted Status Codes...": "Выберите принятые коды состояния…",
|
||||
"Default": "По умолчанию",
|
||||
"Please input title and content": "Пожалуйста, введите название и содержание",
|
||||
"Last Updated": "Последнее Обновление",
|
||||
"Last Updated": "Последнее обновление",
|
||||
"Untitled Group": "Группа без названия",
|
||||
"Services": "Сервисы",
|
||||
"serwersms": "SerwerSMS.pl",
|
||||
@ -379,11 +379,11 @@
|
||||
"smtpDkimSettings": "DKIM Настройки",
|
||||
"smtpDkimDesc": "Пожалуйста ознакомьтесь с {0} Nodemailer DKIM для использования.",
|
||||
"documentation": "документацией",
|
||||
"smtpDkimDomain": "Имя Домена",
|
||||
"smtpDkimDomain": "Имя домена",
|
||||
"smtpDkimKeySelector": "Ключ",
|
||||
"smtpDkimPrivateKey": "Приватный ключ",
|
||||
"smtpDkimHashAlgo": "Алгоритм хэша (опционально)",
|
||||
"smtpDkimheaderFieldNames": "Заголовок ключей для подписи (опционально)",
|
||||
"smtpDkimHashAlgo": "Алгоритм хэша (необязательно)",
|
||||
"smtpDkimheaderFieldNames": "Заголовок ключей для подписи (необязательно)",
|
||||
"smtpDkimskipFields": "Заголовок ключей не для подписи (опционально)",
|
||||
"gorush": "Gorush",
|
||||
"alerta": "Alerta",
|
||||
@ -439,9 +439,9 @@
|
||||
"Uptime Kuma": "Uptime Kuma",
|
||||
"Slug": "Slug",
|
||||
"Accept characters:": "Принимаемые символы:",
|
||||
"startOrEndWithOnly": "Начинается или кончается только {0}",
|
||||
"startOrEndWithOnly": "Начинается или заканчивается только на {0}",
|
||||
"No consecutive dashes": "Без последовательных тире",
|
||||
"The slug is already taken. Please choose another slug.": "Слово уже занято. Пожалуйста, выберите другой вариант.",
|
||||
"The slug is already taken. Please choose another slug.": "Этот slug уже занят. Пожалуйста, выберите другой.",
|
||||
"Page Not Found": "Страница не найдена",
|
||||
"wayToGetCloudflaredURL": "(Скачать cloudflared с {0})",
|
||||
"cloudflareWebsite": "Веб-сайт Cloudflare",
|
||||
@ -467,7 +467,7 @@
|
||||
"onebotMessageType": "Тип сообщения OneBot",
|
||||
"onebotGroupMessage": "Группа",
|
||||
"onebotPrivateMessage": "Private",
|
||||
"onebotUserOrGroupId": "ID группы или пользователя",
|
||||
"onebotUserOrGroupId": "ID группы/пользователя",
|
||||
"onebotSafetyTips": "В целях безопасности необходимо установить токен доступа",
|
||||
"PushDeer Key": "ключ PushDeer",
|
||||
"Footer Text": "Текст нижнего колонтитула",
|
||||
@ -568,7 +568,7 @@
|
||||
"goAlertInfo": "GoAlert — это приложение с открытым исходным кодом для составления расписания вызовов, автоматической эскалации и уведомлений (например, SMS или голосовых звонков). Автоматически привлекайте нужного человека, нужным способом и в нужное время! {0}",
|
||||
"goAlertIntegrationKeyInfo": "Получить общий ключ интеграции API для сервиса в этом формате \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" обычно значение параметра токена скопированного URL.",
|
||||
"goAlert": "GoAlert",
|
||||
"backupOutdatedWarning": "Устарело: поскольку добавлено множество функций, а эта функция резервного копирования немного не поддерживается, она не может создать или восстановить полную резервную копию.",
|
||||
"backupOutdatedWarning": "Устарело: эта функция резервного копирования более не поддерживается. Поскольку добавлено множество функций, она не может создать или восстановить полную резервную копию.",
|
||||
"backupRecommend": "Сделайте резервную копию тома или папки с данными (./data/) напрямую.",
|
||||
"Optional": "Необязательно",
|
||||
"squadcast": "Squadcast",
|
||||
@ -578,24 +578,24 @@
|
||||
"SMSManager": "SMSManager",
|
||||
"You can divide numbers with": "Вы можете делить числа с",
|
||||
"or": "или",
|
||||
"Maintenance": "Обслуживание",
|
||||
"Schedule maintenance": "Запланировать обслуживание",
|
||||
"affectedMonitorsDescription": "Выберите мониторы, которые будут затронуты во время обслуживания",
|
||||
"affectedStatusPages": "Показывать уведомление об обслуживании на выбранных страницах статуса",
|
||||
"Maintenance": "Техобслуживание",
|
||||
"Schedule maintenance": "Запланировать техобслуживание",
|
||||
"affectedMonitorsDescription": "Выберите мониторы, которые будут затронуты во время техбслуживания",
|
||||
"affectedStatusPages": "Показывать уведомление о техбслуживании на выбранных страницах статуса",
|
||||
"atLeastOneMonitor": "Выберите больше одного затрагиваемого монитора",
|
||||
"dnsPortDescription": "По умолчанию порт DNS сервера - 53. Мы можете изменить его в любое время.",
|
||||
"Monitor": "Монитор | Мониторы",
|
||||
"webhookAdditionalHeadersTitle": "Дополнительные Заголовки",
|
||||
"recurringIntervalMessage": "Запускать 1 раз каждый день | Запускать 1 раз каждые {0} дней",
|
||||
"error": "ошибка",
|
||||
"statusMaintenance": "Обслуживание",
|
||||
"statusMaintenance": "Техобслуживание",
|
||||
"Affected Monitors": "Затронутые мониторы",
|
||||
"Start of maintenance": "Начало обслуживания",
|
||||
"Start of maintenance": "Начало техобслуживания",
|
||||
"All Status Pages": "Все страницы статусов",
|
||||
"Select status pages...": "Выберите страницу статуса…",
|
||||
"resendEveryXTimes": "Повтор каждые {0} раз",
|
||||
"resendDisabled": "Повторная отправка отключена",
|
||||
"deleteMaintenanceMsg": "Вы действительно хотите удалить это обслуживание?",
|
||||
"deleteMaintenanceMsg": "Вы действительно хотите удалить это техбслуживание?",
|
||||
"critical": "критично",
|
||||
"Custom Monitor Type": "Собственный тип монитора",
|
||||
"markdownSupported": "Поддерживает синтаксис Markdown",
|
||||
@ -630,7 +630,7 @@
|
||||
"lastDay2": "Второй последний день месяца",
|
||||
"lastDay3": "Третий последний день месяца",
|
||||
"lastDay4": "Четвертый последний день месяца",
|
||||
"No Maintenance": "Без обслуживания",
|
||||
"No Maintenance": "Нет техбслуживаний",
|
||||
"pauseMaintenanceMsg": "Вы уверены что хотите поставить на паузу?",
|
||||
"maintenanceStatus-under-maintenance": "На техобслуживании",
|
||||
"maintenanceStatus-inactive": "Неактивен",
|
||||
@ -640,13 +640,13 @@
|
||||
"Display Timezone": "Показать часовой пояс",
|
||||
"Server Timezone": "Часовой пояс сервера",
|
||||
"statusPageMaintenanceEndDate": "Конец",
|
||||
"IconUrl": "URL Иконки",
|
||||
"IconUrl": "URL иконки",
|
||||
"Enable DNS Cache": "Включить DNS кэш",
|
||||
"Enable": "Включить",
|
||||
"Disable": "Отключить",
|
||||
"Single Maintenance Window": "Единое Окно Обслуживания",
|
||||
"Schedule Maintenance": "Запланировать обслуживание",
|
||||
"Date and Time": "Дата и Время",
|
||||
"Single Maintenance Window": "Единое окно техбслуживания",
|
||||
"Schedule Maintenance": "Запланировать техбслуживание",
|
||||
"Date and Time": "Дата и время",
|
||||
"DateTime Range": "Промежуток даты и времени",
|
||||
"uninstalling": "Удаляется",
|
||||
"dataRetentionTimeError": "Период хранения должен быть равен 0 или больше",
|
||||
@ -676,10 +676,10 @@
|
||||
"Integration URL": "URL интеграции",
|
||||
"do nothing": "ничего не делать",
|
||||
"smseagleTo": "Номер(а) телефона",
|
||||
"smseagleGroup": "Название(я) групп телефонной книги",
|
||||
"smseagleContact": "Имена контактов из телефонной книжки",
|
||||
"smseagleGroup": "Название(я) группы телефонной книги",
|
||||
"smseagleContact": "Имена контактов телефонной книги",
|
||||
"smseagleRecipientType": "Тип получателя",
|
||||
"smseagleRecipient": "Получатель(я) (через запятую, если необходимо указать несколько)",
|
||||
"smseagleRecipient": "Получатель(и) (если множество, должны быть разделены запятой)",
|
||||
"smseagleToken": "Токен доступа API",
|
||||
"smseagleUrl": "URL вашего SMSEagle устройства",
|
||||
"smseagleEncoding": "Отправить в юникоде",
|
||||
@ -695,7 +695,7 @@
|
||||
"telegramProtectContentDescription": "Если включено, сообщения бота в Telegram будут запрещены для пересылки и сохранения.",
|
||||
"telegramSendSilently": "Отправить без звука",
|
||||
"telegramSendSilentlyDescription": "Пользователи получат уведомление без звука.",
|
||||
"Maintenance Time Window of a Day": "Суточный интервал для обслуживания",
|
||||
"Maintenance Time Window of a Day": "Суточный интервал для техбслуживания",
|
||||
"Clone Monitor": "Копия",
|
||||
"Clone": "Копия",
|
||||
"cloneOf": "Копия {0}",
|
||||
@ -703,31 +703,31 @@
|
||||
"Add New Tag": "Добавить тег",
|
||||
"Body Encoding": "Тип содержимого запроса.(JSON or XML)",
|
||||
"Strategy": "Стратегия",
|
||||
"Free Mobile User Identifier": "Бесплатный идентификатор мобильного пользователя",
|
||||
"Free Mobile User Identifier": "Бесплатный мобильный идентификатор пользователя",
|
||||
"Auto resolve or acknowledged": "Автоматическое разрешение или подтверждение",
|
||||
"auto acknowledged": "автоматическое подтверждение",
|
||||
"auto resolve": "автоматическое разрешение",
|
||||
"API Keys": "Ключи API",
|
||||
"Expiry": "Истекает",
|
||||
"Expiry date": "Дата окончания действия",
|
||||
"Expiry": "Срок действия",
|
||||
"Expiry date": "Дата истечения срока действия",
|
||||
"Don't expire": "Не истекает",
|
||||
"Continue": "Продолжать",
|
||||
"Add Another": "Добавьте еще один",
|
||||
"Continue": "Продолжить",
|
||||
"Add Another": "Добавить еще",
|
||||
"Key Added": "Ключ добавлен",
|
||||
"Add API Key": "Добавить ключ API",
|
||||
"No API Keys": "Нет API ключей",
|
||||
"Add API Key": "Добавить API ключ",
|
||||
"No API Keys": "Нет ключей API",
|
||||
"apiKey-active": "Активный",
|
||||
"apiKey-expired": "Истёк",
|
||||
"apiKey-inactive": "Неактивный",
|
||||
"Expires": "Истекает",
|
||||
"disableAPIKeyMsg": "Вы уверены, что хотите отключить этот ключ?",
|
||||
"disableAPIKeyMsg": "Вы уверены, что хотите отключить этот API ключ?",
|
||||
"Generate": "Сгенерировать",
|
||||
"pagertreeResolve": "Автоматическое разрешение",
|
||||
"pagertreeDoNothing": "ничего не делать",
|
||||
"pagertreeDoNothing": "Ничего не делать",
|
||||
"lunaseaTarget": "Цель",
|
||||
"lunaseaDeviceID": "Идентификатор устройства",
|
||||
"lunaseaUserID": "Идентификатор пользователя",
|
||||
"Lowcost": "Низкая стоимость",
|
||||
"Lowcost": "Бюджетный",
|
||||
"pagertreeIntegrationUrl": "URL-адрес интеграции",
|
||||
"pagertreeUrgency": "Срочность",
|
||||
"pagertreeSilent": "Тихий",
|
||||
@ -736,15 +736,15 @@
|
||||
"pagertreeHigh": "Высокий",
|
||||
"pagertreeCritical": "Критический",
|
||||
"high": "высокий",
|
||||
"promosmsAllowLongSMS": "Разрешить длинные SMS-сообщения",
|
||||
"promosmsAllowLongSMS": "Разрешить длинные СМС",
|
||||
"Economy": "Экономия",
|
||||
"wayToGetPagerDutyKey": "Вы можете получить это, перейдя в службу -> Каталог служб -> (Выберите службу) -> Интеграции -> Добавить интеграцию. Здесь вы можете выполнить поиск по \"Events API V2\". Дополнительная информация {0}",
|
||||
"apiKeyAddedMsg": "Ваш API ключ был добавлен. Пожалуйста, запишите это, так как оно больше не будет показан.",
|
||||
"wayToGetPagerDutyKey": "Вы можете это получить, перейдя в Сервис -> Каталог сервисов -> (Выберите сервис) -> Интеграции -> Добавить интеграцию. Здесь вы можете искать «Events API V2». Подробнее {0}",
|
||||
"apiKeyAddedMsg": "Ваш ключ API добавлен. Пожалуйста, обратите внимание на это сообщение, так как оно отображается один раз.",
|
||||
"deleteAPIKeyMsg": "Вы уверены, что хотите удалить этот ключ API?",
|
||||
"wayToGetPagerTreeIntegrationURL": "После создания интеграции Uptime Kuma в PagerTree, скопируйте конечную точку. Смотрите полную информацию {0}",
|
||||
"wayToGetPagerTreeIntegrationURL": "После создания интеграции Uptime Kuma в PagerTree скопируйте файл Endpoint. См. полную информацию {0}",
|
||||
"telegramMessageThreadIDDescription": "Необязательный уникальный идентификатор для цепочки сообщений (темы) форума; только для форумов-супергрупп",
|
||||
"grpcMethodDescription": "Название метода - преобразовать в формат cammelCase, такой как sayHello, check и т.д.",
|
||||
"Proto Service Name": "название службы Proto",
|
||||
"grpcMethodDescription": "Имя метода преобразуется в формат cammelCase, например, sayHello, check и т. д.",
|
||||
"Proto Service Name": "Название службы Proto",
|
||||
"Proto Method": "Метод Proto",
|
||||
"Proto Content": "Содержание Proto",
|
||||
"telegramMessageThreadID": "(Необязательно) ID цепочки сообщений",
|
||||
@ -758,5 +758,40 @@
|
||||
"endDateTime": "Конечная дата и время",
|
||||
"cronExpression": "Выражение для Cron",
|
||||
"cronSchedule": "Расписание: ",
|
||||
"invalidCronExpression": "Неверное выражение Cron: {0}"
|
||||
"invalidCronExpression": "Неверное выражение Cron: {0}",
|
||||
"ntfyUsernameAndPassword": "Логин и пароль",
|
||||
"ntfyAuthenticationMethod": "Способ входа",
|
||||
"Monitor Setting": "Настройка монитора {0}",
|
||||
"Show Clickable Link": "Показать кликабельную ссылку",
|
||||
"Badge Generator": "Генератор значков для {0}",
|
||||
"Badge Type": "Тип значка",
|
||||
"Badge Duration": "Срок действия значка",
|
||||
"Badge Label": "Надпись для значка",
|
||||
"Badge Prefix": "Префикс значка",
|
||||
"Badge Label Color": "Цвет надписи значка",
|
||||
"Badge Color": "Цвет значка",
|
||||
"Badge Label Prefix": "Префикс надписи для значка",
|
||||
"Open Badge Generator": "Открыть генератор значка",
|
||||
"Badge Up Color": "Цвет значка для статуса \"Доступен\"",
|
||||
"Badge Pending Color": "Цвет значка для статуса \"Ожидание\"",
|
||||
"Badge Maintenance Color": "Цвет значка для статуса \"Техобслуживание\"",
|
||||
"Badge Style": "Стиль значка",
|
||||
"Badge Suffix": "Суффикс значка",
|
||||
"Badge value (For Testing only.)": "Значение значка (только для тестирования)",
|
||||
"Badge URL": "URL значка",
|
||||
"Group": "Группа",
|
||||
"Monitor Group": "Группа мониторов",
|
||||
"Show Clickable Link Description": "Если флажок установлен, все, кто имеет доступ к этой странице состояния, могут иметь доступ к URL-адресу монитора.",
|
||||
"pushoverMessageTtl": "TTL сообщения (в секундах)",
|
||||
"Badge Down Color": "Цвет значка для статуса \"Недоступен\"",
|
||||
"Badge Label Suffix": "Суффикс надписи для значка",
|
||||
"Edit Maintenance": "Редактировать техобсоуживание",
|
||||
"Reconnecting...": "Переподключение...",
|
||||
"Cannot connect to the socket server": "Невозможно подключиться к серверу",
|
||||
"Badge Warn Color": "Цвет значка для предупреждения",
|
||||
"Badge Warn Days": "Значок для \"дней предупреждения\"",
|
||||
"Badge Down Days": "Значок для \"дней недоступности\"",
|
||||
"Home": "Главная",
|
||||
"noGroupMonitorMsg": "Не доступно. Создайте сначала группу мониторов.",
|
||||
"Close": "Закрыть"
|
||||
}
|
||||
|
@ -191,5 +191,15 @@
|
||||
"Tag with this name already exist.": "Značka s týmto názvom už existuje.",
|
||||
"Blue": "Modrá",
|
||||
"Search...": "Hľadať…",
|
||||
"statusPageNothing": "Nič tu nie je, pridajte skupinu alebo sledovanie."
|
||||
"statusPageNothing": "Nič tu nie je, pridajte skupinu alebo sledovanie.",
|
||||
"webhookAdditionalHeadersTitle": "Ďalšie položky",
|
||||
"webhookAdditionalHeadersDesc": "Nastaví ďalšie hlavičky odoslané s webovým hákom.",
|
||||
"Webhook URL": "Webhook URL",
|
||||
"Application Token": "Token aplikácie",
|
||||
"Server URL": "Server URL",
|
||||
"Priority": "Priorita",
|
||||
"statusPageRefreshIn": "Obnovenie za: {0}",
|
||||
"emojiCheatSheet": "Emotikony: {0}",
|
||||
"Read more": "Prečítajte si viac",
|
||||
"appriseInstalled": "Apprise je nainštalovaný."
|
||||
}
|
||||
|
@ -105,5 +105,37 @@
|
||||
"Last Result": "Senaste resultat",
|
||||
"Create your admin account": "Skapa ditt administratörskonto",
|
||||
"Repeat Password": "Upprepa Lösenord",
|
||||
"respTime": "Svarstid (ms)"
|
||||
"respTime": "Svarstid (ms)",
|
||||
"Specific Monitor Type": "Applikationsspecifika övervakare",
|
||||
"Push URL": "Push URL",
|
||||
"Passive Monitor Type": "Passiva övervakare",
|
||||
"markdownSupported": "Stödjer markdown-syntax",
|
||||
"Heartbeat Retry Interval": "Omprövningsintervall",
|
||||
"needPushEvery": "Hämta denna URL var {0} sekund",
|
||||
"pushOptionalParams": "Valfria parametrar: {0}",
|
||||
"disableauth.message1": "Vill du verkligen <strong>avaktivera autentisering</strong>?",
|
||||
"disableauth.message2": "Det är designat för när en <strong>tredjeparts autentiseringstjänst</strong> såsom Cloudflare Access eller Authelia används framför Uptime Kuma.",
|
||||
"Please use this option carefully!": "Använd denna funktion varsamt!",
|
||||
"Import Backup": "Importera backup",
|
||||
"Affected Monitors": "Påverkade övervakare",
|
||||
"Start of maintenance": "Påbörja underhåll",
|
||||
"All Status Pages": "Alla statussidor",
|
||||
"alertNoFile": "Välj en fil att importera.",
|
||||
"alertWrongFileType": "Välj en JSON-formatterad fil.",
|
||||
"Help": "Hjälp",
|
||||
"Export": "Export",
|
||||
"Import": "Import",
|
||||
"Game": "Spel",
|
||||
"resendEveryXTimes": "Omsänd efter {0} gånger",
|
||||
"Export Backup": "Exportera backup",
|
||||
"Schedule maintenance": "Schemalägg underhåll",
|
||||
"Monitor": "Övervakare | Övervakare",
|
||||
"Resend Notification if Down X times consecutively": "Sänd notis igen om nere X gånger i rad",
|
||||
"Maintenance": "Underhåll",
|
||||
"retryCheckEverySecond": "Ompröva var {0} sekund",
|
||||
"statusMaintenance": "Underhåll",
|
||||
"resendDisabled": "Omsändning inaktiverat",
|
||||
"Pick Affected Monitors...": "Välj påverkade övervakare…",
|
||||
"Select status pages...": "Välj statussidor…",
|
||||
"General Monitor Type": "Allmänna övervakare"
|
||||
}
|
||||
|
@ -214,7 +214,7 @@
|
||||
"smtpBCC": "BCC",
|
||||
"discord": "Discord",
|
||||
"Discord Webhook URL": "Discord Webhook URL",
|
||||
"wayToGetDiscordURL": "คุณสามารถรับได้โดยการไปที่ Server Settings -> Integrations -> Create Webhook",
|
||||
"wayToGetDiscordURL": "คุณสามารถทำได้โดยการไปที่ Server Settings -> Integrations -> Create Webhook",
|
||||
"Bot Display Name": "ชื่อบอท",
|
||||
"Prefix Custom Message": "คำนำหน้าข้อความที่กำหนดเอง",
|
||||
"Hello @everyone is...": "สวัสดี {'@'}everyone นี่…",
|
||||
@ -605,5 +605,70 @@
|
||||
"pagertreeCritical": "วิกฤต",
|
||||
"pagertreeDoNothing": "ไม่ต้องทำอะไร",
|
||||
"pagertreeResolve": "แก้ไขอัตโนมัติ",
|
||||
"wayToGetPagerTreeIntegrationURL": "หลังจากสร้างการรวม Uptime Kuma ใน PagerTree แล้ว ให้คัดลอก Endpoint, ดูรายละเอียดทั้งหมด {0}"
|
||||
"wayToGetPagerTreeIntegrationURL": "หลังจากสร้างการรวม Uptime Kuma ใน PagerTree แล้ว ให้คัดลอก Endpoint, ดูรายละเอียดทั้งหมด {0}",
|
||||
"telegramSendSilently": "ส่งอย่างเงียบ ๆ",
|
||||
"maintenanceStatus-inactive": "ไม่ใช้งาน",
|
||||
"telegramProtectContent": "ป้องกันการส่งต่อ/บันทึก",
|
||||
"Add New Tag": "เพิ่มแท็กใหม่",
|
||||
"strategyManual": "ตั่งให้ใช้งาน/ไม่ใช้งานด้วยตนเอง",
|
||||
"warningTimezone": "ใช้เขตเวลาของเซิร์ฟเวอร์",
|
||||
"weekdayShortMon": "จันทร์",
|
||||
"weekdayShortTue": "วันอังคาร",
|
||||
"weekdayShortWed": "พุธ",
|
||||
"weekdayShortThu": "พฤหัสบดี",
|
||||
"weekdayShortFri": "ศุกร์",
|
||||
"weekdayShortSat": "เสาร์",
|
||||
"weekdayShortSun": "อาทิตย์",
|
||||
"dayOfWeek": "วันในสัปดาห์",
|
||||
"dayOfMonth": "วันในเดือน",
|
||||
"maintenanceStatus-under-maintenance": "อยู่ภายใต้การบำรุงรักษา",
|
||||
"maintenanceStatus-scheduled": "กำหนดการ",
|
||||
"maintenanceStatus-ended": "สิ้นสุด",
|
||||
"maintenanceStatus-unknown": "ไม่ทราบ",
|
||||
"Specific Monitor Type": "ประเภทมอนิเตอร์เฉพาะ",
|
||||
"telegramMessageThreadID": "(ตัวเลือก) ไอดีเทรดข้อความ",
|
||||
"telegramMessageThreadIDDescription": "ตัวระบุที่ไม่ซ้ำซึ่งเป็นทางเลือกสำหรับเธรดข้อความเป้าหมาย (หัวข้อ) ของฟอรัม สำหรับฟอรัมซูเปอร์กรุ๊ปเท่านั้น",
|
||||
"sameAsServerTimezone": "เช่นเดียวกับเขตเวลาของเซิร์ฟเวอร์",
|
||||
"startDateTime": "วันที่/เวลาเริ่มต้น",
|
||||
"endDateTime": "วันที่/เวลาสิ้นสุด",
|
||||
"cronSchedule": "กำหนดการ: ",
|
||||
"invalidCronExpression": "นิพจน์ Cron ไม่ถูกต้อง: {0}",
|
||||
"cronExpression": "นิพจน์ Cron",
|
||||
"lastDay": "วันสุดท้าย",
|
||||
"lastDay1": "วันสุดท้ายของเดือน",
|
||||
"lastDay2": "วันที่ 2 สุดท้ายของเดือน",
|
||||
"lastDay3": "วันที่ 3 สุดท้ายของเดือน",
|
||||
"lastDay4": "วันที่ 4 สุดท้ายของเดือน",
|
||||
"No Maintenance": "ไม่มีการบำรุงรักษา",
|
||||
"pauseMaintenanceMsg": "แน่ใจไหมว่าต้องการหยุดชั่วคราว",
|
||||
"Display Timezone": "แสดงเขตเวลา",
|
||||
"statusPageMaintenanceEndDate": "จบ",
|
||||
"Server Timezone": "เขตเวลาเซิร์ฟเวอร์",
|
||||
"statusPageRefreshIn": "รีโหลดใน: {0}",
|
||||
"telegramSendSilentlyDescription": "ส่งข้อความอย่างเงียบๆ ผู้ใช้จะได้รับการแจ้งเตือนโดยไม่มีเสียง",
|
||||
"telegramProtectContentDescription": "หากเปิดใช้งาน ข้อความบอทใน Telegram จะได้รับการปกป้องจากการส่งต่อและการบันทึก",
|
||||
"dnsCacheDescription": "อาจจะทำงานไม่ได้กับ IPv6, ปิดใช้งานถ้าเจอปัญหา",
|
||||
"IconUrl": "URL ไอคอน",
|
||||
"Enable DNS Cache": "เปิดใช้งาน DNS Cache",
|
||||
"Enable": "เปิดใช้งาน",
|
||||
"Disable": "ปิดใช้งาน",
|
||||
"Single Maintenance Window": "หน้าการปรับปรุงเดี่ยว",
|
||||
"Clone Monitor": "มอนิเตอร์",
|
||||
"Clone": "โคลนมอนิเตอร์",
|
||||
"cloneOf": "ชื่อเล่นมอนิเตอร์",
|
||||
"wayToGetZohoCliqURL": "คุณสามารถดูวิธีการสร้าง Webhook URL {0}",
|
||||
"Cannot connect to the socket server": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ Socket",
|
||||
"Reconnecting...": "กำลังเชื่อมต่อใหม่",
|
||||
"Home": "หน้าหลัก",
|
||||
"Date and Time": "วันที่และเวลา",
|
||||
"DateTime Range": "ช่วงวันที่และเวลา",
|
||||
"loadingError": "ไม่สามารถดึงข้อมูลได้ โปรดลองอีกครั้งในภายหลัง",
|
||||
"plugin": "ปลั้กอิน | ปลั้กอิน",
|
||||
"install": "ติดตั้ง",
|
||||
"installing": "กำลังติดตั้ง",
|
||||
"uninstall": "ถอนการติดตั้ง",
|
||||
"uninstalling": "กำลังถอนการติดตั้ง",
|
||||
"confirmUninstallPlugin": "แน่ใจหรือไม่ว่าต้องการถอนการติดตั้งปลั้กอินนี้?",
|
||||
"Schedule Maintenance": "กำหนดเวลาซ่อมแซม",
|
||||
"Edit Maintenance": "แก้ใขการบำรุงรักษา"
|
||||
}
|
||||
|
@ -58,7 +58,7 @@
|
||||
"Delete": "Sil",
|
||||
"Current": "Şu anda",
|
||||
"Uptime": "Çalışma zamanı",
|
||||
"Cert Exp.": "Sertifika Süresi",
|
||||
"Cert Exp.": "Sertifika Geç. Süresi",
|
||||
"day": "gün | günler",
|
||||
"-day": "-gün",
|
||||
"hour": "saat",
|
||||
@ -194,7 +194,7 @@
|
||||
"here": "burada",
|
||||
"Required": "Gerekli",
|
||||
"telegram": "Telegram",
|
||||
"Bot Token": "Bot Token",
|
||||
"Bot Token": "Bot Anahtarı",
|
||||
"wayToGetTelegramToken": "{0} adresinden bir token alabilirsiniz.",
|
||||
"Chat ID": "Chat ID",
|
||||
"supportTelegramChatID": "Doğrudan Sohbet / Grup / Kanalın Sohbet Kimliğini Destekleyin",
|
||||
@ -216,8 +216,8 @@
|
||||
"smtpCC": "CC",
|
||||
"smtpBCC": "BCC",
|
||||
"discord": "Discord",
|
||||
"Discord Webhook URL": "Discord Webhook URL",
|
||||
"wayToGetDiscordURL": "Bunu Sunucu Ayarları -> Entegrasyonlar -> Webhookları Görüntüle -> Yeni Webhook Oluştur adımını izleyerek alabilirsiniz.",
|
||||
"Discord Webhook URL": "Discord Webhook Bağlantısı",
|
||||
"wayToGetDiscordURL": "Bunu Sunucu Ayarları -> Entegrasyonlar -> Webhookları Görüntüle -> Yeni Webhook Oluştur adımını izleyerek alabilirsiniz",
|
||||
"Bot Display Name": "Botun Görünecek Adı",
|
||||
"Prefix Custom Message": "Önek Özel Mesaj",
|
||||
"Hello @everyone is...": "Merhaba {'@'}everyone…",
|
||||
@ -262,7 +262,7 @@
|
||||
"octopushPhoneNumber": "Telefon numarası (uluslararası biçim, örneğin: +33612345678) ",
|
||||
"octopushSMSSender": "SMS Gönderici Adı : 3-11 alfanümerik karakter ve boşluk (a-zA-Z0-9)",
|
||||
"LunaSea Device ID": "LunaSea Cihaz ID",
|
||||
"Apprise URL": "Apprise URL",
|
||||
"Apprise URL": "Apprise Bağlantısı",
|
||||
"Example:": "Örnek: {0}",
|
||||
"Read more:": "Daha fazla oku: {0}",
|
||||
"Status:": "Durum: {0}",
|
||||
@ -335,7 +335,7 @@
|
||||
"Please input title and content": "Lütfen başlık ve içerik girin",
|
||||
"Created": "Oluşturuldu",
|
||||
"Last Updated": "Son Güncelleme",
|
||||
"Unpin": "Unpin",
|
||||
"Unpin": "Sabitlemeyi Kaldır",
|
||||
"Switch to Light Theme": "Açık Temaya Geç",
|
||||
"Switch to Dark Theme": "Karanlık Temaya Geç",
|
||||
"Show Tags": "Etiketleri Göster",
|
||||
@ -395,7 +395,7 @@
|
||||
"Valid": "Geçerli",
|
||||
"Invalid": "Geçersiz",
|
||||
"AccessKeyId": "AccessKey ID",
|
||||
"SecretAccessKey": "AccessKey Secret",
|
||||
"SecretAccessKey": "AccessKey Gizli Anahtarı",
|
||||
"PhoneNumbers": "Telefon numaraları",
|
||||
"TemplateCode": "TemplateCode",
|
||||
"SignName": "SignName",
|
||||
@ -414,7 +414,7 @@
|
||||
"High": "High",
|
||||
"Retry": "Tekrar",
|
||||
"Topic": "Başlık",
|
||||
"WeCom Bot Key": "WeCom Bot Key",
|
||||
"WeCom Bot Key": "WeCom Bot Anahtarı",
|
||||
"Setup Proxy": "Proxy kur",
|
||||
"Proxy Protocol": "Proxy Protokolü",
|
||||
"Proxy Server": "Proxy Sunucusu",
|
||||
@ -444,7 +444,7 @@
|
||||
"Backup": "Yedek",
|
||||
"About": "Hakkında",
|
||||
"wayToGetCloudflaredURL": "(Cloudflared'i {0} adresinden indirin)",
|
||||
"cloudflareWebsite": "Cloudflare Website",
|
||||
"cloudflareWebsite": "Cloudflare İnt. Sitesi",
|
||||
"Message:": "Mesaj:",
|
||||
"Don't know how to get the token? Please read the guide:": "Tokeni nasıl alacağınızı bilmiyor musunuz? Lütfen kılavuzu okuyun:",
|
||||
"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.": "Halihazırda Cloudflare Tüneli üzerinden bağlanıyorsanız mevcut bağlantı kesilebilir. Durdurmak istediğinden emin misin? Onaylamak için mevcut şifrenizi yazın.",
|
||||
@ -475,7 +475,7 @@
|
||||
"Domain Names": "Alan isimleri",
|
||||
"signedInDisp": "{0} olarak oturum açıldı",
|
||||
"signedInDispDisabled": "Yetkilendirme Devre Dışı.",
|
||||
"RadiusSecret": "Radius Secret",
|
||||
"RadiusSecret": "Radius Gizli Anahtar",
|
||||
"RadiusSecretDescription": "İstemci ve sunucu arasında paylaşılan gizli anahtar",
|
||||
"RadiusCalledStationId": "Aranan İstasyon Kimliği",
|
||||
"RadiusCalledStationIdDescription": "Aranan cihazın tanımlayıcısı",
|
||||
@ -547,13 +547,13 @@
|
||||
"Docker Host": "Docker Ana Bilgisayarı",
|
||||
"Docker Hosts": "Docker Ana Bilgisayarları",
|
||||
"ntfy Topic": "ntfy Konu",
|
||||
"Domain": "Domain",
|
||||
"Domain": "Alan Adı",
|
||||
"Workstation": "İş İstasyonu",
|
||||
"disableCloudflaredNoAuthMsg": "Yetki yok modundasınız, şifre gerekli değil.",
|
||||
"trustProxyDescription": "'X-Forwarded-*' başlıklarına güvenin. Doğru istemci IP'sini almak istiyorsanız ve Uptime Kuma'nız Nginx veya Apache gibi bir proxy'nin arkasındaysa, bunu etkinleştirmelisiniz.",
|
||||
"wayToGetLineNotifyToken": "{0} adresinden bir erişim jetonu alabilirsiniz.",
|
||||
"wayToGetLineNotifyToken": "{0} adresinden bir erişim jetonu alabilirsiniz",
|
||||
"Examples": "Örnekler",
|
||||
"Home Assistant URL": "Home Assistant URL",
|
||||
"Home Assistant URL": "Home Assistant Bağlantısı",
|
||||
"Long-Lived Access Token": "Long-Lived Erişim Anahtarı",
|
||||
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Long-Lived Erişim Anahtarı, profil adınıza (sol altta) tıklayarak ve aşağıya kaydırarak ve ardından Anahtar Oluştur'a tıklayarak oluşturulabilir. ",
|
||||
"Notification Service": "Bildirim Hizmeti",
|
||||
@ -749,5 +749,40 @@
|
||||
"endDateTime": "Bitiş Tarihi/Saati",
|
||||
"cronExpression": "Cron İfadesi",
|
||||
"cronSchedule": "Zamanlama: ",
|
||||
"invalidCronExpression": "Geçersiz Cron İfadesi: {0}"
|
||||
"invalidCronExpression": "Geçersiz Cron İfadesi: {0}",
|
||||
"ntfyAuthenticationMethod": "Kimlik Doğrulama Yöntemi",
|
||||
"ntfyUsernameAndPassword": "Kullanıcı adı ve şifre",
|
||||
"pushoverMessageTtl": "Mesajın Yaşama Süresi (Saniye)",
|
||||
"Show Clickable Link": "Tıklanabilir Bağlantıyı Göster",
|
||||
"Open Badge Generator": "Rozet Oluşturucuyu Aç",
|
||||
"Badge Generator": "{0} Rozet Oluşturucu",
|
||||
"Badge Type": "Rozet Türü",
|
||||
"Badge Duration": "Rozet Süresi",
|
||||
"Badge Label": "Rozet Etiketi",
|
||||
"Badge Prefix": "Rozet Öneki",
|
||||
"Badge Suffix": "Rozet Eki",
|
||||
"Badge Label Color": "Rozet Etiket Rengi",
|
||||
"Badge Color": "Rozet Rengi",
|
||||
"Badge Label Prefix": "Rozet Etiket Öneki",
|
||||
"Badge Label Suffix": "Rozet Etiket Eki",
|
||||
"Badge Up Color": "Rozet Normal Rengi",
|
||||
"Badge Down Color": "Rozet Hatalı Rengi",
|
||||
"Badge Pending Color": "Rozet Bekleyen Rengi",
|
||||
"Badge Maintenance Color": "Rozet Bakım Rengi",
|
||||
"Badge Warn Color": "Rozet Uyarı Rengi",
|
||||
"Badge Warn Days": "Rozet Uyarı Günleri",
|
||||
"Badge Down Days": "Rozet Hatalı Günleri",
|
||||
"Badge Style": "Rozet Stili",
|
||||
"Badge value (For Testing only.)": "Rozet değeri (Yalnızca Test için.)",
|
||||
"Badge URL": "Rozet URL'i",
|
||||
"Monitor Setting": "{0}'nin Monitör Ayarı",
|
||||
"Show Clickable Link Description": "Eğer işaretlenirse, bu durum sayfasına erişimi olan herkes monitor URL'ine erişebilir.",
|
||||
"Group": "Grup",
|
||||
"Monitor Group": "Monitor Grup",
|
||||
"Cannot connect to the socket server": "Soket sunucusuna bağlanılamıyor",
|
||||
"Edit Maintenance": "Bakımı Düzenle",
|
||||
"Reconnecting...": "Yeniden bağlanılıyor...",
|
||||
"Home": "Anasayfa",
|
||||
"noGroupMonitorMsg": "Uygun değil. Önce bir Grup Monitörü oluşturun.",
|
||||
"Close": "Kapalı"
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
"rrtypeDescription": "Виберіть тип ресурсного запису, який ви хочете відстежувати",
|
||||
"pauseMonitorMsg": "Ви дійсно хочете поставити на паузу?",
|
||||
"Settings": "Налаштування",
|
||||
"Dashboard": "Панель управління",
|
||||
"Dashboard": "Панель керування",
|
||||
"New Update": "Оновлення",
|
||||
"Language": "Мова",
|
||||
"Appearance": "Зовнішній вигляд",
|
||||
@ -120,7 +120,7 @@
|
||||
"Heartbeats": "Опитування",
|
||||
"Auto Get": "Авто-отримання",
|
||||
"enableDefaultNotificationDescription": "Для кожного нового монітора це сповіщення буде включено за замовчуванням. Ви все ще можете відключити сповіщення в кожному моніторі окремо.",
|
||||
"Default enabled": "Використовувати за промовчанням",
|
||||
"Default enabled": "Використовувати за замовчуванням",
|
||||
"Also apply to existing monitors": "Застосувати до існуючих моніторів",
|
||||
"Export": "Експорт",
|
||||
"Import": "Імпорт",
|
||||
@ -270,7 +270,7 @@
|
||||
"octopushPhoneNumber": "Номер телефону (між. формат, наприклад: +380123456789) ",
|
||||
"octopushSMSSender": "Ім'я відправника SMS: 3-11 символів алвафіту, цифр та пробілів (a-zA-Z0-9)",
|
||||
"LunaSea Device ID": "ID пристрою LunaSea",
|
||||
"Apprise URL": "Apprise URL",
|
||||
"Apprise URL": "Apprise URL-адреса",
|
||||
"Example:": "Приклад: {0}",
|
||||
"Read more:": "Докладніше: {0}",
|
||||
"Status:": "Статус: {0}",
|
||||
@ -462,7 +462,7 @@
|
||||
"onebotMessageType": "OneBot тип повідомлення",
|
||||
"onebotGroupMessage": "Група",
|
||||
"onebotPrivateMessage": "Приватне",
|
||||
"onebotUserOrGroupId": "Група/Користувач ID",
|
||||
"onebotUserOrGroupId": "Група/ID користувача",
|
||||
"onebotSafetyTips": "Для безпеки необхідно встановити маркер доступу",
|
||||
"PushDeer Key": "PushDeer ключ",
|
||||
"Footer Text": "Текст нижнього колонтитула",
|
||||
@ -477,35 +477,35 @@
|
||||
"From Name/Number": "Від Ім'я/Номер",
|
||||
"Leave blank to use a shared sender number.": "Залиште поле порожнім, щоб використовувати спільний номер відправника.",
|
||||
"Octopush API Version": "Octopush API версія",
|
||||
"Legacy Octopush-DM": "Legacy Octopush-DM",
|
||||
"Legacy Octopush-DM": "Застарілий Octopush-DM",
|
||||
"endpoint": "кінцева точка",
|
||||
"octopushAPIKey": "\"Ключ API\" з облікових даних HTTP API в панелі керування",
|
||||
"octopushLogin": "\"Ім'я користувача\" з облікових даних HTTP API на панелі керування",
|
||||
"promosmsLogin": "API Логін",
|
||||
"promosmsPassword": "API Пароль",
|
||||
"pushoversounds pushover": "Pushover (по замовчуванню)",
|
||||
"pushoversounds bike": "Bike",
|
||||
"pushoversounds bugle": "Bugle",
|
||||
"pushoversounds cashregister": "Cash Register",
|
||||
"pushoversounds bike": "Велосипед",
|
||||
"pushoversounds bugle": "Горн",
|
||||
"pushoversounds cashregister": "Касовий апарат",
|
||||
"pushoversounds classical": "Classical",
|
||||
"pushoversounds cosmic": "Cosmic",
|
||||
"pushoversounds falling": "Falling",
|
||||
"pushoversounds gamelan": "Gamelan",
|
||||
"pushoversounds incoming": "Incoming",
|
||||
"pushoversounds intermission": "Intermission",
|
||||
"pushoversounds magic": "Magic",
|
||||
"pushoversounds mechanical": "Mechanical",
|
||||
"pushoversounds pianobar": "Piano Bar",
|
||||
"pushoversounds siren": "Siren",
|
||||
"pushoversounds spacealarm": "Space Alarm",
|
||||
"pushoversounds tugboat": "Tug Boat",
|
||||
"pushoversounds alien": "Alien Alarm (long)",
|
||||
"pushoversounds climb": "Climb (long)",
|
||||
"pushoversounds persistent": "Persistent (long)",
|
||||
"pushoversounds echo": "Pushover Echo (long)",
|
||||
"pushoversounds updown": "Up Down (long)",
|
||||
"pushoversounds vibrate": "Vibrate Only",
|
||||
"pushoversounds none": "None (silent)",
|
||||
"pushoversounds falling": "Падіння",
|
||||
"pushoversounds gamelan": "Гамелан",
|
||||
"pushoversounds incoming": "Вхідний",
|
||||
"pushoversounds intermission": "Антракт",
|
||||
"pushoversounds magic": "Магія",
|
||||
"pushoversounds mechanical": "Механічний",
|
||||
"pushoversounds pianobar": "Піано-бар",
|
||||
"pushoversounds siren": "Сирена",
|
||||
"pushoversounds spacealarm": "Космічна тривога",
|
||||
"pushoversounds tugboat": "Буксирний катер",
|
||||
"pushoversounds alien": "Тривога прибульців (довга)",
|
||||
"pushoversounds climb": "Підйом (довгий)",
|
||||
"pushoversounds persistent": "Стійкий (довгий)",
|
||||
"pushoversounds echo": "Pushover ехо (довгий)",
|
||||
"pushoversounds updown": "Вгору вниз (довгий)",
|
||||
"pushoversounds vibrate": "Тільки вібрація",
|
||||
"pushoversounds none": "Нічого (тиша)",
|
||||
"pushyAPIKey": "Секретний ключ API",
|
||||
"pushyToken": "Токен пристрою",
|
||||
"Using a Reverse Proxy?": "Використовувати зворотній проксі?",
|
||||
@ -587,7 +587,7 @@
|
||||
"weekdayShortSun": "Нд",
|
||||
"Single Maintenance Window": "Разове технічне обслуговування",
|
||||
"Maintenance Time Window of a Day": "Період доби для технічного обслуговування",
|
||||
"Effective Date Range": "Діапазон дат вступу в силу",
|
||||
"Effective Date Range": "Діапазон дат вступу в силу (необов'язково)",
|
||||
"Schedule Maintenance": "Розклад обслуговування",
|
||||
"DateTime Range": "Діапазон дат і часу",
|
||||
"loadingError": "Не вдалося отримати дані, спробуйте пізніше.",
|
||||
@ -744,5 +744,51 @@
|
||||
"lunaseaTarget": "Ціль",
|
||||
"Add New Tag": "Додати новий тег",
|
||||
"lunaseaDeviceID": "ID пристрою",
|
||||
"lunaseaUserID": "ID користувача"
|
||||
"lunaseaUserID": "ID користувача",
|
||||
"twilioAccountSID": "SID облікового запису",
|
||||
"twilioAuthToken": "Токен авторизації",
|
||||
"twilioFromNumber": "З номера",
|
||||
"twilioToNumber": "На номер",
|
||||
"sameAsServerTimezone": "Такий самий, як часовий пояс сервера",
|
||||
"startDateTime": "Дата і час початку",
|
||||
"endDateTime": "Дата і час закінчення",
|
||||
"cronExpression": "Cron-вираз",
|
||||
"cronSchedule": "Розклад: ",
|
||||
"invalidCronExpression": "Неправильний Cron-вираз: {0}",
|
||||
"statusPageRefreshIn": "Оновлювати кожні: {0}",
|
||||
"ntfyAuthenticationMethod": "Метод автентифікації",
|
||||
"ntfyUsernameAndPassword": "Ім'я користувача та пароль",
|
||||
"pushoverMessageTtl": "TTL повідомлення (секунди)",
|
||||
"Monitor Setting": "Налаштування монітора {0}",
|
||||
"Show Clickable Link": "Показувати клікабельне посилання",
|
||||
"Show Clickable Link Description": "Якщо позначено, кожен, хто має доступ до цієї сторінки статусу, може мати доступ до URL-адреси моніторингу.",
|
||||
"Open Badge Generator": "Відкрити генератор бейджів",
|
||||
"Badge Generator": "Генератор бейджів {0}",
|
||||
"Badge Type": "Тип бейджа",
|
||||
"Badge Duration": "Тривалість бейджа",
|
||||
"Badge Label": "Ярлик бейджа",
|
||||
"Badge Prefix": "Префікс бейджа",
|
||||
"Badge Suffix": "Суфікс бейджа",
|
||||
"Badge Label Color": "Колір ярлика бейджа",
|
||||
"Badge Color": "Колір бейджа",
|
||||
"Badge Label Prefix": "Префікс ярлика бейджа",
|
||||
"Badge Label Suffix": "Суфікс ярлика бейджа",
|
||||
"Badge Style": "Стиль бейджа",
|
||||
"Badge value (For Testing only.)": "Значення бейджа (тільки для тестування.)",
|
||||
"Badge URL": "URL бейджа",
|
||||
"Badge Up Color": "Колір бейджа \"Доступний\"",
|
||||
"Badge Down Color": "Колір бейджа \"Недоступний\"",
|
||||
"Badge Pending Color": "Колір бейджа \"Очікування\"",
|
||||
"Badge Warn Color": "Колір бейджа \"Попередження\"",
|
||||
"Badge Warn Days": "Бейдж \"Днів попередження\"",
|
||||
"Badge Maintenance Color": "Колір бейджа \"Обслуговування\"",
|
||||
"Badge Down Days": "Бейдж \"Днів недоступний\"",
|
||||
"Group": "Група",
|
||||
"Monitor Group": "Група моніторів",
|
||||
"Edit Maintenance": "Редагувати обслуговування",
|
||||
"Cannot connect to the socket server": "Не вдається підключитися до сервера сокетів",
|
||||
"Reconnecting...": "Повторне підключення...",
|
||||
"Home": "Головна",
|
||||
"noGroupMonitorMsg": "Недоступно. Спочатку створіть групу моніторів.",
|
||||
"Close": "Закрити"
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
{
|
||||
"languageName": "Tiếng Việt",
|
||||
"checkEverySecond": "Kiểm tra mỗi {0} giây.",
|
||||
"retryCheckEverySecond": "Thử lại mỗi {0} giây.",
|
||||
"retriesDescription": "Số lần thử lại tối đa trước khi dịch vụ được đánh dấu là down và gửi thông báo.",
|
||||
"ignoreTLSError": "Bỏ qua lỗi TLS/SSL với các web HTTPS.",
|
||||
"upsideDownModeDescription": "Trạng thái đảo ngược, nếu dịch vụ có thể truy cập được nghĩa là DOWN.",
|
||||
"maxRedirectDescription": "Số lần chuyển hướng (redirect) tối đa. Đặt thành 0 để tắt chuyển hướng",
|
||||
"checkEverySecond": "Kiểm tra mỗi {0} giây",
|
||||
"retryCheckEverySecond": "Thử lại mỗi {0} giây",
|
||||
"retriesDescription": "Số lần thử lại tối đa trước khi dịch vụ được đánh dấu là down và gửi thông báo",
|
||||
"ignoreTLSError": "Bỏ qua lỗi TLS/SSL với các web HTTPS",
|
||||
"upsideDownModeDescription": "Chế độ đảo ngược, nếu dịch vụ có thể truy cập được nghĩa là DOWN.",
|
||||
"maxRedirectDescription": "Số lần chuyển hướng (redirect) tối đa. Đặt thành 0 để tắt chuyển hướng.",
|
||||
"acceptedStatusCodesDescription": "Chọn mã trạng thái được coi là phản hồi thành công.",
|
||||
"passwordNotMatchMsg": "Mật khẩu nhập lại không khớp.",
|
||||
"notificationDescription": "Vui lòng chỉ định một kênh thông báo.",
|
||||
@ -27,7 +27,7 @@
|
||||
"confirmEnableTwoFAMsg": "Bạn chắc chắn muốn bật xác thực 2 lớp (2FA) chứ?",
|
||||
"confirmDisableTwoFAMsg": "Bạn chắc chắn muốn tắt xác thực 2 lớp (2FA) chứ?",
|
||||
"Settings": "Cài đặt",
|
||||
"Dashboard": "Dashboard",
|
||||
"Dashboard": "Trang tổng quan",
|
||||
"New Update": "Bản cập nhật mới",
|
||||
"Language": "Ngôn ngữ",
|
||||
"Appearance": "Giao diện",
|
||||
@ -102,10 +102,10 @@
|
||||
"Enable Auth": "Bật xác minh",
|
||||
"disableauth.message1": "Bạn có muốn <strong>TẮT XÁC THỰC</strong> không?",
|
||||
"disableauth.message2": "Điều này rất nguy hiểm<strong>BẤT KỲ AI</strong> cũng có thể truy cập và cướp quyền điều khiển.",
|
||||
"Please use this option carefully!": "Vui lòng <strong>cẩn thận</strong>.",
|
||||
"Please use this option carefully!": "Vui lòng <strong>cẩn thận</strong>!",
|
||||
"Logout": "Đăng xuất",
|
||||
"Leave": "Rời",
|
||||
"I understand, please disable": "Tôi hiểu, làm ơn hãy tắt!",
|
||||
"I understand, please disable": "Tôi hiểu, làm ơn hãy tắt",
|
||||
"Confirm": "Xác nhận",
|
||||
"Yes": "Có",
|
||||
"No": "Không",
|
||||
@ -158,11 +158,11 @@
|
||||
"Token": "Token",
|
||||
"Show URI": "Hiển thị URI",
|
||||
"Tags": "Tags",
|
||||
"Add New below or Select...": "Thêm mới ở dưới hoặc Chọn...",
|
||||
"Tag with this name already exist.": "Tag với tên đã tồn tại.",
|
||||
"Tag with this value already exist.": "Tag với value đã tồn tại.",
|
||||
"Add New below or Select...": "Thêm mới ở dưới hoặc Chọn…",
|
||||
"Tag with this name already exist.": "Tag với tên này đã tồn tại.",
|
||||
"Tag with this value already exist.": "Tag với giá trị này đã tồn tại.",
|
||||
"color": "Màu sắc",
|
||||
"value (optional)": "Value (tuỳ chọn)",
|
||||
"value (optional)": "Giá trị (tuỳ chọn)",
|
||||
"Gray": "Xám",
|
||||
"Red": "Đỏ",
|
||||
"Orange": "Cam",
|
||||
@ -171,7 +171,7 @@
|
||||
"Indigo": "Chàm",
|
||||
"Purple": "Tím",
|
||||
"Pink": "Hồng",
|
||||
"Search...": "Tìm kiếm...",
|
||||
"Search...": "Tìm kiếm…",
|
||||
"Avg. Ping": "Ping trung bình",
|
||||
"Avg. Response": "Phản hồi trung bình",
|
||||
"Entry Page": "Entry Page",
|
||||
@ -459,5 +459,37 @@
|
||||
"onebotGroupMessage": "Group",
|
||||
"onebotPrivateMessage": "Private",
|
||||
"onebotUserOrGroupId": "Group/User ID",
|
||||
"onebotSafetyTips": "Để đảm bảo an toàn, hãy thiết lập access token"
|
||||
"onebotSafetyTips": "Để đảm bảo an toàn, hãy thiết lập access token",
|
||||
"Custom": "Tùy chỉnh",
|
||||
"Add New Tag": "Thêm thẻ mới",
|
||||
"webhookAdditionalHeadersDesc": "Đặt header bổ sung được gửi cùng với webhook.",
|
||||
"error": "lỗi",
|
||||
"HTTP Headers": "HTTP Headers",
|
||||
"recurringIntervalMessage": "Chạy một lần mỗi ngày | Chạy một lần mỗi {0} ngày",
|
||||
"Retype the address.": "Nhập lại địa chỉ.",
|
||||
"enableGRPCTls": "Cho phép gửi yêu cầu gRPC với kết nối TLS",
|
||||
"affectedMonitorsDescription": "Chọn kênh theo dõi bị ảnh hưởng bởi lịch bảo trì này",
|
||||
"statusMaintenance": "Bảo trì",
|
||||
"Maintenance": "Bảo trì",
|
||||
"Affected Monitors": "Kênh theo dõi bị ảnh hưởng",
|
||||
"Schedule maintenance": "Thêm lịch bảo trì",
|
||||
"markdownSupported": "Có hỗ trợ Markdown",
|
||||
"Start of maintenance": "Bắt đầu bảo trì",
|
||||
"All Status Pages": "Tất cả các trang trạng thái",
|
||||
"Select status pages...": "Chọn trang trạng thái…",
|
||||
"Certificate Expiry Notification": "Thông báo hết hạn chứng chỉ",
|
||||
"Show update if available": "Hiển thị cập nhật (nếu có)",
|
||||
"What you can try:": "Bạn có thể thử:",
|
||||
"trustProxyDescription": "Tin tưởng các header 'X-Forwarded-*'. Nếu bạn muốn lấy đúng IP máy khách và Uptime Kuma của bạn đứng sau một proxy như Nginx hoặc Apache, bạn nên kích hoạt tính năng này.",
|
||||
"webhookAdditionalHeadersTitle": "Header bổ sung",
|
||||
"Help": "Trợ giúp",
|
||||
"Game": "Trò chơi",
|
||||
"Pick Affected Monitors...": "Chọn kênh theo dõi…",
|
||||
"statusPageRefreshIn": "Làm mới trong: {0}",
|
||||
"Authentication": "Xác thực",
|
||||
"Using a Reverse Proxy?": "Bạn đang sử dụng Reverse Proxy?",
|
||||
"Check how to config it for WebSocket": "Kiểm tra cách cấu hình nó cho WebSocket",
|
||||
"Go back to the previous page.": "Quay trở lại trang trước.",
|
||||
"wayToGetLineNotifyToken": "Bạn có thể lấy access token từ {0}",
|
||||
"Resend Notification if Down X times consecutively": "Gửi lại thông báo nếu Down X lần liên tiếp"
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user