Merge branch 'master' into #1209-Logout-button-in-navbar

pull/1229/head
Matthew Nickson 2 years ago committed by GitHub
commit 88604845e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,22 +0,0 @@
name: 'Automatically close stale issues and PRs'
on:
schedule:
- cron: '0 0 * * *'
#Run once a day at midnight
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
with:
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.'
days-before-stale: 180
days-before-close: 7
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,'
exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,'
exempt-issue-assignees: 'louislam'
exempt-pr-assignees: 'louislam'

@ -27,9 +27,20 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
## Can I create a pull request for Uptime Kuma?
Generally, if the pull request is working fine, and it does not affect any existing logic, workflow and performance, I will merge into the master branch once it is tested.
⚠️ 2022-03-02 Update:
If you are not sure whether I will accept your pull request, feel free to create an empty pull request draft first.
Since I found that merging pull requests is a pretty heavy task for me, I try to rearrange it.
✅ Accept:
- Bug/Security fix
- Translations
- Adding notification providers
❌ Avoid:
- Large pull requests
- New big features
My long story here: https://www.reddit.com/r/UptimeKuma/comments/t1t6or/comment/hynyijx/
### Recommended Pull Request Guideline
@ -43,42 +54,6 @@ If you are not sure whether I will accept your pull request, feel free to create
1. Write a proper description
1. Click "Change to draft"
### Pull Request Examples
Here are some example situations in the past.
#### ✅ High - Medium Priority
Easy to review, no breaking change and not touching the existing code
- Add a new notification
- Add a chart
- Fix a bug
- Translations
- Add a independent new feature
#### *️⃣ Requires one more reviewer
I do not have such knowledge to test it.
- Add k8s supports
#### ⚠ Low Priority - Harsh Mode
Some pull requests are required to modify the core. To be honest, I do not want anyone to try to do that, because it would spend a lot of your time. I will review your pull request harshly. Also, you may need to write a lot of unit tests to ensure that there is no breaking change.
- Touch large parts of code of any very important features
- Touch monitoring logic
- Drop a table or drop a column for any reason
- Touch the entry point of Docker or Node.js
- Modify auth
#### *️⃣ Low Priority
It changed my current workflow and require further studies.
- Change my release approach
#### ❌ Won't Merge
- Any breaking changes
@ -221,14 +196,13 @@ https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
### Release Procedures
1. Draft a release note
1. Make sure the repo is cleared
1. `npm run update-version 1.X.X`
1. `npm run build`
1. `npm run build-docker`
1. `git push`
1. Publish the release note as 1.X.X
1. `npm run upload-artifacts`
1. SSH to demo site server and update to 1.X.X
2. Make sure the repo is cleared
3. `npm run release-final with env vars: `VERSION` and `GITHUB_TOKEN`
4. Wait until the `Press any key to continue`
5. `git push`
6. Publish the release note as 1.X.X
7. Press any key to continue
8. SSH to demo site server and update to 1.X.X
Checking:
@ -236,6 +210,15 @@ Checking:
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
- Try clean installation with Node.js
### Release Beta Procedures
1. Draft a release note, check "This is a pre-release"
2. Make sure the repo is cleared
3. `npm run release-beta` with env vars: `VERSION` and `GITHUB_TOKEN`
4. Wait until the `Press any key to continue`
5. Publish the release note as 1.X.X-beta.X
6. Press any key to continue
### Release Wiki
#### Setup Repo

@ -61,8 +61,14 @@ npm run setup
node server/server.js
# (Recommended) Option 2. Run in background using PM2
# Install PM2 if you don't have it: npm install pm2 -g
# Install PM2 if you don't have it:
npm install pm2 -g && pm2 install pm2-logrotate
# Start Server
pm2 start server/server.js --name uptime-kuma
# If you want to see the current console output
pm2 monit
```
Browse to http://localhost:3001 after starting.
@ -115,7 +121,7 @@ Telegram Notification Sample:
## Motivation
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and unmaintained.
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and no longer maintained.
* Want to build a fancy UI.
* Learn Vue 3 and vite.js.
* Show the power of Bootstrap 5.
@ -144,4 +150,4 @@ If you want to translate Uptime Kuma into your language, please read: https://gi
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
English proofreading is needed too because my grammar is not that great, sadly. Feel free to correct my grammar in this README, source code, or wiki.
Unfortunately, English proofreading is needed too because my grammar is not that great. Feel free to correct my grammar in this README, source code, or wiki.

@ -0,0 +1,31 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
CREATE TABLE [status_page](
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
[slug] VARCHAR(255) NOT NULL UNIQUE,
[title] VARCHAR(255) NOT NULL,
[description] TEXT,
[icon] VARCHAR(255) NOT NULL,
[theme] VARCHAR(30) NOT NULL,
[published] BOOLEAN NOT NULL DEFAULT 1,
[search_engine_index] BOOLEAN NOT NULL DEFAULT 1,
[show_tags] BOOLEAN NOT NULL DEFAULT 0,
[password] VARCHAR,
[created_date] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
[modified_date] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX [slug] ON [status_page]([slug]);
CREATE TABLE [status_page_cname](
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
[status_page_id] INTEGER NOT NULL REFERENCES [status_page]([id]) ON DELETE CASCADE ON UPDATE CASCADE,
[domain] VARCHAR NOT NULL UNIQUE
);
ALTER TABLE incident ADD status_page_id INTEGER;
ALTER TABLE [group] ADD status_page_id INTEGER;
COMMIT;

@ -1,8 +1,8 @@
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
FROM node:14-alpine3.12
FROM node:16-alpine3.12
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 && \
pip3 --no-cache-dir install apprise==0.9.6 && \
pip3 --no-cache-dir install apprise==0.9.7 && \
rm -rf /root/.cache

@ -1,6 +1,6 @@
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
# If the image changed, the second stage image should be changed too
FROM node:14-buster-slim
FROM node:16-buster-slim
WORKDIR /app
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
@ -8,5 +8,5 @@ WORKDIR /app
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 && \
pip3 --no-cache-dir install apprise==0.9.6 && \
pip3 --no-cache-dir install apprise==0.9.7 && \
rm -rf /var/lib/apt/lists/*

@ -0,0 +1,76 @@
const pkg = require("../../package.json");
const fs = require("fs");
const child_process = require("child_process");
const util = require("../../src/util");
util.polyfill();
const oldVersion = pkg.version;
const version = process.env.VERSION;
console.log("Beta Version: " + version);
if (!oldVersion || oldVersion.includes("-beta.")) {
console.error("Error: old version should not be a beta version?");
process.exit(1);
}
if (!version || !version.includes("-beta.")) {
console.error("invalid version, beta version only");
process.exit(1);
}
const exists = tagExists(version);
if (! exists) {
// Process package.json
pkg.version = version;
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
commit(version);
tag(version);
} else {
console.log("version tag exists, please delete the tag or use another tag");
process.exit(1);
}
function commit(version) {
let msg = "Update to " + version;
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
let stdout = res.stdout.toString().trim();
console.log(stdout);
if (stdout.includes("no changes added to commit")) {
throw new Error("commit error");
}
res = child_process.spawnSync("git", ["push", "origin", "master"]);
console.log(res.stdout.toString().trim());
}
function tag(version) {
let res = child_process.spawnSync("git", ["tag", version]);
console.log(res.stdout.toString().trim());
res = child_process.spawnSync("git", ["push", "origin", version]);
console.log(res.stdout.toString().trim());
}
function tagExists(version) {
if (! version) {
throw new Error("invalid version");
}
let res = child_process.spawnSync("git", ["tag", "-l", version]);
return res.stdout.toString().trim() === version;
}
function safeDelete(dir) {
if (fs.existsSync(dir)) {
fs.rmdirSync(dir, {
recursive: true,
});
}
}

@ -0,0 +1,19 @@
#!/usr/bin/env node
const childProcess = require("child_process");
let env = process.env;
let cmd = process.argv[2];
let args = process.argv.slice(3);
let replacedArgs = [];
for (let arg of args) {
for (let key in env) {
arg = arg.replaceAll(`$${key}`, env[key]);
}
replacedArgs.push(arg);
}
let child = childProcess.spawn(cmd, replacedArgs);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);

@ -189,7 +189,7 @@ if (type == "local") {
bash("check=$(pm2 --version)");
if (check == "") {
println("Installing PM2");
bash("npm install pm2 -g");
bash("npm install pm2 -g && pm2 install pm2-logrotate");
bash("pm2 startup");
}

@ -0,0 +1,6 @@
console.log("Git Push and Publish the release note on github, then press any key to continue");
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on("data", process.exit.bind(process, 0));

@ -5,10 +5,8 @@ const util = require("../src/util");
util.polyfill();
const oldVersion = pkg.version;
const newVersion = process.argv[2];
const newVersion = process.env.VERSION;
console.log("Old Version: " + oldVersion);
console.log("New Version: " + newVersion);
if (! newVersion) {
@ -22,23 +20,20 @@ if (! exists) {
// Process package.json
pkg.version = newVersion;
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion);
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion);
// Replace the version: https://regex101.com/r/hmj2Bc/1
pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
commit(newVersion);
tag(newVersion);
updateWiki(oldVersion, newVersion);
} else {
console.log("version exists");
}
function commit(version) {
let msg = "update to " + version;
let msg = "Update to " + version;
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
let stdout = res.stdout.toString().trim();
@ -64,37 +59,3 @@ function tagExists(version) {
return res.stdout.toString().trim() === version;
}
function updateWiki(oldVersion, newVersion) {
const wikiDir = "./tmp/wiki";
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
safeDelete(wikiDir);
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
let content = fs.readFileSync(howToUpdateFilename).toString();
content = content.replaceAll(`git checkout ${oldVersion}`, `git checkout ${newVersion}`);
fs.writeFileSync(howToUpdateFilename, content);
child_process.spawnSync("git", ["add", "-A"], {
cwd: wikiDir,
});
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion} from ${oldVersion}`], {
cwd: wikiDir,
});
console.log("Pushing to Github");
child_process.spawnSync("git", ["push"], {
cwd: wikiDir,
});
safeDelete(wikiDir);
}
function safeDelete(dir) {
if (fs.existsSync(dir)) {
fs.rmdirSync(dir, {
recursive: true,
});
}
}

@ -0,0 +1,48 @@
const child_process = require("child_process");
const fs = require("fs");
const newVersion = process.env.VERSION;
if (!newVersion) {
console.log("Missing version");
process.exit(1);
}
updateWiki(newVersion);
function updateWiki(newVersion) {
const wikiDir = "./tmp/wiki";
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
safeDelete(wikiDir);
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
let content = fs.readFileSync(howToUpdateFilename).toString();
// Replace the version: https://regex101.com/r/hmj2Bc/1
content = content.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
fs.writeFileSync(howToUpdateFilename, content);
child_process.spawnSync("git", ["add", "-A"], {
cwd: wikiDir,
});
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion}`], {
cwd: wikiDir,
});
console.log("Pushing to Github");
child_process.spawnSync("git", ["push"], {
cwd: wikiDir,
});
safeDelete(wikiDir);
}
function safeDelete(dir) {
if (fs.existsSync(dir)) {
fs.rmdirSync(dir, {
recursive: true,
});
}
}

@ -159,7 +159,7 @@ fi
check=$(pm2 --version)
if [ "$check" == "" ]; then
"echo" "-e" "Installing PM2"
npm install pm2 -g
npm install pm2 -g && pm2 install pm2-logrotate
pm2 startup
fi
mkdir -p $installPath

10267
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.11.3",
"version": "1.13.1",
"license": "MIT",
"repository": {
"type": "git",
@ -30,15 +30,14 @@
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
"build-docker-alpine": "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:1.11.3-alpine --target release . --push",
"build-docker-debian": "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:1.11.3 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.11.3-debian --target release . --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-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",
"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.11.3 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.13.1 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"update-version": "node extra/update-version.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
"remove-2fa": "node extra/remove-2fa.js",
@ -51,7 +50,10 @@
"simple-dns-server": "node extra/simple-dns-server.js",
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
"ncu-patch": "ncu -u -t patch"
"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",
"git-remove-tag": "git tag -d"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "~1.2.36",
@ -61,21 +63,22 @@
"@louislam/sqlite3": "~6.0.1",
"@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0",
"axios": "~0.21.4",
"axios": "~0.26.1",
"bcryptjs": "~2.4.3",
"bootstrap": "5.1.3",
"bree": "~7.1.0",
"bree": "~7.1.5",
"chardet": "^1.3.0",
"chart.js": "~3.6.0",
"chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0",
"check-password-strength": "^2.0.3",
"check-password-strength": "^2.0.5",
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
"dayjs": "~1.10.7",
"express": "~4.17.1",
"express-basic-auth": "~1.2.0",
"dayjs": "~1.10.8",
"express": "~4.17.3",
"express-basic-auth": "~1.2.1",
"favico.js": "^0.3.10",
"form-data": "~4.0.0",
"http-graceful-shutdown": "~3.1.5",
"http-graceful-shutdown": "~3.1.7",
"iconv-lite": "^0.6.3",
"jsonwebtoken": "~8.5.1",
"jwt-decode": "^3.1.2",
@ -84,9 +87,9 @@
"notp": "~2.0.3",
"password-hash": "~1.2.2",
"postcss-rtlcss": "~3.4.1",
"postcss-scss": "~4.0.2",
"postcss-scss": "~4.0.3",
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.0",
"prometheus-api-metrics": "~3.2.1",
"qrcode": "~1.5.0",
"redbean-node": "0.1.3",
"socket.io": "~4.4.1",
@ -97,14 +100,14 @@
"timezones-list": "~3.0.1",
"v-pagination-3": "~0.1.7",
"vue": "next",
"vue-chart-3": "~0.5.11",
"vue-chart-3": "3.0.9",
"vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "~3.0.4",
"vue-i18n": "~9.1.9",
"vue-image-crop-upload": "~3.0.3",
"vue-multiselect": "~3.0.0-alpha.2",
"vue-qrcode": "~1.0.0",
"vue-router": "~4.0.12",
"vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5",
"vuedraggable": "~4.1.0"
},
@ -112,10 +115,10 @@
"@actions/github": "~5.0.0",
"@babel/eslint-parser": "~7.15.8",
"@babel/preset-env": "^7.15.8",
"@types/bootstrap": "~5.1.6",
"@vitejs/plugin-legacy": "~1.6.3",
"@types/bootstrap": "~5.1.9",
"@vitejs/plugin-legacy": "~1.6.4",
"@vitejs/plugin-vue": "~1.9.4",
"@vue/compiler-sfc": "~3.2.22",
"@vue/compiler-sfc": "~3.2.31",
"babel-plugin-rewire": "~1.2.0",
"core-js": "~3.18.3",
"cross-env": "~7.0.3",
@ -123,8 +126,9 @@
"eslint": "~7.32.0",
"eslint-plugin-vue": "~7.18.0",
"jest": "~27.2.5",
"jest-puppeteer": "~6.0.0",
"puppeteer": "~10.4.0",
"jest-puppeteer": "~6.0.3",
"npm-check-updates": "^12.5.4",
"puppeteer": "~13.1.3",
"sass": "~1.42.1",
"stylelint": "~14.2.0",
"stylelint-config-standard": "~24.0.0",

@ -31,31 +31,34 @@ exports.login = async function (username, password) {
};
function myAuthorizer(username, password, callback) {
setting("disableAuth").then((result) => {
if (result) {
callback(null, true);
} else {
// Login Rate Limit
loginRateLimiter.pass(null, 0).then((pass) => {
if (pass) {
exports.login(username, password).then((user) => {
callback(null, user != null);
if (user == null) {
loginRateLimiter.removeTokens(1);
}
});
} else {
callback(null, false);
// Login Rate Limit
loginRateLimiter.pass(null, 0).then((pass) => {
if (pass) {
exports.login(username, password).then((user) => {
callback(null, user != null);
if (user == null) {
loginRateLimiter.removeTokens(1);
}
});
} else {
callback(null, false);
}
});
}
exports.basicAuth = basicAuth({
authorizer: myAuthorizer,
authorizeAsync: true,
challenge: true,
});
exports.basicAuth = async function (req, res, next) {
const middleware = basicAuth({
authorizer: myAuthorizer,
authorizeAsync: true,
challenge: true,
});
const disabledAuth = await setting("disableAuth");
if (!disabledAuth) {
middleware(req, res, next);
} else {
next();
}
};

@ -1,5 +1,6 @@
const { setSetting } = require("./util-server");
const { setSetting, setting } = require("./util-server");
const axios = require("axios");
const compareVersions = require("compare-versions");
exports.version = require("../package.json").version;
exports.latestVersion = null;
@ -16,6 +17,19 @@ exports.startInterval = () => {
res.data.slow = "1000.0.0";
}
if (!await setting("checkUpdate")) {
return;
}
let checkBeta = await setting("checkBeta");
if (checkBeta && res.data.beta) {
if (compareVersions.compare(res.data.beta, res.data.beta, ">")) {
exports.latestVersion = res.data.beta;
return;
}
}
if (res.data.slow) {
exports.latestVersion = res.data.slow;
}

@ -53,6 +53,7 @@ class Database {
"patch-2fa-invalidate-used-token.sql": true,
"patch-notification_sent_history.sql": true,
"patch-monitor-basic-auth.sql": true,
"patch-status-page.sql": true,
}
/**
@ -170,6 +171,7 @@ class Database {
}
await this.patch2();
await this.migrateNewStatusPage();
}
/**
@ -211,6 +213,74 @@ class Database {
await setSetting("databasePatchedFiles", databasePatchedFiles);
}
/**
* Migrate status page value in setting to "status_page" table
* @returns {Promise<void>}
*/
static async migrateNewStatusPage() {
// Fix 1.13.0 empty slug bug
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
let title = await setting("title");
if (title) {
console.log("Migrating Status Page");
let statusPageCheck = await R.findOne("status_page", " slug = 'default' ");
if (statusPageCheck !== null) {
console.log("Migrating Status Page - Skip, default slug record is already existing");
return;
}
let statusPage = R.dispense("status_page");
statusPage.slug = "default";
statusPage.title = title;
statusPage.description = await setting("description");
statusPage.icon = await setting("icon");
statusPage.theme = await setting("statusPageTheme");
statusPage.published = !!await setting("statusPagePublished");
statusPage.search_engine_index = !!await setting("searchEngineIndex");
statusPage.show_tags = !!await setting("statusPageTags");
statusPage.password = null;
if (!statusPage.title) {
statusPage.title = "My Status Page";
}
if (!statusPage.icon) {
statusPage.icon = "";
}
if (!statusPage.theme) {
statusPage.theme = "light";
}
let id = await R.store(statusPage);
await R.exec("UPDATE incident SET status_page_id = ? WHERE status_page_id IS NULL", [
id
]);
await R.exec("UPDATE [group] SET status_page_id = ? WHERE status_page_id IS NULL", [
id
]);
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
// Migrate Entry Page if it is status page
let entryPage = await setting("entryPage");
if (entryPage === "statusPage") {
await setSetting("entryPage", "statusPage-default", "general");
}
console.log("Migrating Status Page - Done");
}
}
/**
* Used it patch2() only
* @param sqlFilename

@ -3,12 +3,12 @@ const { R } = require("redbean-node");
class Group extends BeanModel {
async toPublicJSON() {
async toPublicJSON(showTags = false) {
let monitorBeanList = await this.getMonitorList();
let monitorList = [];
for (let bean of monitorBeanList) {
monitorList.push(await bean.toPublicJSON());
monitorList.push(await bean.toPublicJSON(showTags));
}
return {

@ -24,18 +24,22 @@ const apicache = require("../modules/apicache");
class Monitor extends BeanModel {
/**
* Return a object that ready to parse to JSON for public
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
*/
async toPublicJSON() {
return {
async toPublicJSON(showTags = false) {
let obj = {
id: this.id,
name: this.name,
};
if (showTags) {
obj.tags = await this.getTags();
}
return obj;
}
/**
* Return a object that ready to parse to JSON
* Return an object that ready to parse to JSON
*/
async toJSON() {
@ -49,7 +53,7 @@ class Monitor extends BeanModel {
notificationIDList[bean.notification_id] = true;
}
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
const tags = await this.getTags();
return {
id: this.id,
@ -82,6 +86,10 @@ class Monitor extends BeanModel {
};
}
async getTags() {
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
}
/**
* Encode user and password to Base64 encoding
* for HTTP "basic" auth, as per RFC-7617
@ -119,6 +127,19 @@ class Monitor extends BeanModel {
const beat = async () => {
let beatInterval = this.interval;
if (! beatInterval) {
beatInterval = 1;
}
if (demoMode) {
if (beatInterval < 20) {
console.log("beat interval too low, reset to 20s");
beatInterval = 20;
}
}
// Expose here for prometheus update
// undefined if not https
let tlsInfo = undefined;
@ -303,7 +324,7 @@ class Monitor extends BeanModel {
} else {
// No need to insert successful heartbeat for push type, so end here
retries = 0;
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
return;
}
@ -377,8 +398,6 @@ class Monitor extends BeanModel {
}
}
let beatInterval = this.interval;
debug(`[${this.name}] Check isImportant`);
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
@ -422,14 +441,6 @@ class Monitor extends BeanModel {
previousBeat = bean;
if (! this.isStop) {
if (demoMode) {
if (beatInterval < 20) {
console.log("beat interval too low, reset to 20s");
beatInterval = 20;
}
}
debug(`[${this.name}] SetTimeout for next check.`);
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
} else {
@ -466,6 +477,12 @@ class Monitor extends BeanModel {
stop() {
clearTimeout(this.heartbeatInterval);
this.isStop = true;
this.prometheus().remove();
}
prometheus() {
return new Prometheus(this);
}
/**

@ -0,0 +1,60 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
class StatusPage extends BeanModel {
static async sendStatusPageList(io, socket) {
let result = {};
let list = await R.findAll("status_page", " ORDER BY title ");
for (let item of list) {
result[item.id] = await item.toJSON();
}
io.to(socket.userID).emit("statusPageList", result);
return list;
}
async toJSON() {
return {
id: this.id,
slug: this.slug,
title: this.title,
description: this.description,
icon: this.getIcon(),
theme: this.theme,
published: !!this.published,
showTags: !!this.show_tags,
};
}
async toPublicJSON() {
return {
slug: this.slug,
title: this.title,
description: this.description,
icon: this.getIcon(),
theme: this.theme,
published: !!this.published,
showTags: !!this.show_tags,
};
}
static async slugToID(slug) {
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
slug
]);
}
getIcon() {
if (!this.icon) {
return "/icon.svg";
} else {
return this.icon;
}
}
}
module.exports = StatusPage;

@ -0,0 +1,67 @@
const NotificationProvider = require("./notification-provider");
const { DOWN, UP } = require("../../src/util");
const axios = require("axios");
class Alerta extends NotificationProvider {
name = "alerta";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let alertaUrl = `${notification.alertaApiEndpoint}`;
let config = {
headers: {
"Content-Type": "application/json;charset=UTF-8",
"Authorization": "Key " + notification.alertaapiKey,
}
};
let data = {
environment: notification.alertaEnvironment,
severity: "critical",
correlate: [],
service: [ "UptimeKuma" ],
value: "Timeout",
tags: [ "uptimekuma" ],
attributes: {},
origin: "uptimekuma",
type: "exceptionAlert",
};
if (heartbeatJSON == null) {
let postData = Object.assign({
event: "msg",
text: msg,
group: "uptimekuma-msg",
resource: "Message",
}, data);
await axios.post(alertaUrl, postData, config);
} else {
let datadup = Object.assign( {
correlate: ["service_up", "service_down"],
event: monitorJSON["type"],
group: "uptimekuma-" + monitorJSON["type"],
resource: monitorJSON["name"],
}, data );
if (heartbeatJSON["status"] == DOWN) {
datadup.severity = notification.alertaAlertState; // critical
datadup.text = "Service " + monitorJSON["type"] + " is down.";
await axios.post(alertaUrl, datadup, config);
} else if (heartbeatJSON["status"] == UP) {
datadup.severity = notification.alertaRecoverState; // cleaned
datadup.text = "Service " + monitorJSON["type"] + " is up.";
await axios.post(alertaUrl, datadup, config);
}
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Alerta;

@ -0,0 +1,42 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Gorush extends NotificationProvider {
name = "gorush";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
let platformMapping = {
"ios": 1,
"android": 2,
"huawei": 3,
};
try {
let data = {
"notifications": [
{
"tokens": [notification.gorushDeviceToken],
"platform": platformMapping[notification.gorushPlatform],
"message": msg,
// Optional
"title": notification.gorushTitle,
"priority": notification.gorushPriority,
"retry": parseInt(notification.gorushRetry) || 0,
"topic": notification.gorushTopic,
}
]
};
let config = {};
await axios.post(`${notification.gorushServerURL}/api/push`, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Gorush;

@ -20,7 +20,7 @@ class Mattermost extends NotificationProvider {
return okMsg;
}
const mattermostChannel = notification.mattermostchannel;
const mattermostChannel = notification.mattermostchannel.toLowerCase();
const mattermostIconEmoji = notification.mattermosticonemo;
const mattermostIconUrl = notification.mattermosticonurl;

@ -9,36 +9,31 @@ class Pushover extends NotificationProvider {
let okMsg = "Sent Successfully.";
let pushoverlink = "https://api.pushover.net/1/messages.json";
let data = {
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg,
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
};
if (notification.pushoverdevice) {
data.device = notification.pushoverdevice;
}
try {
if (heartbeatJSON == null) {
let data = {
"message": msg,
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
};
await axios.post(pushoverlink, data);
return okMsg;
} else {
data.message += "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"];
await axios.post(pushoverlink, data);
return okMsg;
}
let data = {
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
};
await axios.post(pushoverlink, data);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}

@ -0,0 +1,23 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class TechulusPush extends NotificationProvider {
name = "PushByTechulus";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, {
"title": "Uptime-Kuma",
"body": msg,
})
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = TechulusPush;

@ -12,6 +12,7 @@ const ClickSendSMS = require("./notification-providers/clicksendsms");
const Pushbullet = require("./notification-providers/pushbullet");
const Pushover = require("./notification-providers/pushover");
const Pushy = require("./notification-providers/pushy");
const TechulusPush = require("./notification-providers/techulus-push");
const RocketChat = require("./notification-providers/rocket-chat");
const Signal = require("./notification-providers/signal");
const Slack = require("./notification-providers/slack");
@ -27,6 +28,8 @@ const SerwerSMS = require("./notification-providers/serwersms");
const Stackfield = require("./notification-providers/stackfield");
const WeCom = require("./notification-providers/wecom");
const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
const Alerta = require("./notification-providers/alerta");
class Notification {
@ -55,6 +58,7 @@ class Notification {
new Pushbullet(),
new Pushover(),
new Pushy(),
new TechulusPush(),
new RocketChat(),
new Signal(),
new Slack(),
@ -65,7 +69,9 @@ class Notification {
new SerwerSMS(),
new Stackfield(),
new WeCom(),
new GoogleChat()
new GoogleChat(),
new Gorush(),
new Alerta(),
];
for (let item of list) {

@ -86,6 +86,16 @@ class Prometheus {
}
}
remove() {
try {
monitor_cert_days_remaining.remove(this.monitorLabelValues);
monitor_cert_is_valid.remove(this.monitorLabelValues);
monitor_response_time.remove(this.monitorLabelValues);
monitor_status.remove(this.monitorLabelValues);
} catch (e) {
console.error(e);
}
}
}
module.exports = {

@ -6,6 +6,7 @@ const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
const dayjs = require("dayjs");
const { UP, flipStatus, debug } = require("../../src/util");
const StatusPage = require("../model/status_page");
let router = express.Router();
let cache = apicache.middleware;
@ -82,110 +83,80 @@ router.get("/api/push/:pushToken", async (request, response) => {
}
});
// Status Page Config
router.get("/api/status-page/config", async (_request, response) => {
// Status page config, incident, monitor list
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
let config = await getSettings("statusPage");
// Get Status Page
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (! config.statusPageTheme) {
config.statusPageTheme = "light";
}
if (! config.statusPagePublished) {
config.statusPagePublished = true;
}
if (! config.statusPageTags) {
config.statusPageTags = false;
}
if (! config.title) {
config.title = "Uptime Kuma";
if (!statusPage) {
response.statusCode = 404;
response.json({
msg: "Not Found"
});
return;
}
response.json(config);
});
// Status Page - Get the current Incident
// Can fetch only if published
router.get("/api/status-page/incident", async (_, response) => {
allowDevAllOrigin(response);
try {
await checkPublished();
let incident = await R.findOne("incident", " pin = 1 AND active = 1");
// Incident
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
statusPage.id,
]);
if (incident) {
incident = incident.toPublicJSON();
}
response.json({
ok: true,
incident,
});
} catch (error) {
send403(response, error.message);
}
});
// Status Page - Monitor List
// Can fetch only if published
router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => {
allowDevAllOrigin(response);
try {
await checkPublished();
// Public Group List
const publicGroupList = [];
const tagsVisible = (await getSettings("statusPage")).statusPageTags;
const list = await R.find("group", " public = 1 ORDER BY weight ");
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON();
if (tagsVisible) {
monitorGroup.monitorList = await Promise.all(monitorGroup.monitorList.map(async (monitor) => {
// Includes tags as an array in response, allows for tags to be displayed on public status page
const tags = await R.getAll(
`SELECT monitor_tag.monitor_id, monitor_tag.value, tag.name, tag.color
FROM monitor_tag
JOIN tag
ON monitor_tag.tag_id = tag.id
WHERE monitor_tag.monitor_id = ?`, [monitor.id]
);
return {
...monitor,
tags: tags
};
}));
}
const showTags = !!statusPage.show_tags;
debug("Show Tags???" + showTags);
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags);
publicGroupList.push(monitorGroup);
}
response.json(publicGroupList);
// Response
response.json({
config: await statusPage.toPublicJSON(),
incident,
publicGroupList
});
} catch (error) {
send403(response, error.message);
}
});
// Status Page Polling Data
// Can fetch only if published
router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => {
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
allowDevAllOrigin(response);
try {
await checkPublished();
let heartbeatList = {};
let uptimeList = {};
let slug = request.params.slug;
let statusPageID = await StatusPage.slugToID(slug);
let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND public = 1
`);
AND \`group\`.status_page_id = ?
`, [
statusPageID
]);
for (let monitorID of monitorIDList) {
let list = await R.getAll(`
@ -214,22 +185,12 @@ router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, re
}
});
async function checkPublished() {
if (! await isPublished()) {
throw new Error("The status page is not published");
}
}
/**
* Default is published
* @returns {Promise<boolean>}
*/
async function isPublished() {
const value = await setting("statusPagePublished");
if (value === null) {
return true;
}
return value;
return true;
}
function send403(res, msg = "") {

@ -132,6 +132,7 @@ const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sen
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
const TwoFA = require("./2fa");
const StatusPage = require("./model/status_page");
app.use(express.json());
@ -200,8 +201,8 @@ exports.entryPage = "dashboard";
// Entry Page
app.get("/", async (_request, response) => {
if (exports.entryPage === "statusPage") {
response.redirect("/status");
if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
} else {
response.redirect("/dashboard");
}
@ -578,6 +579,9 @@ exports.entryPage = "dashboard";
throw new Error("Permission denied.");
}
// Reset Prometheus labels
monitorList[monitor.id]?.prometheus()?.remove();
bean.name = monitor.name;
bean.type = monitor.type;
bean.url = monitor.url;
@ -1403,6 +1407,8 @@ async function afterLogin(socket, user) {
await sleep(500);
await StatusPage.sendStatusPageList(io, socket);
for (let monitorID in monitorList) {
await sendHeartbeatList(socket, monitorID);
}

@ -1,25 +1,36 @@
const { R } = require("redbean-node");
const { checkLogin, setSettings } = require("../util-server");
const { checkLogin, setSettings, setSetting } = require("../util-server");
const dayjs = require("dayjs");
const { debug } = require("../../src/util");
const ImageDataURI = require("../image-data-uri");
const Database = require("../database");
const apicache = require("../modules/apicache");
const StatusPage = require("../model/status_page");
const server = require("../server");
module.exports.statusPageSocketHandler = (socket) => {
// Post or edit incident
socket.on("postIncident", async (incident, callback) => {
socket.on("postIncident", async (slug, incident, callback) => {
try {
checkLogin(socket);
await R.exec("UPDATE incident SET pin = 0 ");
let statusPageID = await StatusPage.slugToID(slug);
if (!statusPageID) {
throw new Error("slug is not found");
}
await R.exec("UPDATE incident SET pin = 0 WHERE status_page_id = ? ", [
statusPageID
]);
let incidentBean;
if (incident.id) {
incidentBean = await R.findOne("incident", " id = ?", [
incident.id
incidentBean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [
incident.id,
statusPageID
]);
}
@ -31,6 +42,7 @@ module.exports.statusPageSocketHandler = (socket) => {
incidentBean.content = incident.content;
incidentBean.style = incident.style;
incidentBean.pin = true;
incidentBean.status_page_id = statusPageID;
if (incident.id) {
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
@ -52,11 +64,15 @@ module.exports.statusPageSocketHandler = (socket) => {
}
});
socket.on("unpinIncident", async (callback) => {
socket.on("unpinIncident", async (slug, callback) => {
try {
checkLogin(socket);
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1");
let statusPageID = await StatusPage.slugToID(slug);
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1 AND status_page_id = ? ", [
statusPageID
]);
callback({
ok: true,
@ -71,13 +87,23 @@ module.exports.statusPageSocketHandler = (socket) => {
// Save Status Page
// imgDataUrl Only Accept PNG!
socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => {
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
try {
checkLogin(socket);
checkSlug(config.slug);
checkLogin(socket);
apicache.clear();
// Save Config
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
throw new Error("No slug?");
}
const header = "data:image/png;base64,";
// Check logo format
@ -88,16 +114,28 @@ module.exports.statusPageSocketHandler = (socket) => {
throw new Error("Only allowed PNG logo.");
}
const filename = `logo${statusPage.id}.png`;
// Convert to file
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png");
config.logo = "/upload/logo.png?t=" + Date.now();
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + filename);
config.logo = `/upload/${filename}?t=` + Date.now();
} else {
config.icon = imgDataUrl;
}
// Save Config
await setSettings("statusPage", config);
statusPage.slug = config.slug;
statusPage.title = config.title;
statusPage.description = config.description;
statusPage.icon = config.logo;
statusPage.theme = config.theme;
//statusPage.published = ;
//statusPage.search_engine_index = ;
statusPage.show_tags = config.showTags;
//statusPage.password = null;
statusPage.modified_date = R.isoDateTime();
await R.store(statusPage);
// Save Public Group List
const groupIDList = [];
@ -106,13 +144,15 @@ module.exports.statusPageSocketHandler = (socket) => {
for (let group of publicGroupList) {
let groupBean;
if (group.id) {
groupBean = await R.findOne("group", " id = ? AND public = 1 ", [
group.id
groupBean = await R.findOne("group", " id = ? AND public = 1 AND status_page_id = ? ", [
group.id,
statusPage.id
]);
} else {
groupBean = R.dispense("group");
}
groupBean.status_page_id = statusPage.id;
groupBean.name = group.name;
groupBean.public = true;
groupBean.weight = groupOrder++;
@ -124,7 +164,6 @@ module.exports.statusPageSocketHandler = (socket) => {
]);
let monitorOrder = 1;
console.log(group.monitorList);
for (let monitor of group.monitorList) {
let relationBean = R.dispense("monitor_group");
@ -141,7 +180,18 @@ module.exports.statusPageSocketHandler = (socket) => {
// Delete groups that not in the list
debug("Delete groups that not in the list");
const slots = groupIDList.map(() => "?").join(",");
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
const data = [
...groupIDList,
statusPage.id
];
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
// Also change entry page to new slug if it is the default one, and slug is changed.
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
server.entryPage = "statusPage-" + statusPage.slug;
await setSetting("entryPage", server.entryPage, "general");
}
callback({
ok: true,
@ -149,7 +199,7 @@ module.exports.statusPageSocketHandler = (socket) => {
});
} catch (error) {
console.log(error);
console.error(error);
callback({
ok: false,
@ -158,4 +208,115 @@ module.exports.statusPageSocketHandler = (socket) => {
}
});
// Add a new status page
socket.on("addStatusPage", async (title, slug, callback) => {
try {
checkLogin(socket);
title = title?.trim();
slug = slug?.trim();
// Check empty
if (!title || !slug) {
throw new Error("Please input all fields");
}
// Make sure slug is string
if (typeof slug !== "string") {
throw new Error("Slug -Accept string only");
}
// lower case only
slug = slug.toLowerCase();
checkSlug(slug);
let statusPage = R.dispense("status_page");
statusPage.slug = slug;
statusPage.title = title;
statusPage.theme = "light";
statusPage.icon = "";
await R.store(statusPage);
callback({
ok: true,
msg: "OK!"
});
} catch (error) {
console.error(error);
callback({
ok: false,
msg: error.message,
});
}
});
// Delete a status page
socket.on("deleteStatusPage", async (slug, callback) => {
try {
checkLogin(socket);
let statusPageID = await StatusPage.slugToID(slug);
if (statusPageID) {
// Reset entry page if it is the default one.
if (server.entryPage === "statusPage-" + slug) {
server.entryPage = "dashboard";
await setSetting("entryPage", server.entryPage, "general");
}
// No need to delete records from `status_page_cname`, because it has cascade foreign key.
// But for incident & group, it is hard to add cascade foreign key during migration, so they have to be deleted manually.
// Delete incident
await R.exec("DELETE FROM incident WHERE status_page_id = ? ", [
statusPageID
]);
// Delete group
await R.exec("DELETE FROM `group` WHERE status_page_id = ? ", [
statusPageID
]);
// Delete status_page
await R.exec("DELETE FROM status_page WHERE id = ? ", [
statusPageID
]);
} else {
throw new Error("Status Page is not found");
}
callback({
ok: true,
});
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
};
/**
* Check slug a-z, 0-9, - only
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
*/
function checkSlug(slug) {
if (typeof slug !== "string") {
throw new Error("Slug must be string");
}
slug = slug.trim();
if (!slug) {
throw new Error("Slug cannot be empty");
}
if (!slug.match(/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/)) {
throw new Error("Invalid Slug");
}
}

@ -92,6 +92,10 @@ textarea.form-control {
}
}
.btn-dark {
background-color: #161B22;
}
@media (max-width: 550px) {
.table-shadow-box {
padding: 10px !important;
@ -144,6 +148,10 @@ textarea.form-control {
background-color: #090c10;
color: $dark-font-color;
mark, .mark {
background-color: #b6ad86;
}
&::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
background: $dark-border-color;
}
@ -156,13 +164,24 @@ textarea.form-control {
.form-check-input {
background-color: $dark-bg2;
border-color: $dark-border-color;
}
.input-group-text {
background-color: #282f39;
border-color: $dark-border-color;
color: $dark-font-color;
}
.form-check-input:checked {
border-color: $primary; // Re-apply bootstrap border
}
.form-switch .form-check-input {
background-color: #232f3b;
}
a,
a:not(.btn),
.table,
.nav-link {
color: $dark-font-color;
@ -329,11 +348,8 @@ textarea.form-control {
.monitor-list {
&.scrollbar {
min-height: calc(100vh - 240px);
max-height: calc(100vh - 30px);
overflow-y: auto;
position: sticky;
top: 10px;
height: calc(100% - 65px);
}
.item {
@ -433,6 +449,10 @@ textarea.form-control {
border-radius: 10px !important;
}
.spinner {
color: $primary;
}
// Localization
@import "localization.scss";

@ -1,5 +1,5 @@
<template>
<div class="shadow-box mb-3">
<div class="shadow-box mb-3" :style="boxStyle">
<div class="list-header">
<div class="placeholder"></div>
<div class="search-wrapper">
@ -63,9 +63,16 @@ export default {
data() {
return {
searchText: "",
windowTop: 0,
};
},
computed: {
boxStyle() {
return {
height: `calc(100vh - 160px + ${this.windowTop}px)`,
};
},
sortedMonitorList() {
let result = Object.values(this.$root.monitorList);
@ -108,7 +115,20 @@ export default {
return result;
},
},
mounted() {
window.addEventListener("scroll", this.onScroll);
},
beforeUnmount() {
window.removeEventListener("scroll", this.onScroll);
},
methods: {
onScroll() {
if (window.top.scrollY <= 133) {
this.windowTop = window.top.scrollY;
} else {
this.windowTop = 133;
}
},
monitorURL(id) {
return getMonitorRelativeURL(id);
},
@ -122,6 +142,12 @@ export default {
<style lang="scss" scoped>
@import "../assets/vars.scss";
.shadow-box {
height: calc(100vh - 150px);
position: sticky;
top: 10px;
}
.small-padding {
padding-left: 5px !important;
padding-right: 5px !important;
@ -142,6 +168,12 @@ export default {
}
}
.dark {
.footer {
// background-color: $dark-bg;
}
}
@media (max-width: 770px) {
.list-header {
margin: -20px;

@ -85,7 +85,9 @@ export default {
model: null,
processing: false,
id: null,
notificationTypes: Object.keys(NotificationFormList),
notificationTypes: Object.keys(NotificationFormList).sort((a, b) => {
return a.toLowerCase().localeCompare(b.toLowerCase());
}),
notification: {
name: "",
/** @type { null | keyof NotificationFormList } */
@ -143,12 +145,9 @@ export default {
this.id = null;
this.notification = {
name: "",
type: null,
type: "telegram",
isDefault: false,
};
// Set Default value here
this.notification.type = this.notificationTypes[0];
}
this.modal.show();

@ -41,7 +41,7 @@
<Uptime :monitor="monitor.element" type="24" :pill="true" />
{{ monitor.element.name }}
</div>
<div class="tags">
<div v-if="showTags" class="tags">
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
@ -76,6 +76,9 @@ export default {
type: Boolean,
required: true,
},
showTags: {
type: Boolean,
}
},
data() {
return {

@ -0,0 +1,14 @@
<template>
<div class="mb-3">
<label for="alerta-api-endpoint" class="form-label">{{ $t("alertaApiEndpoint") }}</label>
<input id="alerta-api-endpoint" v-model="$parent.notification.alertaApiEndpoint" type="text" class="form-control" required>
<label for="alerta-environment" class="form-label">{{ $t("alertaEnvironment") }}</label>
<input id="alerta-environment" v-model="$parent.notification.alertaEnvironment" type="text" class="form-control" required>
<label for="alerta-api-key" class="form-label">{{ $t("alertaApiKey") }}</label>
<input id="alerta-api-key" v-model="$parent.notification.alertaApiKey" type="text" class="form-control" required>
<label for="alerta-alert-state" class="form-label">{{ $t("alertaAlertState") }}</label>
<input id="alerta-alert-state" v-model="$parent.notification.alertaAlertState" type="text" class="form-control" placeholder="critical" required>
<label for="alerta-recover-state" class="form-label">{{ $t("alertaRecoverState") }}</label>
<input id="alerta-recover-state" v-model="$parent.notification.alertaRecoverState" type="text" class="form-control" placeholder="cleared" required>
</div>
</template>

@ -6,7 +6,7 @@
<label for="secretAccessKey" class="form-label">{{ $t("SecretAccessKey") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="secretAccessKey" v-model="$parent.notification.secretAccessKey" type="text" class="form-control" required>
<label for="phonenumber" class="form-label">{{ $t("Phonenumber") }}<span style="color: red;"><sup>*</sup></span></label>
<label for="phonenumber" class="form-label">{{ $t("PhoneNumbers") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="phonenumber" v-model="$parent.notification.phonenumber" type="text" class="form-control" required>
<label for="templateCode" class="form-label">{{ $t("TemplateCode") }}<span style="color: red;"><sup>*</sup></span></label>
@ -16,7 +16,7 @@
<input id="signName" v-model="$parent.notification.signName" type="text" class="form-control" required>
<div class="form-text">
<p>Sms template must contain parameters: <br> <code>${name} ${time} ${status} ${msg}</code></p>
<p>{{ $t("Sms template must contain parameters: ") }}<br> <code>${name} ${time} ${status} ${msg}</code></p>
<i18n-t tag="p" keypath="Read more:">
<a href="https://help.aliyun.com/document_detail/101414.html" target="_blank">https://help.aliyun.com/document_detail/101414.html</a>
</i18n-t>

@ -7,9 +7,9 @@
<input id="secretKey" v-model="$parent.notification.secretKey" type="text" class="form-control" required>
<div class="form-text">
<p>For safety, must use secret key</p>
<p>{{ $t("For safety, must use secret key") }}</p>
<i18n-t tag="p" keypath="Read more:">
<a href="https://developers.dingtalk.com/document/robots/custom-robot-access" target="_blank">https://developers.dingtalk.com/document/robots/custom-robot-access</a>
<a href="https://developers.dingtalk.com/document/robots/custom-robot-access" target="_blank">https://developers.dingtalk.com/document/robots/custom-robot-access</a> <a href="https://open.dingtalk.com/document/robots/customize-robot-security-settings#title-7fs-kgs-36x" target="_blank">https://open.dingtalk.com/document/robots/customize-robot-security-settings#title-7fs-kgs-36x</a>
</i18n-t>
</div>
</div>

@ -0,0 +1,51 @@
<template>
<div class="mb-3">
<label for="gorush-device-token" class="form-label">{{ $t("Device Token") }}</label><span style="color: red;"><sup>*</sup></span>
<div class="input-group mb-3">
<input id="gorush-device-token" v-model="$parent.notification.gorushDeviceToken" type="text" class="form-control" required>
</div>
</div>
<div class="mb-3">
<label for="gorush-server-url" class="form-label">{{ $t("Server URL") }}</label><span style="color: red;"><sup>*</sup></span>
<div class="input-group mb-3">
<input id="gorush-server-url" v-model="$parent.notification.gorushServerURL" type="text" class="form-control" required>
</div>
</div>
<div class="mb-3">
<label for="gorush-platform" class="form-label">{{ $t("Platform") }}</label><span style="color: red;"><sup>*</sup></span>
<select id="gorush-platform" v-model="$parent.notification.gorushPlatform" class="form-select">
<option value="ios">{{ $t("iOS") }}</option>
<option value="android">{{ $t("Android") }}</option>
<option value="huawei">{{ $t("Huawei") }}</option>
</select>
</div>
<div class="mb-3">
<label for="gorush-title" class="form-label">{{ $t("Title") }}</label>
<input id="gorush-title" v-model="$parent.notification.gorushTitle" type="text" class="form-control">
</div>
<div class="mb-3">
<label for="gorush-priority" class="form-label">{{ $t("Priority") }}</label>
<select id="gorush-priority" v-model="$parent.notification.gorushPriority" class="form-select">
<option value="normal">{{ $t("Normal") }}</option>
<option value="high">{{ $t("High") }}</option>
</select>
</div>
<div class="mb-3">
<label for="gorush-retry" class="form-label">{{ $t("Retry") }}</label>
<input id="gorush-retry" v-model="$parent.notification.gorushRetry" type="number" class="form-control">
</div>
<div class="mb-3">
<label for="gorush-topic" class="form-label">{{ $t("Topic") }}</label>
<input id="gorush-topic" v-model="$parent.notification.gorushTopic" type="text" class="form-control">
</div>
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
</div>
</template>

@ -0,0 +1,20 @@
<template>
<div class="mb-3">
<label for="push-api-key" class="form-label">API_KEY</label>
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://docs.push.techulus.com" target="_blank">https://docs.push.techulus.com</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

@ -63,9 +63,9 @@ export default {
let update = res.data.result[res.data.result.length - 1];
if (update.channel_post) {
this.notification.telegramChatID = update.channel_post.chat.id;
this.$parent.notification.telegramChatID = update.channel_post.chat.id;
} else if (update.message) {
this.notification.telegramChatID = update.message.chat.id;
this.$parent.notification.telegramChatID = update.message.chat.id;
} else {
throw new Error(this.$t("chatIDNotFound"));
}

@ -9,6 +9,7 @@ import RocketChat from "./RocketChat.vue";
import Teams from "./Teams.vue";
import Pushover from "./Pushover.vue";
import Pushy from "./Pushy.vue";
import TechulusPush from "./TechulusPush.vue";
import Octopush from "./Octopush.vue";
import PromoSMS from "./PromoSMS.vue";
import ClickSendSMS from "./ClickSendSMS.vue";
@ -26,6 +27,8 @@ import SerwerSMS from "./SerwerSMS.vue";
import Stackfield from './Stackfield.vue';
import WeCom from "./WeCom.vue";
import GoogleChat from "./GoogleChat.vue";
import Gorush from "./Gorush.vue";
import Alerta from "./Alerta.vue";
/**
* Manage all notification form.
@ -44,6 +47,7 @@ const NotificationFormList = {
"rocket.chat": RocketChat,
"pushover": Pushover,
"pushy": Pushy,
"PushByTechulus": TechulusPush,
"octopush": Octopush,
"promosms": PromoSMS,
"clicksendsms": ClickSendSMS,
@ -60,7 +64,9 @@ const NotificationFormList = {
"serwersms": SerwerSMS,
"stackfield": Stackfield,
"WeCom": WeCom,
"GoogleChat": GoogleChat
"GoogleChat": GoogleChat,
"gorush": Gorush,
"alerta": Alerta,
};
export default NotificationFormList;

@ -4,14 +4,39 @@
<object class="my-4" width="200" height="200" data="/icon.svg" />
<div class="fs-4 fw-bold">Uptime Kuma</div>
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
<div class="my-1 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
<div class="my-3 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
<div class="mt-1">
<div class="form-check">
<label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> Show update if available</label>
</div>
<div class="form-check">
<label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> Also check beta release</label>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
settings() {
return this.$parent.$parent.$parent.settings;
},
saveSettings() {
return this.$parent.$parent.$parent.saveSettings;
},
settingsLoaded() {
return this.$parent.$parent.$parent.settingsLoaded;
},
},
watch: {
}
};
</script>

@ -62,31 +62,31 @@
<div class="form-check">
<input
id="entryPageYes"
id="entryPageDashboard"
v-model="settings.entryPage"
class="form-check-input"
type="radio"
name="statusPage"
name="entryPage"
value="dashboard"
required
/>
<label class="form-check-label" for="entryPageYes">
<label class="form-check-label" for="entryPageDashboard">
{{ $t("Dashboard") }}
</label>
</div>
<div class="form-check">
<div v-for="statusPage in $root.statusPageList" :key="statusPage.id" class="form-check">
<input
id="entryPageNo"
:id="'status-page-' + statusPage.id"
v-model="settings.entryPage"
class="form-check-input"
type="radio"
name="statusPage"
value="statusPage"
name="entryPage"
:value="'statusPage-' + statusPage.slug"
required
/>
<label class="form-check-label" for="entryPageNo">
{{ $t("Status Page") }}
<label class="form-check-label" :for="'status-page-' + statusPage.id">
{{ $t("Status Page") }} - {{ statusPage.title }}
</label>
</div>
</div>

@ -215,12 +215,24 @@
<p>Dette er for <strong>de som har tredjepartsautorisering</strong> foran Uptime Kuma, for eksempel Cloudflare Access.</p>
<p>Vennligst vær forsiktig.</p>
</template>
<template v-else-if="$i18n.locale === 'cs-CZ' ">
<p>Opravdu chcete <strong>deaktivovat autentifikaci</strong>?</p>
<p>Tato možnost je určena pro případy, kdy <strong>máte autentifikaci zajištěnou třetí stranou</strong> ještě před přístupem do Uptime Kuma, například prostřednictvím Cloudflare Access.</p>
<p>Používejte ji prosím s rozmyslem.</p>
</template>
<template v-else-if="$i18n.locale === 'vi-VN' ">
<p>Bạn muốn <strong>TẮT XÁC THỰC</strong> không?</p>
<p>Điều này rất nguy hiểm<strong>BẤT KỲ AI</strong> cũng thể truy cập cướp quyền điều khiển.</p>
<p>Vui lòng <strong>cẩn thận</strong>.</p>
</template>
<!-- English (en) -->
<template v-else>
<p>Are you sure want to <strong>disable auth</strong>?</p>
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
<p>Please use it carefully.</p>
<p>Are you sure want to <strong>disable authentication</strong>?</p>
<p>It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.</p>
<p>Please use this option carefully!</p>
</template>
</Confirm>
</div>

@ -2,6 +2,7 @@ import { createI18n } from "vue-i18n/index";
import en from "./languages/en";
const languageList = {
"cs-CZ": "Čeština",
"zh-HK": "繁體中文 (香港)",
"bg-BG": "Български",
"de-DE": "Deutsch (Deutschland)",
@ -27,7 +28,7 @@ const languageList = {
"zh-CN": "简体中文",
"pl": "Polski",
"et-EE": "eesti",
"vi": "Vietnamese",
"vi-VN": "Tiếng Việt",
"zh-TW": "繁體中文 (台灣)"
};

@ -35,6 +35,9 @@ import {
faLink,
faChevronDown,
faSignOutAlt,
faPen,
faExternalLinkSquareAlt,
faSpinner,
} from "@fortawesome/free-solid-svg-icons";
library.add(
@ -69,6 +72,9 @@ library.add(
faLink,
faChevronDown,
faSignOutAlt,
faPen,
faExternalLinkSquareAlt,
faSpinner,
);
export { FontAwesomeIcon };

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "Пауза",
deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?",
deleteNotificationMsg: "Наистина ли желаете да изтриете това известяване за всички монитори?",
resoverserverDescription: "Cloudflare е сървърът по подразбиране, но можете да го промените по всяко време.",
resolverserverDescription: "Cloudflare е сървърът по подразбиране, но можете да го промените по всяко време.",
rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате",
pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?",
enableDefaultNotificationDescription: "За всеки нов монитор това известяване ще бъде активирано по подразбиране. Можете да го изключите за всеки отделен монитор.",
@ -197,6 +197,7 @@ export default {
line: "Line Messenger",
mattermost: "Mattermost",
"Status Page": "Статус страница",
"Status Pages": "Статус страница",
"Primary Base URL": "Основен базов URL адрес",
"Push URL": "Генериран Push URL адрес",
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди",
@ -360,4 +361,14 @@ export default {
smtpDkimHashAlgo: "Хеш алгоритъм (по желание)",
smtpDkimheaderFieldNames: "Хедър ключове за подписване (по желание)",
smtpDkimskipFields: "Хедър ключове, които да не се подписват (по желание)",
PushByTechulus: "Push от Techulus",
GoogleChat: "Google Chat (Само за работното пространство на Google)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "Крайна точка на API",
alertaEnvironment: "Среда",
alertaApiKey: "API Ключ",
alertaAlertState: "Състояние на тревога",
alertaRecoverState: "Състояние на възстановяване",
deleteStatusPageMsg: "Сигурни ли сте, че желаете да изтриете тази статус страница?",
};

@ -0,0 +1,365 @@
export default {
languageName: "Czech",
checkEverySecond: "Kontrolovat každých {0} sekund",
retryCheckEverySecond: "Opakovat každých {0} sekund",
retriesDescription: "Maximální počet pokusů před označením služby jako nedostupné a odesláním oznámení",
ignoreTLSError: "Ignorovat TLS/SSL chyby na HTTPS stránkách",
upsideDownModeDescription: "Pomocí této možnosti změníte způsob vyhodnocování stavu. Pokud je služba dosažitelná, je NEDOSTUPNÁ.",
maxRedirectDescription: "Maximální počet přesměrování, která se mají následovat. Nastavením hodnoty 0 zakážete přesměrování.",
acceptedStatusCodesDescription: "Vyberte stavové kódy, které jsou považovány za úspěšnou odpověď.",
passwordNotMatchMsg: "Hesla se neshodují",
notificationDescription: "Pro zajištění funkčnosti oznámení je nutné je přiřadit dohledu.",
keywordDescription: "Vyhledat klíčové slovo v prosté odpovědi HTML nebo JSON. Při hledání se rozlišuje velikost písmen.",
pauseDashboardHome: "Pozastavit",
deleteMonitorMsg: "Opravdu chcete odstranit tento dohled?",
deleteNotificationMsg: "Opravdu chcete odstranit toto oznámení pro všechny dohledy?",
resolverserverDescription: "Cloudflare je výchozí server. Resolver server můžete kdykoli změnit.",
rrtypeDescription: "Vyberte typ záznamu o prostředku, který chcete monitorovat",
pauseMonitorMsg: "Opravdu chcete dohled pozastavit?",
enableDefaultNotificationDescription: "Toto oznámení bude standardně aktivní pro nové dohledy. V případě potřeby můžete oznámení stále zakázat na úrovni jednotlivých dohledů.",
clearEventsMsg: "Opravdu chcete odstranit všechny události pro tento dohled?",
clearHeartbeatsMsg: "Opravdu chcete odstranit všechny heartbeaty pro tento dohled?",
confirmClearStatisticsMsg: "Opravdu chcete smazat VŠECHNY statistiky?",
importHandleDescription: "Možnost 'Přeskočit existující' vyberte v případě, že chcete přeskočit všechny dohledy nebo oznámení se stejným názvem. Vybráním možnosti 'Přepsat' dojde k odstranění všech existujících dohledů a oznámení.",
confirmImportMsg: "Opravdu chcete importovat zálohu? Prosím ověřte, zda jste vybrali správnou možnost importu.",
twoFAVerifyLabel: "Prosím, zadejte svůj token pro ověření 2FA:",
tokenValidSettingsMsg: "Token je platný! Nyní můžete uložit nastavení 2FA.",
confirmEnableTwoFAMsg: "Opravdu chcete zapnout 2FA?",
confirmDisableTwoFAMsg: "Opravdu chcete deaktivovat 2FA?",
Settings: "Nastavení",
Dashboard: "Nástěnka",
"New Update": "Nová aktualizace",
Language: "Jazyk",
Appearance: "Vzhled",
Theme: "Motiv",
General: "Obecné",
"Primary Base URL": "Primární URL adresa",
Version: "Verze",
"Check Update On GitHub": "Zkontrolovat aktualizace na GitHubu",
List: "Seznam",
Add: "Přidat",
"Add New Monitor": "Přidat nový dohled",
"Quick Stats": "Rychlé statistiky",
Up: "Běží",
Down: "Nedostupný",
Pending: "Čekám",
Unknown: "Neznámý",
Pause: "Pozastavit",
Name: "Název",
Status: "Stav",
DateTime: "DateTime",
Message: "Zpráva",
"No important events": "Žádné důležité události",
Resume: "Pokračovat",
Edit: "Změnit",
Delete: "Vymazat",
Current: "Aktuální",
Uptime: "Doba provozu",
"Cert Exp.": "Platnost certifikátu",
days: "dny/í",
day: "den",
"-day": "-dní",
hour: "hodina",
"-hour": "-hodin",
Response: "Odpověď",
Ping: "Ping",
"Monitor Type": "Typ dohledu",
Keyword: "Klíčové slovo",
"Friendly Name": "Obecný název",
URL: "URL",
Hostname: "Adresa serveru",
Port: "Port",
"Heartbeat Interval": "Heartbeat interval",
Retries: "Počet pokusů",
"Heartbeat Retry Interval": "Interval opakování prezenčního signálu",
Advanced: "Rozšířené",
"Upside Down Mode": "Inverzní režim",
"Max. Redirects": "Max. Přesměrování",
"Accepted Status Codes": "Akceptované stavové kódy",
"Push URL": "Push URL",
needPushEvery: "Tuto URL adresu byste měli volat každých {0} sekund.",
pushOptionalParams: "Volitelné parametry: {0}",
Save: "Uložit",
Notifications: "Oznámení",
"Not available, please setup.": "Není k dispozici, prosím nastavte.",
"Setup Notification": "Nastavení oznámení",
Light: "Světlý",
Dark: "Tmavý",
Auto: "Automaticky",
"Theme - Heartbeat Bar": "Motiv Heartbeat panel",
Normal: "Normální",
Bottom: "Dole",
None: "Žádné",
Timezone: "Časové pásmo",
"Search Engine Visibility": "Viditelnost pro vyhledávače",
"Allow indexing": "Povolit indexování",
"Discourage search engines from indexing site": "Zabránit vyhledávačům v indexování stránky",
"Change Password": "Změnit heslo",
"Current Password": "Aktuální heslo",
"New Password": "Nové heslo",
"Repeat New Password": "Znovu zadat nové heslo",
"Update Password": "Aktualizovat heslo",
"Disable Auth": "Deaktivovat ověřování",
"Enable Auth": "Povolit ověřování",
Logout: "Odhlášení",
Leave: "Odejít",
"I understand, please disable": "Rozumím, chci ji deaktivovat",
Confirm: "Potvrzení",
Yes: "Ano",
No: "Ne",
Username: "Uživatelské jméno",
Password: "Heslo",
"Remember me": "Zapamatovat si mě",
Login: "Přihlášení",
"No Monitors, please": "Žádné dohledy, prosím",
"add one": "přidat jeden",
"Notification Type": "Typ oznámení",
Email: "E-mail",
Test: "Test",
"Certificate Info": "Informace o certifikátu",
"Resolver Server": "Resolver Server",
"Resource Record Type": "Typ záznamu o prostředku",
"Last Result": "Poslední výsledek",
"Create your admin account": "Vytvořit účet administrátora",
"Repeat Password": "Znovu zadat heslo",
"Import Backup": "Importovat zálohu",
"Export Backup": "Exportovat zálohu",
Export: "Exportovat",
Import: "Importovat",
respTime: "Odezva Čas (ms)",
notAvailableShort: "N/A",
"Default enabled": "Standardně povoleno",
"Apply on all existing monitors": "Použít pro všechny existující dohledy",
Create: "Vytvořit",
"Clear Data": "Vymazat data",
Events: "Události",
Heartbeats: "Heartbeaty",
"Auto Get": "Získat automaticky",
backupDescription: "Všechny dohledy a oznámení můžete zálohovat do souboru ve formátu JSON.",
backupDescription2: "Poznámka: Nezahrnuje historii a data událostí.",
backupDescription3: "Součástí exportovaného souboru jsou citlivá data jako tokeny oznámení; export si prosím bezpečně uložte.",
alertNoFile: "Vyberte soubor, který chcete importovat.",
alertWrongFileType: "Vyberte soubor ve formátu JSON.",
"Clear all statistics": "Vymazat všechny statistiky",
"Skip existing": "Přeskočit existující",
Overwrite: "Přepsat",
Options: "Možnosti",
"Keep both": "Ponechat obojí",
"Verify Token": "Ověřit token",
"Setup 2FA": "Nastavení 2FA",
"Enable 2FA": "Povolit 2FA",
"Disable 2FA": "Deaktivovat 2FA",
"2FA Settings": "Nastavení 2FA",
"Two Factor Authentication": "Dvoufaktorová autentifikace",
Active: "Zapnuto",
Inactive: "Neaktivní",
Token: "Token",
"Show URI": "Zobrazit URI",
Tags: "Štítky",
"Add New below or Select...": "Níže přidejte nový nebo vyberte existující…",
"Tag with this name already exist.": "Štítek s tímto názvem již existuje.",
"Tag with this value already exist.": "Štítek touto hodnotou již existuje.",
color: "barva",
"value (optional)": "hodnota (volitelné)",
Gray: "Šedá",
Red: "Červená",
Orange: "Oranžová",
Green: "Zelená",
Blue: "Modrá",
Indigo: "Indigo",
Purple: "Purpurová",
Pink: "Růžová",
"Search...": "Hledat…",
"Avg. Ping": "Průměr Ping",
"Avg. Response": "Průměr Odpověď",
"Entry Page": "Vstupní stránka",
statusPageNothing: "Nic tady není, přidejte prosím skupinu nebo dohled.",
"No Services": "Žádné služby",
"All Systems Operational": "Všechny systémy běží",
"Partially Degraded Service": "Částečně zhoršená služba",
"Degraded Service": "Zhoršená služba",
"Add Group": "Přidat skupinu",
"Add a monitor": "Přidání dohledu",
"Edit Status Page": "Upravit stavovou stránku",
"Go to Dashboard": "Přejít na nástěnku",
"Status Page": "Stavová stránka",
"Status Pages": "Stavová stránka",
defaultNotificationName: "Moje {notification} upozornění ({číslo})",
here: "sem",
Required: "Vyžadováno",
telegram: "Telegram",
"Bot Token": "Token robota",
wayToGetTelegramToken: "Token můžete získat od {0}.",
"Chat ID": "ID chatu",
supportTelegramChatID: "Podpora přímého chatu / skupiny / ID chatu kanálu",
wayToGetTelegramChatID: "ID chatu můžete získat tak, že robotovi zašlete zprávu a přejdete na tuto adresu URL, kde zobrazíte chat_id:",
"YOUR BOT TOKEN HERE": "YOUR BOT TOKEN HERE",
chatIDNotFound: "ID chatu nebylo nalezeno; nejprve tomuto robotovi zašlete zprávu",
webhook: "Webhook",
"Post URL": "URL adresa příspěvku",
"Content Type": "Typ obsahu",
webhookJsonDesc: "{0} je vhodný pro všechny moderní servery HTTP, jako je Express.js",
webhookFormDataDesc: "{multipart} je vhodné pro PHP. JSON bude nutné analyzovat prostřednictvím {decodeFunction}",
smtp: "E-mail (SMTP)",
secureOptionNone: "Žádné / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "Ignorovat chybu TLS",
"From Email": "Odesílatel",
emailCustomSubject: "Vlastní předmět",
"To Email": "Příjemce",
smtpCC: "Kopie",
smtpBCC: "Skrytá kopie",
discord: "Discord",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "Získáte tak, že přejdete do Nastavení serveru - > Integrace - > Vytvořit Webhook",
"Bot Display Name": "Zobrazované jméno robota",
"Prefix Custom Message": "Předpona vlastní zprávy",
"Hello @everyone is...": "Dobrý den, {'@'}všichni jsou…",
teams: "Microsoft Teams",
"Webhook URL": "URL adresa webhooku",
wayToGetTeamsURL: "Informace o tom, jak vytvořit URL adresu webhooku naleznete {0}.",
signal: "Signal",
Number: "Číslo",
Recipients: "Příjemci",
needSignalAPI: "Musíte mít Signal klienta s REST API.",
wayToCheckSignalURL: "Pro zobrazení instrukcí, jak službu nastavit, přejděte na následující adresu:",
signalImportant: "Důležité V seznamu příjemců není možné současně použít skupiny a čísla!",
gotify: "Gotify",
"Application Token": "Token aplikace",
"Server URL": "URL adresa serveru",
Priority: "Priorita",
slack: "Slack",
"Icon Emoji": "Ikona smajlíka",
"Channel Name": "Název kanálu",
"Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "Více informací o Webhoocích naleznete na adrese: {0}",
aboutChannelName: "Pro vynechání Webhook kanálu zadejte jeho název do pole Název kanálu {0}. Příklad: #jiny-kanal",
aboutKumaURL: "Pokud ponecháte pole URL adresa Uptime Kuma prázdné, použije se domovská stránka GitHub projektu.",
emojiCheatSheet: "Tahák smajlíků: {0}",
"rocket.chat": "Rocket.Chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
clicksendsms: "ClickSend SMS",
lunasea: "LunaSea",
apprise: "Apprise (podpora více než 50 oznamovacích služeb)",
GoogleChat: "Google Chat (pouze Google Workspace)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"User Key": "Klíč uživatele",
Device: "Zařízení",
"Message Title": "Nadpis zprávy",
"Notification Sound": "Zvuk oznámení",
"More info on:": "Více informací naleznete na adrese: {0}",
pushoverDesc1: "Výchozí časový limit pro emergency prioritu (2) je 30 sekund mezi opakovanými pokusy a vyprší po 1 hodině.",
pushoverDesc2: "Pokud chcete odesílat oznámení do různých zařízení, vyplňte pole Zařízení.",
"SMS Type": "Typ SMS",
octopushTypePremium: "Premium (rychlé doporučeno pro upozornění)",
octopushTypeLowCost: "Nízké náklady (pomalé někdy blokované operátorem)",
checkPrice: "Ceny {0} zjistíte na adrese:",
apiCredentials: "API přihlašovací údaje",
octopushLegacyHint: "Používáte starší verzi Octopush (2011-2020) nebo novou verzi?",
"Check octopush prices": "Ceny octopush naleznete na adrese {0}.",
octopushPhoneNumber: "Telefonní číslo (v mezinárodním formátu, např: +42012345678) ",
octopushSMSSender: "Odesílatel SMS: 3-11 alfanumerických znaků a mezera (a-zA-Z0-9)",
"LunaSea Device ID": "ID zařízení LunaSea",
"Apprise URL": "Apprise URL",
"Example:": "Příklad: {0}",
"Read more:": "Více informací: {0}",
"Status:": "Stav: {0}",
"Read more": "Více informací",
appriseInstalled: "Apprise je nainstalován.",
appriseNotInstalled: "Apprise není nainstalován. {0}",
"Access Token": "Přístupový token",
"Channel access token": "Přístupový token ke kanálu",
"Line Developers Console": "Konzole Line Developers",
lineDevConsoleTo: "Konzole Line Developers - {0}",
"Basic Settings": "Obecné nastavení",
"User ID": "ID uživatele",
"Messaging API": "Messaging API",
wayToGetLineChannelToken: "Nejprve otevřete {0}, vytvořte poskytovatele a kanál (Messaging API). Poté můžete získat přístupový token ke kanálu a ID uživatele, v sekci uvedené výše.",
"Icon URL": "URL adresa ikony",
aboutIconURL: "Pro přepsání výchozího profilového obrázku můžete do pole \"URL adresa ikony\" zadat odkaz na obrázek. Nebude použito, pokud je nastavena ikona smajlíka.",
aboutMattermostChannelName: "Výchozí kanál, do kterého jsou zasílány Webhook příspěvky, můžete přepsat zadáním názvu kanálu do pole \"Název kanálu\". Tato možnost musí být povolena v nastavení Mattermost Webhooku. Příklad: #jiny-kanal",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO levné, ale pomalé a často přetížené. Omezeno pouze na polské příjemce.",
promosmsTypeFlash: "SMS FLASH zpráva se automaticky zobrazí na zařízení příjemce. Omezeno pouze na polské příjemce.",
promosmsTypeFull: "SMS FULL prémiová úroveň SMS. Můžete definovat odesílatele (vyžadována registrace jména). Spolehlivý pro výstrahy.",
promosmsTypeSpeed: "SMS SPEED nejvyšší priorita v systému. Velmi rychlé a spolehlivé, ale nákladné (přibližně dvojnásobek ceny SMS FULL).",
promosmsPhoneNumber: "Telefonní číslo (polští příjemci mohou vynechat telefonní předvolbu)",
promosmsSMSSender: "Odesílatel SMS: Předem zaregistrovaný název nebo jeden z výchozích: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "URL adresa domácího serveru (s http(s):// a volitelně portem)",
"Internal Room Id": "ID interní místnosti",
matrixDesc1: "ID interní místnosti naleznete v Matrix klientovi v rozšířeném nastavení místnosti. Mělo by být ve tvaru !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "Důrazně doporučujeme vytvořit nového uživatele a nepoužívat váš vlastní přístupový token uživatele Matrix. Pomocí něj je možné získat přístup k vašemu účtu a všem místnostem, ke kterým jste se připojili. Místo toho vytvořte nového uživatele a pozvěte jej pouze do místnosti, do které chcete oznámení dostávat. Přístupový token můžete získat spuštěním {0}",
Method: "Metoda",
Body: "Tělo",
Headers: "Hlavičky",
PushUrl: "Push URL",
HeadersInvalidFormat: "The request headers are not valid JSON: ",
BodyInvalidFormat: "The request body is not valid JSON: ",
"Monitor History": "Historie dohledu",
clearDataOlderThan: "Historie dohledu bude uchovávána po dobu {0} dní.",
PasswordsDoNotMatch: "Hesla se neshodují.",
records: "záznamů",
"One record": "Jeden záznam",
steamApiKeyDescription: "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key here: ",
"Current User": "Aktuálně přihlášený uživatel",
recent: "Poslední",
Done: "Hotovo",
Info: "Informace",
Security: "Bezpečnost",
"Steam API Key": "API klíč služby Steam",
"Shrink Database": "Zmenšit databázi",
"Pick a RR-Type...": "Vyberte typ záznamu o prostředku…",
"Pick Accepted Status Codes...": "Vyberte stavové kódy, které chcete akceptovat…",
Default: "Standardní",
"HTTP Options": "Možnosti protokolu HTTP",
"Create Incident": "Vytvořit incident",
Title: "Předmět",
Content: "Obsah",
Style: "Styl",
info: "informace",
warning: "upozornění",
danger: "riziko",
primary: "primární",
light: "světlý",
dark: "tmavý",
Post: "Publikovat",
"Please input title and content": "Zadejte prosím název a obsah",
Created: "Vytvořen",
"Last Updated": "Poslední aktualizace",
Unpin: "Odepnout",
"Switch to Light Theme": "Přepnout na světlý motiv",
"Switch to Dark Theme": "Přepnutí na tmavý motiv",
"Show Tags": "Zobrazit štítky",
"Hide Tags": "Skrýt štítky",
Description: "Popis",
"No monitors available.": "Není dostupný žádný dohled.",
"Add one": "Přidat jeden",
"No Monitors": "Žádný dohled",
"Untitled Group": "Skupina bez názvu",
Services: "Služby",
Discard: "Zahodit",
Cancel: "Zrušit",
"Powered by": "Poskytuje",
shrinkDatabaseDescription: "Pomocí této možnosti provedete příkaz VACUUM nad SQLite databází. Pokud byla databáze vytvořena po vydání verze 1.10.0, AUTO_VACUUM je již povolena a tato akce není vyžadována.",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API uživatelské jméno (včetně předpony webapi_)",
serwersmsAPIPassword: "API heslo",
serwersmsPhoneNumber: "Telefonní číslo",
serwersmsSenderName: "Odesílatel SMS (registrováno prostřednictvím zákaznického portálu)",
"stackfield": "Stackfield",
smtpDkimSettings: "Nastavení DKIM",
smtpDkimDesc: "Informace o použití naleznete v {0} Nodemailer DKIM.",
documentation: "dokumentaci",
smtpDkimDomain: "Název domény",
smtpDkimKeySelector: "Selector klíče",
smtpDkimPrivateKey: "Privátní klíč",
smtpDkimHashAlgo: "Hashovací algoritmus (volitelné)",
smtpDkimheaderFieldNames: "Podepisovat tyto hlavičky (volitelné)",
smtpDkimskipFields: "Nepodepisovat tyto hlavičky (volitelné)",
};

@ -98,7 +98,7 @@ export default {
keywordDescription: "Søg efter et søgeord i almindelig HTML- eller JSON -output. Bemærk, at der skelnes mellem store og små bogstaver.",
deleteMonitorMsg: "Er du sikker på, at du vil slette overvågeren?",
deleteNotificationMsg: "Er du sikker på, at du vil slette denne underretning for alle overvågere? ",
resoverserverDescription: "Cloudflare er standardserveren, den kan til enhver tid ændres.",
resolverserverDescription: "Cloudflare er standardserveren, den kan til enhver tid ændres.",
"Resolver Server": "Navne-server",
rrtypeDescription: "Vælg den type RR, du vil overvåge.",
"Last Result": "Seneste resultat",
@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Rediger Statusside",
"Go to Dashboard": "Gå til Betjeningspanel",
"Status Page": "Statusside",
"Status Pages": "Statusside",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",

@ -4,14 +4,14 @@ export default {
Dashboard: "Dashboard",
"New Update": "Update verfügbar",
Language: "Sprache",
Appearance: "Erscheinung",
Theme: "Thema",
Appearance: "Erscheinungsbild",
Theme: "Erscheinungsbild",
General: "Allgemein",
Version: "Version",
"Check Update On GitHub": "Auf GitHub nach Updates suchen",
List: "Liste",
Add: "Hinzufügen",
"Add New Monitor": "Neuer Monitor",
"Add New Monitor": "Neuen Monitor hinzufügen",
"Quick Stats": "Übersicht",
Up: "Aktiv",
Down: "Inaktiv",
@ -49,20 +49,20 @@ export default {
retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
Advanced: "Erweitert",
ignoreTLSError: "Ignoriere TLS-/SSL-Fehler von Webseiten",
"Upside Down Mode": "Invertierter Modus",
upsideDownModeDescription: "Im invertierten Modus wird der Dienst als inaktiv angezeigt, wenn er erreichbar ist.",
"Upside Down Mode": "Umgekehrter Modus",
upsideDownModeDescription: "Im umgekehrten Modus wird der Dienst als inaktiv angezeigt, wenn er erreichbar ist.",
"Max. Redirects": "Max. Weiterleitungen",
maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Auf 0 setzen, um Weiterleitungen zu deaktivieren.",
"Accepted Status Codes": "Erlaubte HTTP-Statuscodes",
acceptedStatusCodesDescription: "Wähle die Statuscodes aus, welche trotzdem als erfolgreich gewertet werden sollen.",
acceptedStatusCodesDescription: "Statuscodes auswählen, die als erfolgreiche Verbindung gelten sollen.",
Save: "Speichern",
Notifications: "Benachrichtigungen",
"Not available, please setup.": "Keine verfügbar, bitte einrichten.",
"Not available, please setup.": "Nicht verfügbar, bitte einrichten.",
"Setup Notification": "Benachrichtigung einrichten",
Light: "Hell",
Dark: "Dunkel",
Auto: "Auto",
"Theme - Heartbeat Bar": "Thema - Zeitleiste",
"Theme - Heartbeat Bar": "Erscheinungsbild - Zeitleiste",
Normal: "Normal",
Bottom: "Unten",
None: "Keine",
@ -71,15 +71,15 @@ export default {
"Allow indexing": "Indizierung zulassen",
"Discourage search engines from indexing site": "Halte Suchmaschinen von der Indexierung der Seite ab",
"Change Password": "Passwort ändern",
"Current Password": "Derzeitiges Passwort",
"Current Password": "Aktuelles Passwort",
"New Password": "Neues Passwort",
"Repeat New Password": "Neues Passwort wiederholen",
passwordNotMatchMsg: "Passwörter stimmen nicht überein. ",
passwordNotMatchMsg: "Passwörter stimmen nicht überein.",
"Update Password": "Passwort aktualisieren",
"Disable Auth": "Authentifizierung deaktivieren",
"Enable Auth": "Authentifizierung aktivieren",
Logout: "Ausloggen",
notificationDescription: "Weise den Monitor(en) eine Benachrichtigung zu, damit diese Funktion greift.",
notificationDescription: "Benachrichtigungen müssen einem Monitor zugewiesen werden, damit diese funktionieren.",
Leave: "Verlassen",
"I understand, please disable": "Ich verstehe, bitte deaktivieren",
Confirm: "Bestätigen",
@ -87,7 +87,7 @@ export default {
No: "Nein",
Username: "Benutzername",
Password: "Passwort",
"Remember me": "Passwort merken",
"Remember me": "Angemeldet bleiben",
Login: "Einloggen",
"No Monitors, please": "Keine Monitore, bitte",
"add one": "hinzufügen",
@ -98,7 +98,7 @@ export default {
keywordDescription: "Ein Suchwort in der HTML- oder JSON-Ausgabe finden. Bitte beachte: es wird zwischen Groß-/Kleinschreibung unterschieden.",
deleteMonitorMsg: "Bist du sicher, dass du den Monitor löschen möchtest?",
deleteNotificationMsg: "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?",
resoverserverDescription: "Cloudflare ist als der Standardserver festgelegt, dieser kann jederzeit geändern werden.",
resolverserverDescription: "Cloudflare ist als der Standardserver festgelegt. Dieser kann jederzeit geändert werden.",
"Resolver Server": "Auflösungsserver",
rrtypeDescription: "Wähle den RR-Typ aus, welchen du überwachen möchtest.",
"Last Result": "Letztes Ergebnis",
@ -110,8 +110,8 @@ export default {
Heartbeats: "Statistiken",
confirmClearStatisticsMsg: "Bist du dir sicher, dass du ALLE Statistiken löschen möchtest?",
"Create your admin account": "Erstelle dein Admin-Konto",
"Repeat Password": "Wiederhole das Passwort",
"Resource Record Type": "Resource Record Type",
"Repeat Password": "Passwort erneut eingeben",
"Resource Record Type": "Ressourcen Record Typ",
Export: "Export",
Import: "Import",
respTime: "Antw.-Zeit (ms)",
@ -148,7 +148,7 @@ export default {
Token: "Token",
"Show URI": "URI anzeigen",
Tags: "Tags",
"Add New below or Select...": "Bestehenden Tag auswählen oder neuen hinzufügen...",
"Add New below or Select...": "Einen bestehenden Tag auswählen oder neuen hinzufügen...",
"Tag with this name already exist.": "Ein Tag mit diesem Namen existiert bereits.",
"Tag with this value already exist.": "Ein Tag mit diesem Wert existiert bereits.",
color: "Farbe",
@ -162,8 +162,8 @@ export default {
Purple: "Lila",
Pink: "Pink",
"Search...": "Suchen...",
"Heartbeat Retry Interval": "Heartbeat-Wiederholungsintervall",
retryCheckEverySecond: "Versuche alle {0} Sekunden",
"Heartbeat Retry Interval": "Überprüfungsintervall",
retryCheckEverySecond: "Alle {0} Sekunden neu versuchen",
"Import Backup": "Backup importieren",
"Export Backup": "Backup exportieren",
"Avg. Ping": "Durchschn. Ping",
@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Bearbeite Status-Seite",
"Go to Dashboard": "Gehe zum Dashboard",
"Status Page": "Status-Seite",
"Status Pages": "Status-Seite",
telegram: "Telegram",
webhook: "Webhook",
smtp: "E-Mail (SMTP)",
@ -202,7 +203,7 @@ export default {
"Push URL": "Push URL",
needPushEvery: "Du solltest diese URL alle {0} Sekunden aufrufen",
pushOptionalParams: "Optionale Parameter: {0}",
defaultNotificationName: "Meine {notification} Alarm ({number})",
defaultNotificationName: "Mein {notification} Alarm ({number})",
here: "hier",
Required: "Erforderlich",
"Bot Token": "Bot Token",
@ -214,23 +215,23 @@ export default {
chatIDNotFound: "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
"Post URL": "Post URL",
"Content Type": "Content Type",
webhookJsonDesc: "{0} ist gut für alle modernen HTTP-Server sowie Express.js",
webhookFormDataDesc: "{multipart} ist gut für PHP. Die JSON muss mit {decodeFunction} geparst werden",
webhookJsonDesc: "{0} ist gut für alle modernen HTTP-Server, wie z.B. Express.js, geeignet",
webhookFormDataDesc: "{multipart} ist gut für PHP. Das JSON muss mit {decodeFunction} verarbeitet werden",
secureOptionNone: "Keine / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "TLS-Fehler ignorieren",
"From Email": "Von Email",
"From Email": "Absender E-Mail",
emailCustomSubject: "Benutzerdefinierter Betreff",
"To Email": "Zu Email",
"To Email": "Empfänger E-Mail",
smtpCC: "CC",
smtpBCC: "BCC",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "Du kannst diesen erhalten, indem du zu den Servereinstellungen gehst -> Integrationen -> Neuer Webhook",
wayToGetDiscordURL: "Du kannst diese erhalten, indem du zu den Servereinstellungen gehst -> Integrationen -> Neuer Webhook",
"Bot Display Name": "Bot-Anzeigename",
"Prefix Custom Message": "Benutzerdefinierter Nachrichten Präfix",
"Hello @everyone is...": "Hallo {'@'}everyone ist...",
"Webhook URL": "Webhook URL",
wayToGetTeamsURL: "Hier erfährst du, wie eine Webhook-URL erstellt werden kann {0}.",
wayToGetTeamsURL: "Wie eine Webhook-URL erstellt werden kann, erfährst du {0}.",
Number: "Nummer",
Recipients: "Empfänger",
needSignalAPI: "Es wird ein Signal Client mit REST-API benötigt.",
@ -243,22 +244,22 @@ export default {
"Channel Name": "Kanalname",
"Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "Weitere Informationen zu Webhooks auf: {0}",
aboutChannelName: "Gebe den Kanalnamen ein auf {0} Feld Kanalname, wenn du den Webhook-Kanal umgehen möchtest. Ex: #other-channel",
aboutKumaURL: "Wenn das Feld für die Uptime Kuma URL leer gelassen wird, wird es standardmäßig die GitHub Projekt Seite verwenden.",
aboutChannelName: "Gebe den Kanalnamen ein in {0} Feld Kanalname, falls du den Webhook-Kanal umgehen möchtest. Ex: #other-channel",
aboutKumaURL: "Wenn das Feld für die Uptime Kuma URL leer gelassen wird, wird standardmäßig die GitHub Projekt Seite verwendet.",
emojiCheatSheet: "Emoji Cheat Sheet: {0}",
"User Key": "Benutzerschlüssel",
Device: "Gerät",
"Message Title": "Nachrichtentitel",
"Notification Sound": "Benachrichtigungston",
"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.",
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.",
"SMS Type": "SMS Typ",
octopushTypePremium: "Premium (Schnell - zur Benachrichtigung empfohlen)",
octopushTypeLowCost: "Kostengünstig (Langsam - manchmal vom Betreiber gesperrt)",
checkPrice: "Prüfe {0} Preise:",
octopushLegacyHint: "Verwendest du die Legacy-Version von Octopush (2011-2020) oder die neue Version?",
"Check octopush prices": "Überprüfe die Oktopush Preise {0}.",
"Check octopush prices": "Vergleiche die Oktopush Preise {0}.",
octopushPhoneNumber: "Telefonnummer (Internationales Format, z.B : +49612345678) ",
octopushSMSSender: "Name des SMS-Absenders : 3-11 alphanumerische Zeichen und Leerzeichen (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea Geräte ID",
@ -279,24 +280,24 @@ export default {
wayToGetLineChannelToken: "Rufe zuerst {0} auf, erstelle dann einen Provider und Channel (Messaging API). Als nächstes kannst du den Channel access token und die User ID aus den oben genannten Menüpunkten abrufen.",
"Icon URL": "Icon URL",
aboutIconURL: "Du kannst einen Link zu einem Bild in 'Icon URL' übergeben um das Standardprofilbild zu überschreiben. Wird nicht verwendet, wenn ein Icon Emoji gesetzt ist.",
aboutMattermostChannelName: "Du kannst den Standardkanal, auf dem der Webhook postet überschreiben, indem der Kanalnamen in das Feld 'Channel Name' eingeben wird. Dies muss in den Mattermost Webhook-Einstellungen aktiviert werden. Ex: #other-channel",
aboutMattermostChannelName: "Du kannst den Standardkanal, auf dem der Webhook gesendet wird überschreiben, indem der Kanalnamen in das Feld 'Channel Name' eingeben wird. Dies muss in den Mattermost Webhook-Einstellungen aktiviert werden. Ex: #other-channel",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - billig, aber langsam und oft überladen. Nur auf polnische Empfänger beschränkt.",
promosmsTypeFlash: "SMS FLASH - Die Nachricht wird automatisch auf dem Empfängergerät angezeigt. Nur auf polnische Empfänger beschränkt.",
promosmsTypeEco: "SMS ECO - billig, aber langsam und oft überladen. Auf polnische Empfänger beschränkt.",
promosmsTypeFlash: "SMS FLASH - Die Nachricht wird automatisch auf dem Empfängergerät angezeigt. Auf polnische Empfänger beschränkt.",
promosmsTypeFull: "SMS FULL - Premium Stufe von SMS, es kann der Absendernamen verwendet werden (Der Name musst zuerst registriert werden). Zuverlässig für Warnungen.",
promosmsTypeSpeed: "SMS SPEED - Höchste Priorität im System. Sehr schnell und zuverlässig, aber teuer (Ungefähr das doppelte von SMS FULL).",
promosmsPhoneNumber: "Phone number (Für polnische Empfänger können die Vorwahlen übersprungen werden)",
promosmsPhoneNumber: "Telefonnummer (für polnische Empfänger können die Vorwahlen übersprungen werden)",
promosmsSMSSender: "Name des SMS-Absenders : vorregistrierter Name oder einer der Standardwerte: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu Webhook URL",
matrixHomeserverURL: "Heimserver URL (mit http(s):// und optionalen Ports)",
"Internal Room Id": "Interne Raum-ID",
matrixDesc1: "Die interne Raum-ID findest du im erweiterten Bereich der Raumeinstellungen im Matrix-Client. Es sollte es aussehen wie z.B. !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "Es wird dringend empfohlen, dass ein neuen Benutzer erstellt wird und nicht den Zugriffstoken deines eigenen Matrix-Benutzers verwendest. Anderfalls ermöglicht es vollen Zugriff auf dein Konto und alle Räume, denen du beigetreten bist. Erstelle stattdessen einen neuen Benutzer und lade ihn nur in den Raum ein, in dem du die Benachrichtigung erhalten möchtest. Du kannst den Zugriffstoken erhalten, indem du folgendes ausführst {0}",
matrixDesc1: "Die interne Raum-ID findest du im erweiterten Bereich der Raumeinstellungen im Matrix-Client. Es sollte aussehen wie z.B. !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "Es wird dringend empfohlen einen neuen Benutzer anzulegen und nicht den Zugriffstoken deines eigenen Matrix-Benutzers zu verwenden. Anderenfalls ermöglicht es vollen Zugriff auf dein Konto und alle Räume, denen du beigetreten bist. Erstelle stattdessen einen neuen Benutzer und lade ihn nur in den Raum ein, in dem du die Benachrichtigung erhalten möchtest. Du kannst den Zugriffstoken erhalten, indem du Folgendes ausführst {0}",
Method: "Method",
Body: "Body",
Headers: "Headers",
PushUrl: "Push URL",
HeadersInvalidFormat: "Die Header ist kein gültiges JSON: ",
HeadersInvalidFormat: "Der Header ist kein gültiges JSON: ",
BodyInvalidFormat: "Der Body ist kein gültiges JSON: ",
"Monitor History": "Monitor Verlauf",
clearDataOlderThan: "Bewahre die Monitor-Verlaufsdaten für {0} Tage auf.",
@ -350,4 +351,15 @@ export default {
serwersmsPhoneNumber: "Telefonnummer",
serwersmsSenderName: "Name des SMS-Absenders (über Kundenportal registriert)",
"stackfield": "Stackfield",
clicksendsms: "ClickSend SMS",
apiCredentials: "API Zugangsdaten",
smtpDkimSettings: "DKIM Einstellungen",
smtpDkimDesc: "Details zur Konfiguration sind in der Nodemailer DKIM {0} zu finden.",
documentation: "Dokumentation",
smtpDkimDomain: "Domain Name",
smtpDkimKeySelector: "Schlüssel Auswahl",
smtpDkimPrivateKey: "Privater Schlüssel",
smtpDkimHashAlgo: "Hash-Algorithmus (Optional)",
smtpDkimheaderFieldNames: "Zu validierende Header-Schlüssel (optional)",
smtpDkimskipFields: "Zu ignorierende Header Schlüssel (optional)",
};

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "Pause",
deleteMonitorMsg: "Are you sure want to delete this monitor?",
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?",
resoverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.",
resolverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.",
rrtypeDescription: "Select the RR type you want to monitor",
pauseMonitorMsg: "Are you sure want to pause?",
enableDefaultNotificationDescription: "This notification will be enabled by default for new monitors. You can still disable the notification separately for each monitor.",
@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page",
"Status Pages": "Status Pages",
defaultNotificationName: "My {notification} Alert ({number})",
here: "here",
Required: "Required",
@ -238,6 +239,7 @@ export default {
"rocket.chat": "Rocket.Chat",
pushover: "Pushover",
pushy: "Pushy",
PushByTechulus: "Push by Techulus",
octopush: "Octopush",
promosms: "PromoSMS",
clicksendsms: "ClickSend SMS",
@ -329,21 +331,21 @@ export default {
dark: "dark",
Post: "Post",
"Please input title and content": "Please input title and content",
Created: "Created",
"Created": "Created",
"Last Updated": "Last Updated",
Unpin: "Unpin",
"Unpin": "Unpin",
"Switch to Light Theme": "Switch to Light Theme",
"Switch to Dark Theme": "Switch to Dark Theme",
"Show Tags": "Show Tags",
"Hide Tags": "Hide Tags",
Description: "Description",
"Description": "Description",
"No monitors available.": "No monitors available.",
"Add one": "Add one",
"No Monitors": "No Monitors",
"Untitled Group": "Untitled Group",
Services: "Services",
Discard: "Discard",
Cancel: "Cancel",
"Services": "Services",
"Discard": "Discard",
"Cancel": "Cancel",
"Powered by": "Powered by",
shrinkDatabaseDescription: "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
serwersms: "SerwerSMS.pl",
@ -351,7 +353,7 @@ export default {
serwersmsAPIPassword: "API Password",
serwersmsPhoneNumber: "Phone number",
serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
"stackfield": "Stackfield",
stackfield: "Stackfield",
smtpDkimSettings: "DKIM Settings",
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
documentation: "documentation",
@ -361,4 +363,12 @@ export default {
smtpDkimHashAlgo: "Hash Algorithm (Optional)",
smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
smtpDkimskipFields: "Header Keys not to sign (Optional)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "Environment",
alertaApiKey: "API Key",
alertaAlertState: "Alert State",
alertaRecoverState: "Recover State",
deleteStatusPageMsg: "Are you sure want to delete this status page?",
};

@ -12,7 +12,7 @@ export default {
pauseDashboardHome: "Pausado",
deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?",
deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?",
resoverserverDescription: "Cloudflare es el servidor por defecto, puedes cambiar el servidor de resolución en cualquier momento.",
resolverserverDescription: "Cloudflare es el servidor por defecto, puedes cambiar el servidor de resolución en cualquier momento.",
rrtypeDescription: "Selecciona el tipo de registro que quieres monitorizar",
pauseMonitorMsg: "¿Seguro que quieres pausar?",
Settings: "Ajustes",
@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Editar página de estado",
"Go to Dashboard": "Ir al panel de control",
"Status Page": "Página de estado",
"Status Pages": "Página de estado",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",

@ -12,11 +12,12 @@ export default {
pauseDashboardHome: "Seisatud",
deleteMonitorMsg: "Kas soovid eemaldada seire?",
deleteNotificationMsg: "Kas soovid eemaldada selle teavitusteenuse kõikidelt seiretelt?",
resoverserverDescription: "Cloudflare on vaikimisi pöördserver.",
resolverserverDescription: "Cloudflare on vaikimisi pöördserver.",
rrtypeDescription: "Vali kirje tüüp, mida soovid jälgida.",
pauseMonitorMsg: "Kas soovid peatada seire?",
Settings: "Seaded",
"Status Page": "Ülevaade",
"Status Pages": "Ülevaated",
Dashboard: "Töölaud",
"New Update": "Uuem tarkvara versioon on saadaval.",
Language: "Keel",
@ -197,4 +198,10 @@ export default {
pushbullet: "Pushbullet",
line: "LINE",
mattermost: "Mattermost",
alerta: "Alerta",
alertaApiEndpoint: "API otsik",
alertaEnvironment: "Keskkond",
alertaApiKey: "API võti",
alertaAlertState: "Häireseisund",
alertaRecoverState: "Taasta algolek",
};

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "متوقف شده",
deleteMonitorMsg: "آیا از حذف این مانیتور مطمئن هستید؟",
deleteNotificationMsg: "آیا مطمئن هستید که میخواهید این سرویس اطلاع‌رسانی را برای تمامی مانیتورها حذف کنید؟",
resoverserverDescription: "سرویس CloudFlare به عنوان سرور پیش‌فرض استفاده می‌شود، شما میتوانید آنرا به هر سرور دیگری بعدا تغییر دهید.",
resolverserverDescription: "سرویس CloudFlare به عنوان سرور پیش‌فرض استفاده می‌شود، شما میتوانید آنرا به هر سرور دیگری بعدا تغییر دهید.",
rrtypeDescription: "لطفا نوع Resource Record را انتخاب کنید.",
pauseMonitorMsg: "آیا مطمئن هستید که میخواهید این مانیتور را متوقف کنید ؟",
enableDefaultNotificationDescription: "برای هر مانیتور جدید، این سرویس اطلاع‌رسانی به صورت پیش‌فرض فعال خواهد شد. البته که شما میتوانید به صورت دستی آنرا برای هر مانیتور به صورت جداگانه غیر فعال کنید.",
@ -178,6 +178,7 @@ export default {
"Add a monitor": "اضافه کردن مانیتور",
"Edit Status Page": "ویرایش صفحه وضعیت",
"Status Page": "صفحه وضعیت",
"Status Pages": "صفحه وضعیت",
"Go to Dashboard": "رفتن به پیشخوان",
"Uptime Kuma": "آپتایم کوما",
records: "مورد",

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "En pause",
deleteMonitorMsg: "Êtes-vous sûr de vouloir supprimer cette sonde ?",
deleteNotificationMsg: "Êtes-vous sûr de vouloir supprimer ce type de notifications ? Une fois désactivée, les services qui l'utilisent ne pourront plus envoyer de notifications.",
resoverserverDescription: "Le DNS de Cloudflare est utilisé par défaut, mais vous pouvez le changer si vous le souhaitez.",
resolverserverDescription: "Le DNS de Cloudflare est utilisé par défaut, mais vous pouvez le changer si vous le souhaitez.",
rrtypeDescription: "Veuillez sélectionner un type d'enregistrement DNS",
pauseMonitorMsg: "Êtes-vous sûr de vouloir mettre en pause cette sonde ?",
enableDefaultNotificationDescription: "Pour chaque nouvelle sonde, cette notification sera activée par défaut. Vous pouvez toujours désactiver la notification séparément pour chaque sonde.",
@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Modifier la page de statut",
"Go to Dashboard": "Accéder au tableau de bord",
"Status Page": "Status Page",
"Status Pages": "Status Pages",
defaultNotificationName: "Ma notification {notification} numéro ({number})",
here: "ici",
Required: "Requis",
@ -304,4 +305,9 @@ export default {
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",
recent: "Récent",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "Environement",
alertaApiKey: "Clé de l'API",
alertaAlertState: "État de l'Alerte",
alertaRecoverState: "État de récupération",
};

@ -12,7 +12,7 @@ export default {
keywordDescription: "Ključna riječ za pretragu, u obliku običnog HTML-a ili u JSON formatu. Pretraga je osjetljiva na velika i mala slova.",
deleteMonitorMsg: "Jeste li sigurni da želite izbrisati monitor?",
deleteNotificationMsg: "Jeste li sigurni da želite izbrisati ovu obavijest za sve monitore?",
resoverserverDescription: "Cloudflare je zadani DNS poslužitelj. Možete to promijeniti u bilo kojem trenutku.",
resolverserverDescription: "Cloudflare je zadani DNS poslužitelj. Možete to promijeniti u bilo kojem trenutku.",
rrtypeDescription: "Odaberite vrstu DNS zapisa o resursu kojeg želite pratiti",
pauseMonitorMsg: "Jeste li sigurni da želite pauzirati?",
enableDefaultNotificationDescription: "Ova će obavijesti biti omogućena za sve nove monitore. Možete ju ručno onemogućiti za pojedini monitor.",
@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Uredi Statusnu stranicu",
"Go to Dashboard": "Na Kontrolnu ploču",
"Status Page": "Statusna stranica",
"Status Pages": "Statusne stranice",
defaultNotificationName: "Moja {number}. {notification} obavijest",
here: "ovdje",
Required: "Potrebno",
@ -338,13 +339,38 @@ export default {
"Hide Tags": "Sakrij oznake",
Description: "Opis",
"No monitors available.": "Nema dostupnih monitora.",
"Add one": "Add one",
"No Monitors": "Bez monitora",
"Add one": "Stvori jednog",
"No Monitors": "Bez monitora",
"Untitled Group": "Bezimena grupa",
Services: "Usluge",
Discard: "Odbaci",
Cancel: "Otkaži",
"Powered by": "Pokreće",
Saved: "Spremljeno",
PushByTechulus: "Push by Techulus",
GoogleChat: "Google Chat (preko platforme Google Workspace)",
shrinkDatabaseDescription: "Pokreni VACUUM operaciju za SQLite. Ako je baza podataka kreirana nakon inačice 1.10.0, AUTO_VACUUM opcija već je uključena te ova akcija nije nužna.",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API korisničko ime (uključujući webapi_ prefiks)",
serwersmsAPIPassword: "API lozinka",
serwersmsPhoneNumber: "Broj telefona",
serwersmsSenderName: "Ime SMS pošiljatelja (registrirano preko korisničkog portala)",
stackfield: "Stackfield",
smtpDkimSettings: "DKIM postavke",
smtpDkimDesc: "Za više informacija, postoji Nodemailer DKIM {0}.",
documentation: "dokumentacija",
smtpDkimDomain: "Domena",
smtpDkimKeySelector: "Odabir ključa",
smtpDkimPrivateKey: "Privatni ključ",
smtpDkimHashAlgo: "Hash algoritam (neobavezno)",
smtpDkimheaderFieldNames: "Ključevi zaglavlja za potpis (neobavezno)",
smtpDkimskipFields: "Ključevi zaglavlja koji se neće potpisati (neobavezno)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "Krajnja točka API-ja (Endpoint)",
alertaEnvironment: "Okruženje (Environment)",
alertaApiKey: "API ključ",
alertaAlertState: "Stanje upozorenja",
alertaRecoverState: "Stanje oporavka",
deleteStatusPageMsg: "Sigurno želite obrisati ovu statusnu stranicu?",
};

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "Szünetel",
deleteMonitorMsg: "Biztos, hogy törölni akarja ezt a figyelőt?",
deleteNotificationMsg: "Biztos, hogy törölni akarja ezt az értesítést az összes figyelőnél?",
resoverserverDescription: "A Cloudflare az alapértelmezett szerver, bármikor meg tudja változtatni a resolver server-t.",
resolverserverDescription: "A Cloudflare az alapértelmezett szerver, bármikor meg tudja változtatni a resolver server-t.",
rrtypeDescription: "Válassza ki az RR-típust a figyelőhöz",
pauseMonitorMsg: "Biztos, hogy szüneteltetni akarja?",
enableDefaultNotificationDescription: "Minden új figyelőhöz ez az értesítés engedélyezett lesz alapértelmezetten. Kikapcsolhatja az értesítést külön minden figyelőnél.",
@ -197,6 +197,7 @@ export default {
line: "Line Messenger",
mattermost: "Mattermost",
"Status Page": "Státusz oldal",
"Status Pages": "Státusz oldal",
"Primary Base URL": "Elsődleges URL",
"Push URL": "Meghívandó URL",
needPushEvery: "Ezt az URL-t kell meghívni minden {0} másodpercben.",
@ -350,4 +351,23 @@ export default {
serwersmsAPIPassword: "API jelszó",
serwersmsPhoneNumber: "Telefonszám",
serwersmsSenderName: "SMS feladó neve (regisztrált név az oldalon)",
GoogleChat: "Google Chat (csak Google Workspace)",
stackfield: "Stackfield",
smtpDkimSettings: "DKIM beállítások",
smtpDkimDesc: "Nézze meg a Nodemailer DKIM {0} használati szabályokat.",
documentation: "dokumentáció",
smtpDkimDomain: "Domain név",
smtpDkimKeySelector: "Kulcs választó",
smtpDkimPrivateKey: "Privát kulcs",
smtpDkimHashAlgo: "Hash algoritmus (nem kötelező)",
smtpDkimheaderFieldNames: "Fejléc kulcsok a bejelentkezéshez (nem kötelező)",
smtpDkimskipFields: "Fejléc kulcsok egyéb esetben (nem kötelező)",
PushByTechulus: "Techulus push",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API végpont",
alertaEnvironment: "Környezet",
alertaApiKey: "API kulcs",
alertaAlertState: "Figyelmeztetési állapot",
alertaRecoverState: "Visszaállási állapot",
};

@ -7,13 +7,13 @@ export default {
upsideDownModeDescription: "Balikkan statusnya. Jika layanan dapat dijangkau, TIDAK AKTIF.",
maxRedirectDescription: "Jumlah maksimum pengalihan untuk diikuti. Setel ke 0 untuk menonaktifkan pengalihan.",
acceptedStatusCodesDescription: "Pilih kode status yang dianggap sebagai tanggapan yang berhasil.",
passwordNotMatchMsg: "Sandi kedua tidak cocok.",
passwordNotMatchMsg: "Kata sandi kedua tidak cocok.",
notificationDescription: "Harap atur notifikasi ke monitor agar berfungsi.",
keywordDescription: "Cari kata kunci dalam code html atau JSON huruf besar-kecil berpengaruh",
pauseDashboardHome: "Jeda",
deleteMonitorMsg: "Apakah Anda mau menghapus monitor ini?",
deleteNotificationMsg: "Apakah Anda mau menghapus notifikasi ini untuk semua monitor?",
resoverserverDescription: "Cloudflare adalah server bawaan, Anda dapat mengubah server resolver kapan saja.",
deleteNotificationMsg: "Apakah Anda mau menghapus notifikasi untuk semua monitor?",
resolverserverDescription: "Cloudflare adalah server bawaan, Anda dapat mengubah server resolver kapan saja.",
rrtypeDescription: "Pilih RR-Type yang mau Anda monitor",
pauseMonitorMsg: "Apakah Anda yakin mau menjeda?",
enableDefaultNotificationDescription: "Untuk setiap monitor baru, notifikasi ini akan diaktifkan secara bawaan. Anda masih dapat menonaktifkan notifikasi secara terpisah untuk setiap monitor.",
@ -23,9 +23,9 @@ export default {
importHandleDescription: "Pilih 'Lewati yang ada' jika Anda ingin melewati setiap monitor atau notifikasi dengan nama yang sama. 'Timpa' akan menghapus setiap monitor dan notifikasi yang ada.",
confirmImportMsg: "Apakah Anda yakin untuk mengimpor cadangan? Pastikan Anda telah memilih opsi impor yang tepat.",
twoFAVerifyLabel: "Silakan ketik token Anda untuk memverifikasi bahwa 2FA berfungsi",
tokenValidSettingsMsg: "Tokennya benar! Anda sekarang dapat menyimpan pengaturan 2FA.",
confirmEnableTwoFAMsg: "Apakah Anda yakin ingin mengaktifkan 2FA?",
confirmDisableTwoFAMsg: "Apakah Anda yakin ingin menonaktifkan 2FA?",
tokenValidSettingsMsg: "Token benar! Anda sekarang dapat menyimpan pengaturan 2FA.",
confirmEnableTwoFAMsg: "Apakah anda yakin ingin mengaktifkan 2FA?",
confirmDisableTwoFAMsg: "Apakah anda yakin ingin menonaktifkan 2FA?",
Settings: "Pengaturan",
Dashboard: "Dasbor",
"New Update": "Pembaruan Baru",
@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Edit Halaman Status",
"Go to Dashboard": "Pergi ke Dasbor",
"Status Page": "Halaman Status",
"Status Pages": "Halaman Status",
defaultNotificationName: "{notification} saya Peringatan ({number})",
here: "di sini",
Required: "Dibutuhkan",
@ -276,7 +277,7 @@ export default {
promosmsTypeEco: "SMS ECO - murah tapi lambat dan sering kelebihan beban. Terbatas hanya untuk penerima Polandia.",
promosmsTypeFlash: "SMS FLASH - Pesan akan otomatis muncul di perangkat penerima. Terbatas hanya untuk penerima Polandia.",
promosmsTypeFull: "SMS FULL - SMS tingkat premium, Anda dapat menggunakan Nama Pengirim Anda (Anda harus mendaftarkan nama terlebih dahulu). Dapat diAndalkan untuk peringatan.",
promosmsTypeSpeed: "SMS SPEED - Prioritas tertinggi dalam sistem. Sangat cepat dan dapat diAndalkan tetapi mahal (sekitar dua kali lipat dari harga SMS FULL).",
promosmsTypeSpeed: "SMS SPEED - Prioritas tertinggi dalam sistem. Sangat cepat dan dapat diandalkan tetapi mahal (sekitar dua kali lipat dari harga SMS FULL).",
promosmsPhoneNumber: "Nomor telepon (untuk penerima Polandia Anda dapat melewati kode area)",
promosmsSMSSender: "Nama Pengirim SMS : Nama pra-registrasi atau salah satu bawaan: InfoSMS, Info SMS, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu WebHookUrl",

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "In Pausa",
deleteMonitorMsg: "Si è certi di voler eliminare questo oggetto monitorato?",
deleteNotificationMsg: "Si è certi di voler eliminare questa notifica per tutti gli oggetti monitorati?",
resoverserverDescription: "Cloudflare è il server predefinito, è possibile cambiare il server DNS.",
resolverserverDescription: "Cloudflare è il server predefinito, è possibile cambiare il server DNS.",
rrtypeDescription: "Scegliere il tipo di RR che si vuole monitorare",
pauseMonitorMsg: "Si è certi di voler mettere in pausa?",
enableDefaultNotificationDescription: "Per ogni nuovo monitor questa notifica sarà abilitata di default. È comunque possibile disabilitare la notifica singolarmente.",
@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Modifica pagina di stato",
"Go to Dashboard": "Vai alla dashboard",
"Status Page": "Pagina di stato",
"Status Pages": "Pagina di stato",
defaultNotificationName: "Notifica {notification} ({number})",
here: "qui",
Required: "Obbligatorio",

@ -12,7 +12,7 @@ export default {
pauseDashboardHome: "一時停止",
deleteMonitorMsg: "この監視を削除してよろしいですか?",
deleteNotificationMsg: "全ての監視のこの通知を削除してよろしいですか?",
resoverserverDescription: "Cloudflareがデフォルトのサーバーですが、いつでもリゾルバサーバーを変更できます。",
resolverserverDescription: "Cloudflareがデフォルトのサーバーですが、いつでもリゾルバサーバーを変更できます。",
rrtypeDescription: "監視するRRタイプを選択します",
pauseMonitorMsg: "一時停止しますか?",
Settings: "設定",
@ -180,6 +180,7 @@ export default {
"Edit Status Page": "ステータスページ編集",
"Go to Dashboard": "ダッシュボード",
"Status Page": "ステータスページ",
"Status Pages": "ステータスページ",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "일시 정지",
deleteMonitorMsg: "정말 이 모니터링을 삭제할까요?",
deleteNotificationMsg: "정말 이 알림을 모든 모니터링에서 삭제할까요?",
resoverserverDescription: "Cloudflare가 기본 서버예요, 원한다면 언제나 다른 Resolver 서버로 변경할 수 있어요.",
resolverserverDescription: "Cloudflare가 기본 서버예요, 원한다면 언제나 다른 Resolver 서버로 변경할 수 있어요.",
rrtypeDescription: "모니터링할 RR-Type을 선택해요.",
pauseMonitorMsg: "정말 이 모니터링을 일시 정지할까요?",
enableDefaultNotificationDescription: "새로 추가하는 모든 모니터링에 이 알림을 기본적으로 활성화해요. 각 모니터에 대해 별도로 알림을 비활성화할 수 있어요.",
@ -179,6 +179,7 @@ export default {
"Edit Status Page": "상태 페이지 수정",
"Go to Dashboard": "대시보드로 가기",
"Status Page": "상태 페이지",
"Status Pages": "상태 페이지",
defaultNotificationName: "내 {notification} 알림 ({number})",
here: "여기",
Required: "필수",
@ -188,7 +189,7 @@ export default {
"Chat ID": "채팅 ID",
supportTelegramChatID: "Direct Chat / Group / Channel's Chat ID를 지원해요.",
wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.",
"YOUR BOT TOKEN HERE": "YOUR BOT TOKEN HERE",
"YOUR BOT TOKEN HERE": "여기에 BOT 토큰을 적어주세요.",
chatIDNotFound: "채팅 ID를 찾을 수 없어요. 먼저 봇에게 메시지를 보내주세요.",
webhook: "Webhook",
"Post URL": "Post URL",
@ -281,15 +282,15 @@ export default {
promosmsSMSSender: "SMS 보내는 사람 이름 : 미리 등록된 이름 혹은 기본값 중 하나예요: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Primary Base URL": "기본 URL",
"Push URL": "Push URL",
needPushEvery: "You should call this URL every {0} seconds.",
pushOptionalParams: "Optional parameters: {0}",
emailCustomSubject: "Custom Subject",
needPushEvery: "이 URL을 {0} 초 마다 호출할 수 있어요.",
pushOptionalParams: "선택적 파라미터: {0}",
emailCustomSubject: "커스텀 주제",
clicksendsms: "ClickSend SMS",
checkPrice: "{0} 가격 확인:",
apiCredentials: "API credentials",
apiCredentials: "API 인증정보",
octopushLegacyHint: "Octopush 레거시 버전 (2011-2020) 을 사용하시나요? 아니면 새 버전을 사용하시나요?",
"Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)",
matrixHomeserverURL: "Homeserver URL (http(s):// 와 함께 적어주세요. 그리고 포트 번호는 선택적 입니다.)",
"Internal Room Id": "내부 방 ID",
matrixDesc1: "Matrix 클라이언트 방 설정의 고급 섹션에서 내부 방 ID를 찾을 수 있어요. 내부 방 ID는 이렇게 생겼답니다: !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "사용자의 모든 방에 대한 엑세스가 허용될 수 있어서 새로운 사용자를 만들고 원하는 방에만 초대한 후 엑세스 토큰을 사용하는 것이 좋아요. {0} 이 명령어를 통해 엑세스 토큰을 얻을 수 있어요.",
@ -349,6 +350,6 @@ export default {
serwersmsAPIUser: "API Usename (webapi_ 접두사 포함)",
serwersmsAPIPassword: "API 비밀번호",
serwersmsPhoneNumber: "휴대전화 번호",
serwersmsSenderName: "보내는 사람 이름 (registered via customer portal)",
serwersmsSenderName: "보내는 사람 이름 (customer portal를 통해 가입된 정보)",
stackfield: "Stackfield",
};

@ -4,24 +4,24 @@ export default {
retryCheckEverySecond: "Prøv igjen hvert {0} sekund.",
retriesDescription: "Maksimalt antall forsøk før tjenesten er merket som nede og et varsel sendes",
ignoreTLSError: "Ignorer TLS/SSL-feil for HTTPS-nettsteder",
upsideDownModeDescription: "Snu statusen opp ned. Hvis tjenesten er tilgjengelig, er den NED.",
upsideDownModeDescription: "Snu statusen opp ned. Hvis tjenesten er tilgjengelig, er den NEDE.",
maxRedirectDescription: "Maksimalt antall viderekoblinger å følge. Sett til 0 for å deaktivere viderekoblinger.",
acceptedStatusCodesDescription: "Velg statuskoder som anses som et vellykket svar.",
acceptedStatusCodesDescription: "Velg statuskoder som anses som en vellykket respons.",
passwordNotMatchMsg: "Passordene stemmer ikke overens.",
notificationDescription: "Tilordne et varsel for å overvåkningen for å få det til å fungere.",
keywordDescription: "Søk etter nøkkelord i vanlig HTML eller JSON, og det er versalfølsom",
notificationDescription: "Varsler må tilordnes en overvåkning for å fungere.",
keywordDescription: "Søk etter nøkkelord i ren HTML eller JSON. Søket skiller mellom store og små bokstaver.",
pauseDashboardHome: "Pause",
deleteMonitorMsg: "Er du sikker på at du vil slette denne overvåkningen?",
deleteNotificationMsg: "Er du sikker på at du vil slette dette varselet for alle overvåkningene?",
resoverserverDescription: "Cloudflare er standardserveren, kan du når som helst endre DNS-serveren.",
resolverserverDescription: "Cloudflare er standardserveren. Du kan endre DNS-serveren når som helst.",
rrtypeDescription: "Velg RR-typen du vil overvåke",
pauseMonitorMsg: "Er du sikker på at du vil sette en pause?",
pauseMonitorMsg: "Er du sikker på at du vil sette pause?",
enableDefaultNotificationDescription: "For hver ny overvåkning vil denne varslingen være aktivert som standard. Du kan fortsatt deaktivere varselet separat for hver overvåkning.",
clearEventsMsg: "Er du sikker på at du vil slette alle hendelser for denne overvåkningen?",
clearHeartbeatsMsg: "Er du sikker på at du vil slette alle hjerteslag for denne overvåkningen?",
confirmClearStatisticsMsg: "Er du sikker på at du vil slette ALL statistikk?",
importHandleDescription: "Velg 'Hopp over eksisterende' hvis du vil hoppe over hver overvåkning eller varsel med samme navn. 'Overskriv' sletter alle eksisterende overvåkninger og varsler.",
confirmImportMsg: "Er du sikker på å importere sikkerhetskopien? Sørg for at du har valgt riktig importalternativ.",
confirmImportMsg: "Er du sikker på at du vil importere denne sikkerhetskopien? Sørg for at du har valgt riktig importalternativ.",
twoFAVerifyLabel: "Skriv inn tokenet ditt for å bekrefte at 2FA fungerer",
tokenValidSettingsMsg: "Token er gyldig! Du kan nå lagre 2FA-innstillingene.",
confirmEnableTwoFAMsg: "Er du sikker på at du vil aktivere 2FA?",
@ -50,7 +50,7 @@ export default {
Message: "Melding",
"No important events": "Ingen viktige hendelser",
Resume: "Fortsett",
Edit: "Endre",
Edit: "Rediger",
Delete: "Slett",
Current: "Nåværende",
Uptime: "Oppetid",
@ -77,7 +77,7 @@ export default {
"Accepted Status Codes": "Godkjente statuskoder",
Save: "Lagre",
Notifications: "Varsler",
"Not available, please setup.": "Ikke tilgjengelig, sett opp.",
"Not available, please setup.": "Ikke tilgjengelig, venligst sett opp.",
"Setup Notification": "Sett opp varsel",
Light: "Lys",
Dark: "Mørk",
@ -87,9 +87,9 @@ export default {
Bottom: "Bunn",
None: "Ingen",
Timezone: "Tidssone",
"Search Engine Visibility": "Søkemotor synlighet",
"Search Engine Visibility": "Søkemotor-synlighet",
"Allow indexing": "Tillat indeksering",
"Discourage search engines from indexing site": "Avskrekk søkemotorer fra å indeksere nettstedet",
"Discourage search engines from indexing site": "Fraråd søkemotorer fra å indeksere nettstedet",
"Change Password": "Endre passord",
"Current Password": "Nåværende passord",
"New Password": "Nytt passord",
@ -99,7 +99,7 @@ export default {
"Enable Auth": "Aktiver autentisering",
Logout: "Logg ut",
Leave: "Forlat",
"I understand, please disable": "Jeg forstår, deaktiver",
"I understand, please disable": "Jeg forstår, vennligst deaktiver",
Confirm: "Bekreft",
Yes: "Ja",
No: "Nei",
@ -125,15 +125,15 @@ export default {
respTime: "Svartid (ms)",
notAvailableShort: "N/A",
"Default enabled": "Standard aktivert",
"Apply on all existing monitors": "Påfør på alle eksisterende overvåkninger",
"Apply on all existing monitors": "Anvend for alle eksisterende overvåkninger",
Create: "Opprett",
"Clear Data": "Slett data",
Events: "Hendelser",
Heartbeats: "Hjerteslag",
"Auto Get": "Auto Get",
"Auto Get": "Auto Hent",
backupDescription: "Du kan sikkerhetskopiere alle overvåkninger og alle varsler til en JSON-fil.",
backupDescription2: "PS: Historikk og hendelsesdata er ikke inkludert.",
backupDescription3: "Følsomme data som varslingstokener er inkludert i eksportfilen. Vennligst oppbevar dem nøye.",
backupDescription3: "Følsomme data som varslingstokener er inkludert i eksportfilen. Vennligst oppbevar dem sikkert.",
alertNoFile: "Velg en fil som skal importeres.",
alertWrongFileType: "Velg en JSON-fil.",
"Clear all statistics": "Fjern all statistikk",
@ -154,7 +154,7 @@ export default {
Tags: "Etiketter",
"Add New below or Select...": "Legg til nytt nedenfor eller Velg ...",
"Tag with this name already exist.": "Etikett med dette navnet eksisterer allerede.",
"Tag with this value already exist.": "Etikett med denne verdien finnes allerede.",
"Tag with this value already exist.": "Etikett med denne verdien eksisterer allerede.",
color: "farge",
"value (optional)": "verdi (valgfritt)",
Gray: "Grå",
@ -172,29 +172,30 @@ export default {
statusPageNothing: "Ingenting her, vennligst legg til en gruppe eller en overvåkning.",
"No Services": "Ingen tjenester",
"All Systems Operational": "Alle systemer i drift",
"Partially Degraded Service": "Delvis degradert drift",
"Degraded Service": "Degradert drift",
"Partially Degraded Service": "Delvis degradert tjeneste",
"Degraded Service": "Degradert tjeneste",
"Add Group": "Legg til gruppe",
"Add a monitor": "Legg til en overvåkning",
"Edit Status Page": "Rediger statusside",
"Go to Dashboard": "Gå til Dashboard",
"Status Page": "Statusside",
"Status Pages": "Statusside",
defaultNotificationName: "Min {notification} varsling ({number})",
here: "here",
here: "her",
Required: "Obligatorisk",
telegram: "Telegram",
"Bot Token": "Bot Token",
wayToGetTelegramToken: "Du kan få et token fra {0}.",
"Chat ID": "Chat ID",
supportTelegramChatID: "Support Direct Chat / Group / Channel's Chat ID",
wayToGetTelegramChatID: "Du kan få chat-ID-en din ved å sende meldingen til boten og gå til denne nettadressen for å se chat_id:",
wayToGetTelegramChatID: "Du kan få chat-ID-en din ved å sende en melding til boten og gå til denne nettadressen for å se chat_id:",
"YOUR BOT TOKEN HERE": "DITT BOT TOKEN HER",
chatIDNotFound: "Chat-ID ble ikke funnet. Send en melding til denne boten først",
webhook: "Webhook",
"Post URL": "Post URL",
"Content Type": "Content Type",
"Content Type": "Innholdstype",
webhookJsonDesc: "{0} er bra for alle moderne HTTP-servere som express.js",
webhookFormDataDesc: "{multipart} er bra for PHP, du trenger bare å analysere JSON etter {decodeFunction}",
webhookFormDataDesc: "{multipart} er bra for PHP. JSON trenger å bli analysert med {decodeFunction}",
smtp: "E-post (SMTP)",
secureOptionNone: "None / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
@ -205,7 +206,7 @@ export default {
smtpBCC: "BCC",
discord: "Discord",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "Du kan få dette ved å gå til Serverinnstillinger -> Integrasjoner -> Webhooks -> Ny webhook",
wayToGetDiscordURL: "Du kan få denne ved å gå til Serverinnstillinger -> Integrasjoner -> Opprett en Webhook",
"Bot Display Name": "Bot Visningsnavn",
"Prefix Custom Message": "Prefiks tilpasset melding",
"Hello @everyone is...": "Hei {'@'}everyone det er...",
@ -240,43 +241,43 @@ export default {
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"User Key": "User Key",
Device: "Device",
"Message Title": "Message Title",
"Notification Sound": "Notification Sound",
"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.",
"User Key": "Bruker-nøkkel",
Device: "Enhet",
"Message Title": "Meldingstittel",
"Notification Sound": "Notifikasjonslyd",
"More info on:": "Mer info på: {0}",
pushoverDesc1: "Nødsprioritet (2) har en standard 30 sekunders tidsavbrudd mellom forsøk og vil utløpe etter 1 time.",
pushoverDesc2: "Hvis du vil sende varsler til forskjellige enheteter, fyll ut Enhet-feltet.",
"SMS Type": "SMS Type",
octopushTypePremium: "Premium (Fast - recommended for alerting)",
octopushTypeLowCost: "Low Cost (Slow, sometimes blocked by operator)",
"Check octopush prices": "Check octopush prices {0}.",
octopushPhoneNumber: "Phone number (intl format, eg : +33612345678) ",
octopushSMSSender: "SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea Device ID",
octopushTypePremium: "Premium (Raskt - anbefalt for varsling)",
octopushTypeLowCost: "Lav kostnad (Sakte, noen ganger blokkert av leverandør)",
"Check octopush prices": "Sjekk octopush priser {0}.",
octopushPhoneNumber: "Telefonnummer (intl format, eg : +4791234567) ",
octopushSMSSender: "SMS Avsendernavn : 3-11 alphanumeriske tegn og mellomrom (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea Enhet ID",
"Apprise URL": "Apprise URL",
"Example:": "Example: {0}",
"Read more:": "Read more: {0}",
"Example:": "Eksempel: {0}",
"Read more:": "Les mer: {0}",
"Status:": "Status: {0}",
"Read more": "Read more",
appriseInstalled: "Apprise is installed.",
appriseNotInstalled: "Apprise is not installed. {0}",
"Access Token": "Access Token",
"Channel access token": "Channel access token",
"Line Developers Console": "Line Developers Console",
lineDevConsoleTo: "Line Developers Console - {0}",
"Basic Settings": "Basic Settings",
"User ID": "User ID",
"Messaging API": "Messaging API",
wayToGetLineChannelToken: "First access the {0}, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.",
"Icon URL": "Icon URL",
aboutIconURL: "You can provide a link to a picture in \"Icon URL\" to override the default profile picture. Will not be used if Icon Emoji is set.",
aboutMattermostChannelName: "You can override the default channel that webhook posts to by entering the channel name into \"Channel Name\" field. This needs to be enabled in Mattermost webhook settings. Ex: #other-channel",
"Read more": "Les mer",
appriseInstalled: "Apprise er installert.",
appriseNotInstalled: "Apprise ikke installert. {0}",
"Access Token": "Tilgangs-Token",
"Channel access token": "Kanal tilgangs-token",
"Line Developers Console": "Line Utviklserskonsoll",
lineDevConsoleTo: "Line Utviklserskonsoll - {0}",
"Basic Settings": "Grunnleggende instillinger",
"User ID": "Bruker-ID",
"Messaging API": "Meldings-API",
wayToGetLineChannelToken: "Først, få tilgang til {0}, lag en leverandør og kanal (Meldings-API), deretter kan du hente kanaltilgangs-token og bruker id fra menu-valgene nevnt over.",
"Icon URL": "Ikon URL",
aboutIconURL: "Du kan gi en link til et bilde i \"Ikon URL\" for å overskrive det standard profilbildet. Vil ikke bli brukt hvis Ikon Emoji ikke er satt.",
aboutMattermostChannelName: "Du kan overskrive standardkanalen som webhook-en poster i ved å skrive enn kanalnavnet i \"Kanalnavn\" feltet. Dette må være skrudd på i Mattermost webhook-instillingene. Eks: #other-channel",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - cheap but slow and often overloaded. Limited only to Polish recipients.",
promosmsTypeFlash: "SMS FLASH - Message will automatically show on recipient device. Limited only to Polish recipients.",
promosmsTypeFull: "SMS FULL - Premium tier of SMS, You can use Your Sender Name (You need to register name first). Reliable for alerts.",
promosmsTypeSpeed: "SMS SPEED - Highest priority in system. Very quick and reliable but costly (about twice of SMS FULL price).",
promosmsPhoneNumber: "Phone number (for Polish recipient You can skip area codes)",
promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
promosmsTypeEco: "SMS ECO - billig, men treg og ofte overbelastet. Begrenset til bare polske mottakere.",
promosmsTypeFlash: "SMS FLASH - Melding vil automatisk vises på mottakker-enhet. Begrenset til bare polske mottakere.",
promosmsTypeFull: "SMS FULL - Premuimnivå SMS. Du kan bruke dit avsendernavn (Du må registerere et navn først). Pålitelig for alle varslinger.",
promosmsTypeSpeed: "SMS SPEED - Høyest prioritet i systemet.Veldig rask på pålitelig, men dyrt (omtrent det dobbeltet av SMS FULL pris).",
promosmsPhoneNumber: "Telefonnummber (for polske mottakere. Du trenger ikke områdekode.)",
promosmsSMSSender: "SMS Avsendernavn : Forhåndsregistert navn eller en av standardnavnene: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
};

@ -12,7 +12,7 @@ export default {
pauseDashboardHome: "Gepauzeerd",
deleteMonitorMsg: "Weet u zeker dat u deze monitor wilt verwijderen?",
deleteNotificationMsg: "Weet u zeker dat u deze melding voor alle monitoren wilt verwijderen?",
resoverserverDescription: "Cloudflare is de standaardserver, u kunt de resolver server op elk moment wijzigen.",
resolverserverDescription: "Cloudflare is de standaardserver, u kunt de resolver server op elk moment wijzigen.",
rrtypeDescription: "Selecteer het RR-type dat u wilt monitoren",
pauseMonitorMsg: "Weet je zeker dat je wilt pauzeren?",
enableDefaultNotificationDescription: "Voor elke nieuwe monitor wordt deze melding standaard ingeschakeld. U kunt de melding nog steeds afzonderlijk uitschakelen voor elke monitor.",
@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Wijzig status pagina",
"Go to Dashboard": "Ga naar Dashboard",
"Status Page": "Status Pagina",
"Status Pages": "Status Pagina",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "Wstrzymane",
deleteMonitorMsg: "Czy na pewno chcesz usunąć ten monitor?",
deleteNotificationMsg: "Czy na pewno chcesz usunąć to powiadomienie dla wszystkich monitorów?",
resoverserverDescription: "Cloudflare jest domyślnym serwerem, możesz zmienić serwer resolver w każdej chwili.",
resolverserverDescription: "Cloudflare jest domyślnym serwerem, możesz zmienić serwer resolver w każdej chwili.",
rrtypeDescription: "Wybierz rodzaj rekordu, który chcesz monitorować.",
pauseMonitorMsg: "Czy na pewno chcesz wstrzymać monitorowanie?",
enableDefaultNotificationDescription: "Dla każdego nowego monitora to powiadomienie będzie domyślnie włączone. Nadal możesz wyłączyć powiadomienia osobno dla każdego monitora.",
@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Edytuj stronę statusu",
"Go to Dashboard": "Idź do panelu",
"Status Page": "Strona statusu",
"Status Pages": "Strona statusu",
defaultNotificationName: "Moje powiadomienie {notification} ({number})",
here: "tutaj",
Required: "Wymagane",

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "Pausar",
deleteMonitorMsg: "Tem certeza de que deseja excluir este monitor?",
deleteNotificationMsg: "Tem certeza de que deseja excluir esta notificação para todos os monitores?",
resoverserverDescription: "Cloudflare é o servidor padrão, você pode alterar o servidor resolvedor a qualquer momento.",
resolverserverDescription: "Cloudflare é o servidor padrão, você pode alterar o servidor resolvedor a qualquer momento.",
rrtypeDescription: "Selecione o RR-Type que você deseja monitorar",
pauseMonitorMsg: "Tem certeza que deseja fazer uma pausa?",
enableDefaultNotificationDescription: "Para cada novo monitor, esta notificação será habilitada por padrão. Você ainda pode desativar a notificação separadamente para cada monitor.",
@ -169,6 +169,7 @@ export default {
"Avg. Ping": "Ping Médio.",
"Avg. Response": "Resposta Média. ",
"Status Page": "Página de Status",
"Status Pages": "Página de Status",
"Entry Page": "Página de entrada",
statusPageNothing: "Nada aqui, por favor, adicione um grupo ou monitor.",
"No Services": "Nenhum Serviço",

@ -12,11 +12,11 @@ export default {
pauseDashboardHome: "Пауза",
deleteMonitorMsg: "Вы действительно хотите удалить данный монитор?",
deleteNotificationMsg: "Вы действительно хотите удалить это уведомление для всех мониторов?",
resoverserverDescription: "Cloudflare является сервером по умолчанию. Вы всегда можете сменить данный сервер.",
resolverserverDescription: "Cloudflare является сервером по умолчанию. Вы всегда можете сменить данный сервер.",
rrtypeDescription: "Выберите тип ресурсной записи, который вы хотите отслеживать",
pauseMonitorMsg: "Вы действительно хотите поставить на паузу?",
Settings: "Настройки",
Dashboard: "Панель мониторов",
Dashboard: "Панель управления",
"New Update": "Обновление",
Language: "Язык",
Appearance: "Внешний вид",
@ -60,7 +60,7 @@ export default {
"Heartbeat Interval": "Частота опроса",
Retries: "Попыток",
Advanced: "Дополнительно",
"Upside Down Mode": "Режим реверса статуса",
"Upside Down Mode": "Реверс статуса",
"Max. Redirects": "Макс. количество перенаправлений",
"Accepted Status Codes": "Допустимые коды статуса",
Save: "Сохранить",
@ -75,9 +75,9 @@ export default {
Bottom: "Снизу",
None: "Отсутствует",
Timezone: "Часовой пояс",
"Search Engine Visibility": "Видимость поисковым движком",
"Search Engine Visibility": "Индексация поисковыми системами:",
"Allow indexing": "Разрешить индексирование",
"Discourage search engines from indexing site": "Не позволять индексировать сайт",
"Discourage search engines from indexing site": "Запретить индексирование",
"Change Password": "Сменить пароль",
"Current Password": "Текущий пароль",
"New Password": "Новый пароль",
@ -96,6 +96,7 @@ export default {
"Remember me": "Запомнить меня",
Login: "Вход в систему",
"No Monitors, please": "Мониторов нет, пожалуйста",
"No Monitors": "Мониторы отсутствуют",
"add one": "создайте новый",
"Notification Type": "Тип уведомления",
Email: "Почта",
@ -107,7 +108,7 @@ export default {
"Create your admin account": "Создайте аккаунт администратора",
"Repeat Password": "Повторите пароль",
respTime: "Время ответа (мс)",
notAvailableShort: "Н",
notAvailableShort: "N/A",
Create: "Создать",
clearEventsMsg: "Вы действительно хотите удалить всю статистику событий данного монитора?",
clearHeartbeatsMsg: "Вы действительно хотите удалить всю статистику опросов данного монитора?",
@ -119,8 +120,8 @@ export default {
enableDefaultNotificationDescription: "Для каждого нового монитора это уведомление будет включено по умолчанию. Вы всё ещё можете отключить уведомления в каждом мониторе отдельно.",
"Default enabled": "Использовать по умолчанию",
"Also apply to existing monitors": "Применить к существующим мониторам",
Export: "Резервная копия",
Import: "Восстановление",
Export: "Экспорт",
Import: "Импорт",
backupDescription: "Вы можете сохранить резервную копию всех мониторов и уведомлений в виде JSON-файла",
backupDescription2: "P.S. История и события сохранены не будут",
backupDescription3: "Важные данные, такие как токены уведомлений, добавляются при экспорте, поэтому храните файлы в безопасном месте",
@ -141,13 +142,13 @@ export default {
Inactive: "Неактивно",
Token: "Токен",
"Show URI": "Показать URI",
"Clear all statistics": "Удалить всю статистику",
"Clear all statistics": "Очистить статистику",
retryCheckEverySecond: "Повтор каждые {0} секунд",
importHandleDescription: "Выберите \"Пропустить существующие\", если вы хотите пропустить каждый монитор или уведомление с таким же именем. \"Перезаписать\" удалит каждый существующий монитор или уведомление и добавит заново. Вариант \"Не проверять\" принудительно восстанавливает все мониторы и уведомления, даже если они уже существуют.",
confirmImportMsg: "Вы действительно хотите восстановить резервную копию? Убедитесь, что вы выбрали подходящий вариант импорта.",
"Heartbeat Retry Interval": "Интервал повтора опроса",
"Import Backup": "Восстановление резервной копии",
"Export Backup": "Резервная копия",
"Import Backup": "Импорт",
"Export Backup": "Скачать",
"Skip existing": "Пропустить существующие",
Overwrite: "Перезаписать",
Options: "Опции",
@ -172,14 +173,15 @@ export default {
"Entry Page": "Главная страница",
statusPageNothing: "Здесь пусто. Добавьте группу или монитор.",
"No Services": "Нет сервисов",
"All Systems Operational": "Все сервисы работают",
"Partially Degraded Service": "Сервисы частично не работают",
"All Systems Operational": "Все системы работают в штатном режиме",
"Partially Degraded Service": "Сервисы работают частично",
"Degraded Service": "Все сервисы не работают",
"Add Group": "Добавить группу",
"Add a monitor": "Добавить монитор",
"Edit Status Page": "Редактировать",
"Go to Dashboard": "Панель мониторов",
"Status Page": "Статус сервисов",
"Go to Dashboard": "Панель управления",
"Status Page": "Мониторинг",
"Status Pages": "Панель мониторингов",
Discard: "Отмена",
"Create Incident": "Создать инцидент",
"Switch to Dark Theme": "Тёмная тема",
@ -302,22 +304,23 @@ export default {
PushUrl: "URL пуша",
HeadersInvalidFormat: "Заголовки запроса некорректны JSON: ",
BodyInvalidFormat: "Тело запроса некорректно JSON: ",
"Monitor History": "История мониторов",
clearDataOlderThan: "Сохранять историю мониторов в течение {0} дней.",
"Monitor History": "Статистика",
clearDataOlderThan: "Сохранять статистику за {0} дней.",
PasswordsDoNotMatch: "Пароли не совпадают.",
records: "записей",
"One record": "Одна запись",
steamApiKeyDescription: "Для мониторинга игрового сервера Steam вам необходим Web-API ключ Steam. Зарегистрировать его можно здесь: ",
"Certificate Chain:": "Цепочка сертификатов:",
"Certificate Chain": "Цепочка сертификатов",
"Valid": "Действительный",
"Hide Tags": "Скрыть тэги",
"Title:": "Название инцидента:",
"Content:": "Содержание инцидента:",
"Post": "Опубликовать",
Title: "Название инцидента:",
Content: "Содержание инцидента:",
Post: "Опубликовать",
"Cancel": "Отмена",
"Created:": "Создано:",
"Created": "Создано",
"Unpin": "Открепить",
"Show Tags": "Показать тэги",
"Recent": "Текущее",
"recent": "Сейчас",
"3h": "3 часа",
"6h": "6 часов",
"24h": "24 часа",
@ -326,6 +329,28 @@ export default {
"Add one": "Добавить новый",
"Backup": "Резервная копия",
"Security": "Безопасность",
"Current User:": "Текущий пользователь:",
"All Systems Operational": "Все системы работают",
"Shrink Database": "Сжать Базу Данных",
"Current User": "Текущий пользователь",
"About": "О программе",
"Description": "Описание",
"Powered by": "Работает на основе скрипта от",
shrinkDatabaseDescription: "Включает VACUUM для базы данных SQLite. Если ваша база данных была создана на версии 1.10.0 и более, AUTO_VACUUM уже включен и это действие не требуется.",
deleteStatusPageMsg: "Вы действительно хотите удалить эту страницу статуса сервисов?",
Style: "Стиль",
info: "ИНФО",
warning: "ВНИМАНИЕ",
danger: "ОШИБКА",
primary: "ОСНОВНОЙ",
light: "СВЕТЛЫЙ",
dark: "ТЕМНЫЙ",
"New Status Page": "Новый мониторинг",
"Show update if available": "Показывать доступные обновления",
"Also check beta release": "Проверять обновления для бета версий",
"Add New Status Page": "Добавить страницу мониторинга",
"Next": "Далее",
"Accept characters: a-z 0-9 -": "Разрешены символы: a-z 0-9 -",
"Start or end with a-z 0-9 only": "Начало и окончание имени только на символы: a-z 0-9",
"No consecutive dashes --": "Запрещено использовать тире --",
"HTTP Options": "HTTP Опции",
"Basic Auth": "HTTP Авторизация",
};

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "Pavza",
deleteMonitorMsg: "Ste prepričani, da želite izbrisati ta monitor?",
deleteNotificationMsg: "Ste prepričani, da želite izbrisati to obvestilo za vse monitorje?",
resoverserverDescription: "Cloudflare je privzeti strežnik. DNS strežnik lahko spremenite kadarkoli.",
resolverserverDescription: "Cloudflare je privzeti strežnik. DNS strežnik lahko spremenite kadarkoli.",
rrtypeDescription: "Izberite RR tip, ki ga želite spremljati",
pauseMonitorMsg: "Ste prepričani, da želite pavzirati?",
enableDefaultNotificationDescription: "To obvestilo bo kot privzeto omogočeno za vse nove monitorje. Še vedno ga lahko izključite posebej za vsak monitor.",
@ -182,7 +182,8 @@ export default {
"Add a monitor": "Dodaj monitor",
"Edit Status Page": "Uredi statusno stran",
"Go to Dashboard": "Pojdi na nadzorno ploščo",
"Status Page": "Status",
"Status Page": "Página de Status",
"Status Pages": "Página de Status",
defaultNotificationName: "Moje {notification} Obvestilo ({number})",
here: "tukaj",
Required: "Obvezno",
@ -339,7 +340,6 @@ export default {
"No monitors available.": "Nobenega monitorja ni na voljo.",
"Add one": "Dodaj enega",
"No Monitors": "Ni monitorjev",
"Add one": "Dodaj enega",
"Untitled Group": "Skupina brez imena",
Services: "Storitve",
Discard: "zavrzi",
@ -352,4 +352,4 @@ export default {
serwersmsPhoneNumber: "Telefonska številka",
serwersmsSenderName: "Ime SMS pošiljatelja (registrirani prek portala za stranke)",
"stackfield": "Stackfield",
};
};

@ -12,7 +12,7 @@ export default {
pauseDashboardHome: "Pauziraj",
deleteMonitorMsg: "Da li ste sigurni da želite da obrišete ovog posmatrača?",
deleteNotificationMsg: "Da li ste sigurni d aželite da uklonite ovo obaveštenje za sve posmatrače?",
resoverserverDescription: "Cloudflare je podrazumevani server. Možete promeniti server za raszrešavanje u bilo kom trenutku.",
resolverserverDescription: "Cloudflare je podrazumevani server. Možete promeniti server za raszrešavanje u bilo kom trenutku.",
rrtypeDescription: "Odaberite RR-Type koji želite da posmatrate",
pauseMonitorMsg: "Da li ste sigurni da želite da pauzirate?",
Settings: "Podešavanja",
@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page",
"Status Pages": "Status Pages",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",

@ -12,7 +12,7 @@ export default {
pauseDashboardHome: "Паузирај",
deleteMonitorMsg: "Да ли сте сигурни да желите да обришете овог посматрача?",
deleteNotificationMsg: "Да ли сте сигурни д ажелите да уклоните ово обавештење за све посматраче?",
resoverserverDescription: "Cloudflare је подразумевани сервер. Можете променити сервер за расзрешавање у било ком тренутку.",
resolverserverDescription: "Cloudflare је подразумевани сервер. Можете променити сервер за расзрешавање у било ком тренутку.",
rrtypeDescription: "Одаберите RR-Type који желите да посматрате",
pauseMonitorMsg: "Да ли сте сигурни да желите да паузирате?",
Settings: "Подешавања",
@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page",
"Status Pages": "Status Pages",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",

@ -12,7 +12,7 @@ export default {
pauseDashboardHome: "Pausa",
deleteMonitorMsg: "Är du säker på att du vill ta bort den här övervakningen?",
deleteNotificationMsg: "Är du säker på att du vill ta bort den här notisen för alla övervakare?",
resoverserverDescription: "Cloudflare är den förvalda servern. Du kan byta resolver när som helst.",
resolverserverDescription: "Cloudflare är den förvalda servern. Du kan byta resolver när som helst.",
rrtypeDescription: "Välj den RR-typ du vill övervaka",
pauseMonitorMsg: "Är du säker på att du vill pausa?",
Settings: "Inställningar",
@ -108,94 +108,4 @@ export default {
"Repeat Password": "Upprepa Lösenord",
respTime: "Svarstid (ms)",
notAvailableShort: "Ej Tillg.",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
"Apply on all existing monitors": "Apply on all existing monitors",
"Verify Token": "Verify Token",
"Setup 2FA": "Setup 2FA",
"Enable 2FA": "Enable 2FA",
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Active",
Inactive: "Inactive",
Token: "Token",
"Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exist.",
"Tag with this value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
statusPageNothing: "Nothing here, please add a group or a monitor.",
"No Services": "No Services",
"All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service",
"Degraded Service": "Degraded Service",
"Add Group": "Add Group",
"Add a monitor": "Add a monitor",
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",
discord: "Discord",
teams: "Microsoft Teams",
signal: "Signal",
gotify: "Gotify",
slack: "Slack",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (Support 50+ Notification services)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
};

@ -12,7 +12,7 @@ export default {
pauseDashboardHome: "Durdur",
deleteMonitorMsg: "Servisi silmek istediğinden emin misin?",
deleteNotificationMsg: "Bu bildirimi tüm servisler için silmek istediğinden emin misin?",
resoverserverDescription: "Cloudflare varsayılan sunucudur, çözümleyici sunucusunu istediğiniz zaman değiştirebilirsiniz.",
resolverserverDescription: "Cloudflare varsayılan sunucudur, çözümleyici sunucusunu istediğiniz zaman değiştirebilirsiniz.",
rrtypeDescription: "İzlemek istediğiniz servisin RR-Tipini seçin",
pauseMonitorMsg: "Durdurmak istediğinden emin misin?",
clearEventsMsg: "Bu servisin bütün kayıtlarını silmek istediğinden emin misin?",
@ -124,7 +124,7 @@ export default {
tokenValidSettingsMsg: "Token geçerli! Şimdi 2FA ayarlarını kaydedebilirsiniz. ",
confirmEnableTwoFAMsg: "2FA'ı etkinleştirmek istediğinizden emin misiniz?",
confirmDisableTwoFAMsg: "2FA'ı devre dışı bırakmak istediğinize emin misiniz?",
"Heartbeat Retry Interval": "Sağlık Dırımları Tekrar Deneme Sıklığı",
"Heartbeat Retry Interval": "Sağlık Durumları Tekrar Deneme Sıklığı",
"Import Backup": "Yedeği içe aktar",
"Export Backup": "Yedeği dışa aktar",
Export: "Dışa aktar",
@ -149,52 +149,4 @@ export default {
"Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)",
Active: "Aktif",
Inactive: "İnaktif",
Token: "Token",
"Show URI": "Show URI",
Tags: "Tags",
"Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exist.",
"Tag with this value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
statusPageNothing: "Nothing here, please add a group or a monitor.",
"No Services": "No Services",
"All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service",
"Degraded Service": "Degraded Service",
"Add Group": "Add Group",
"Add a monitor": "Add a monitor",
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",
discord: "Discord",
teams: "Microsoft Teams",
signal: "Signal",
gotify: "Gotify",
slack: "Slack",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (Support 50+ Notification services)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
};

@ -1,31 +1,31 @@
export default {
languageName: "Vietnamese",
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",
acceptedStatusCodesDescription: "Chọn mã code trạng thái được coi là phản hồi thành cô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.",
keywordDescription: "Từ khoá tìm kiếm phản hồi ở dạng html hoặc JSON, có phân biệt chữ HOA - thường",
pauseDashboardHome: "Tạm dừng",
deleteMonitorMsg: "Bạn chắc chắn muốn xóa monitor này chứ?",
deleteNotificationMsg: "Bạn có chắc chắn muốn xóa kênh thông báo này cho tất cả monitor?",
resoverserverDescription: "Cloudflare là máy chủ mặc định, bạn có thể thay đổi bất cứ lúc nào.",
deleteMonitorMsg: "Bạn chắc chắn muốn xóa kênh theo dõi này chứ?",
deleteNotificationMsg: "Bạn có chắc chắn muốn xóa kênh thông báo này cho tất cả kênh theo dõi?",
resolverserverDescription: "Cloudflare là máy chủ mặc định, bạn có thể thay đổi bất cứ lúc nào.",
rrtypeDescription: "Hãy chọn RR-Type mà bạn muốn giám sát",
pauseMonitorMsg: "Bạn chắc chắn muốn tạm dừng chứ?",
enableDefaultNotificationDescription: "Bật làm mặc định cho mọi monitor mới về sau. Bạn vẫn có thể tắt thông báo riêng cho từng monitor.",
clearEventsMsg: "Bạn chắc chắn muốn xoá TẤT CẢ sự kiện cho monitor này chứ?",
clearHeartbeatsMsg: "Bạn chắc chắn muốn xoá TẤT CẢ heartbeats cho monitor này chứ?",
enableDefaultNotificationDescription: "Bật làm mặc định cho mọi kênh theo dõi mới về sau. Bạn vẫn có thể tắt thông báo riêng cho từng kênh theo dõi.",
clearEventsMsg: "Bạn chắc chắn muốn xoá TẤT CẢ sự kiện cho kênh theo dõi này chứ?",
clearHeartbeatsMsg: "Bạn chắc chắn muốn xoá TẤT CẢ heartbeats cho kênh theo dõi này chứ?",
confirmClearStatisticsMsg: "Bạn chắc chắn muốn xoá TẤT CẢ số liệu thống kê?",
importHandleDescription: "Chọn 'Skip existing' nếu bạn muốn bỏ qua mọi monitor và kênh thông báo trùng tên. 'Overwrite' sẽ ghi đè lên tất cả các monitor và kênh thông báo.",
importHandleDescription: "Chọn 'Giữ lại' nếu bạn muốn bỏ qua mọi kênh theo dõi và kênh thông báo trùng tên. 'Ghi đè' sẽ ghi đè lên tất cả các kênh theo dõi và kênh thông báo.",
confirmImportMsg: "Bạn có chắc chắn muốn khôi phục bản bản sao lưu này không?.",
twoFAVerifyLabel: "Vui lòng nhập mã token của bạn để xác minh rằng 2FA đang hoạt động",
tokenValidSettingsMsg: "Mã token hợp lệ! Bạn có thể lưu cài đặt 2FA bây giờ.",
confirmEnableTwoFAMsg: "Bạn chắc chắn muốn bật 2FA chứ?",
confirmDisableTwoFAMsg: "Bạn chắc chắn muốn tắt 2FA chứ?",
twoFAVerifyLabel: "Vui lòng nhập mã token của bạn để xác minh rằng xác thực 2 lớp (2FA) đang hoạt động",
tokenValidSettingsMsg: "Mã token hợp lệ! Bạn có thể lưu cài đặt xác thực 2 lớp (2FA) bây giờ.",
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",
"New Update": "Bản cập nhật mới",
@ -38,7 +38,7 @@ export default {
"Check Update On GitHub": "Kiểm tra bản cập nhật mới trên GitHub",
List: "List",
Add: "Thêm",
"Add New Monitor": "Thêm mới Monitor",
"Add New Monitor": "Thêm mới kênh theo dõi",
"Quick Stats": "Thống kê nhanh",
Up: "Up",
Down: "Down",
@ -63,9 +63,9 @@ export default {
"-hour": "-giờ",
Response: "Phản hồi",
Ping: "Ping",
"Monitor Type": "Kiểu monitor",
"Monitor Type": "Kiểu kênh theo dõi",
Keyword: "Từ khoá",
"Friendly Name": "Tên monitor",
"Friendly Name": "Tên rút gọn",
URL: "URL",
Hostname: "Hostname",
Port: "Port",
@ -111,7 +111,7 @@ export default {
Password: "Mật khẩu",
"Remember me": "Lưu phiên đăng nhập",
Login: "Đăng nhập",
"No Monitors, please": "Không có monitor nào",
"No Monitors, please": "Không có kênh theo dõi nào",
"add one": "Thêm mới",
"Notification Type": "Kiểu thông báo",
Email: "Email",
@ -125,31 +125,31 @@ export default {
"Import Backup": "Khôi phục bản sao lưu",
"Export Backup": "Xuất bản sao lưu",
Export: "Xuất",
Import: "Khôi phục",
Import: "Nhập",
respTime: "Thời gian phản hồi (ms)",
notAvailableShort: "N/A",
"Default enabled": "Mặc định bật",
"Apply on all existing monitors": "Áp dụng cho tất cả monitor đang có",
"Apply on all existing monitors": "Áp dụng cho tất cả kênh theo dõi đang có",
Create: "Tạo",
"Clear Data": "Xoá dữ liệu",
Events: "Sự kiện",
Heartbeats: "Heartbeats",
"Auto Get": "Tự động lấy",
backupDescription: "Bạn có thể sao lưu tất cả các monitor và tất cả các thông báo vào một file JSON.",
backupDescription2: "PS: Không bao gồm dữ liệu lịch sử các sự kiện.",
backupDescription3: "Hãy lưu giữ file này cẩn thận vì trong file đó chứa cả các mã token thông báo.",
backupDescription: "Sao lưu tất cả các kênh theo dõi và tất cả các thông báo vào một file định dạng JSON.",
backupDescription2: "Lưu ý: Không bao gồm dữ liệu lịch sử các sự kiện.",
backupDescription3: "Hãy lưu giữ file này cẩn thận, trong file đó chứa cả các token thông báo.",
alertNoFile: "Hãy chọn file để khôi phục.",
alertWrongFileType: "Hãy chọn file JSON.",
alertWrongFileType: "Hãy chọn file định dạng JSON.",
"Clear all statistics": "Xoá tất cả thống kê",
"Skip existing": "Skip existing",
"Skip existing": "Giữ lại",
Overwrite: "Ghi đè",
Options: "Tuỳ chọn",
"Keep both": "Giữ lại cả hai",
"Verify Token": "Xác minh Token",
"Setup 2FA": "Cài đặt 2FA",
"Enable 2FA": "Bật 2FA",
"Disable 2FA": "Tắt 2FA",
"2FA Settings": "Cài đặt 2FA",
"Setup 2FA": "Cài đặt xác thực 2 lớp (2FA)",
"Enable 2FA": "Bật xác thực 2 lớp (2FA)",
"Disable 2FA": "Tắt xác thực 2 lớp (2FA)",
"2FA Settings": "Cài đặt xác thực 2 lớp (2FA)",
"Two Factor Authentication": "Xác thực hai yếu tố",
Active: "Hoạt động",
Inactive: "Ngừng hoạt động",
@ -170,19 +170,20 @@ export default {
Purple: "Tím",
Pink: "Hồng",
"Search...": "Tìm kiếm...",
"Avg. Ping": "Ping Trung bình",
"Avg. Ping": "Ping trung bình",
"Avg. Response": "Phản hồi trung bình",
"Entry Page": "Entry Page",
statusPageNothing: "Không có gì, hãy thêm nhóm monitor hoặc monitor.",
statusPageNothing: "Chưa có thông tin gì, hãy thêm nhóm kênh theo dõi hoặc kênh theo dõi.",
"No Services": "Không có dịch vụ",
"All Systems Operational": "Tất cả các hệ thống hoạt động bình thường",
"Partially Degraded Service": "Dịch vụ xuống cấp một phần",
"Degraded Service": "Degraded Service",
"Partially Degraded Service": "Có hệ thống bị ngưng",
"Degraded Service": "Toàn bộ hệ thống bị ngưng",
"Add Group": "Thêm nhóm",
"Add a monitor": "Thêm monitor",
"Add a monitor": "Thêm kênh theo dõi",
"Edit Status Page": "Sửa trang trạng thái",
"Go to Dashboard": "Đi tới Dashboard",
"Status Page": "Trang trạng thái",
"Status Pages": "Trang trạng thái",
defaultNotificationName: "My {notification} Alerts ({number})",
here: "tại đây",
Required: "Bắt buộc",
@ -195,23 +196,23 @@ export default {
"YOUR BOT TOKEN HERE": "MÃ BOT TOKEN CỦA BẠN",
chatIDNotFound: "Không tìm thấy Chat ID, vui lòng gửi tin nhắn cho bot này trước",
webhook: "Webhook",
"Post URL": "URL đăng",
"Post URL": "URL webhook",
"Content Type": "Loại nội dung",
webhookJsonDesc: "{0} phù hợp với bất kỳ máy chủ http hiện đại nào như express.js",
webhookFormDataDesc: "{multipart} phù hợp với PHP, bạn chỉ cần phân tích cú pháp json bằng {decodeFunction}",
webhookJsonDesc: "{0} tương thích với máy chủ HTTP ví dụ như Express.js",
webhookFormDataDesc: "{multipart} tương thích với máy chủ PHP, bạn chỉ cần phân tích cú pháp json bằng {decodeFunction}",
smtp: "Email (SMTP)",
secureOptionNone: "None / STARTTLS (25, 587)",
secureOptionNone: "None/STARTTLS(25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "Bỏ qua lỗi TLS",
"From Email": "Từ Email",
"From Email": "Email gửi",
emailCustomSubject: "Tuỳ chỉnh tiêu đề",
"To Email": "Tới Email",
"To Email": "Email nhận",
smtpCC: "CC",
smtpBCC: "BCC",
discord: "Discord",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "Để lấy Discord, hãy vào: Server Settings -> Integrations -> Create Webhook",
"Bot Display Name": "Tên hiển thị của Bot",
"Bot Display Name": "Tên hiển thị của BOT",
"Prefix Custom Message": "Tiền tố tin nhắn tuỳ chọn",
"Hello @everyone is...": "Xin chào {'@'} mọi người đang...",
teams: "Microsoft Teams",
@ -220,8 +221,8 @@ export default {
signal: "Tín hiệu",
Number: "Số",
Recipients: "Người nhận",
needSignalAPI: "Bạn cần một tín hiệu client với REST API.",
wayToCheckSignalURL: "Bạn có thể kiểm tra url này để xem cách thiết lập:",
needSignalAPI: "Bạn cần một tín hiệu kết nối với REST API.",
wayToCheckSignalURL: "Bạn có thể kiểm tra URL này để xem cách thiết lập:",
signalImportant: "QUAN TRỌNG: Bạn không thể kết hợp các nhóm và số trong người nhận!",
gotify: "Gotify",
"Application Token": "Mã Token ứng dụng",
@ -242,7 +243,8 @@ export default {
promosms: "PromoSMS",
clicksendsms: "ClickSend SMS",
lunasea: "LunaSea",
apprise: "Apprise (Hỗ trợ 50+ dịch vụ thông báo)",
apprise: "Apprise (Hỗ trợ trên 50 dịch vụ thông báo)",
GoogleChat: "Google Chat (Google Workspace only)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
@ -260,7 +262,7 @@ export default {
apiCredentials: "API credentials",
octopushLegacyHint: "Bạn muốn sử dụng phiên bản cũ của Octopush (2011-2020) hay phiên bản mới?",
"Check octopush prices": "Kiểm tra giá octopush {0}.",
octopushPhoneNumber: "Số điện thoại (Định dạng intl, vd : +84123456789) ",
octopushPhoneNumber: "Số điện thoại (Định dạng intl, vd : +84692341165) ",
octopushSMSSender: "SMS người gửi : 3-11 ký tự chữ, số và dấu cách (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea ID thiết bị",
"Apprise URL": "Apprise URL",
@ -299,12 +301,65 @@ export default {
PushUrl: "Push URL",
HeadersInvalidFormat: "Header request không hợp lệ JSON: ",
BodyInvalidFormat: "Tequest body không hợp lệ JSON: ",
"Monitor History": "Lịch sử Monitor",
clearDataOlderThan: "Giữ dữ liệu lịch sử monitor {0} ngày.",
"Monitor History": "Lịch sử kênh theo dõi",
clearDataOlderThan: "Giữ dữ liệu lịch sử kênh theo dõi {0} ngày.",
PasswordsDoNotMatch: "Passwords không khớp.",
records: "records",
"One record": "One record",
steamApiKeyDescription: "Để monitor các Steam Game Server bạn cần một Steam Web-API key. Bạn có thể đăng ký API key tại đây: ",
steamApiKeyDescription: "Để theo dõi các Steam Game Server bạn cần một Steam Web-API key. Bạn có thể đăng ký API key tại đây: ",
"Current User": "User hiện tại",
recent: "Gần đây",
Done: "Hoàn thành",
Info: "Thông tin",
Security: "Bảo mật",
"Steam API Key": "Steam API Key",
"Shrink Database": "Shrink Database",
"Pick a RR-Type...": "Pick a RR-Type...",
"Pick Accepted Status Codes...": "Chọn các Codes trạng thái chấp nhận được...",
Default: "Mặc định",
"HTTP Options": "Tuỳ chọn HTTP",
"Create Incident": "Tạo Incident",
Title: "Tiêu đề",
Content: "Nội dung",
Style: "Style",
info: "thông tin",
warning: "cảnh báo",
danger: "nguy hiểm",
primary: "cơ sở",
light: "sáng",
dark: "tối",
Post: "Post",
"Please input title and content": "Hãy nhập tiêu đề và nội dung",
Created: "Đã tạo",
"Last Updated": "Cập nhật mới nhất",
Unpin: "Bỏ ghim",
"Switch to Light Theme": "Chuyển sang giao diện Sáng",
"Switch to Dark Theme": "Chuyển sang giao diện Tối",
"Show Tags": "Hiện Tags",
"Hide Tags": "Ẩn Tags",
Description: "Mô tả",
"No monitors available.": "Không có kênh theo dõi nào.",
"Add one": "Thêm mới",
"No Monitors": "Không có kênh theo dõi",
"Untitled Group": "Nhóm không có tiêu đề",
Services: "Dịch vụ",
Discard: "Bỏ",
Cancel: "Hủy",
"Powered by": "Được cung cấp bởi",
shrinkDatabaseDescription: "Khởi chạy database VACCUM cho SQLite. Nếu database được tạo sau version 1.10.0, AUTO_VACCUM đã được bật sẵn, hành động này không cần thiết.",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API Username (incl. webapi_ prefix)",
serwersmsAPIPassword: "API Password",
serwersmsPhoneNumber: "Số điện thoại",
serwersmsSenderName: "Tên người gửi SMS (Đã đăng ký qua portal)",
"stackfield": "Stackfield",
smtpDkimSettings: "Cài đặt xác thực Email(DKIM)",
smtpDkimDesc: "Xem hướng dẫn tại {0}.",
documentation: "Nodemailer DKIM",
smtpDkimDomain: "Mail domain",
smtpDkimKeySelector: "DKIM Key Selector",
smtpDkimPrivateKey: "Private Key",
smtpDkimHashAlgo: "Hash Algorithm (Tuỳ chọn)",
smtpDkimheaderFieldNames: "Header Keys to sign (Tuỳ chọn)",
smtpDkimskipFields: "Header Keys not to sign (Tuỳ chọn)",
};

@ -1,91 +1,104 @@
export default {
languageName: "简体中文",
checkEverySecond: "检测频率 {0} 秒",
retriesDescription: "最大重试失败次数",
ignoreTLSError: "忽略HTTPS站点的证书错误",
upsideDownModeDescription: "反向状态监控(状态码范围外为有效状态,反之为无效)",
maxRedirectDescription: "最大重定向次数,设置为 0 禁止重定向",
acceptedStatusCodesDescription: "选择被视为成功响应的状态码",
passwordNotMatchMsg: "两次密码输入不一致",
notificationDescription: "请为监控项配置消息通知",
keywordDescription: "检测响应内容中的关键字,区分大小写",
retryCheckEverySecond: "重试间隔 {0} 秒",
retriesDescription: "服务被标记为故障并发送通知之前得最大重试次数",
ignoreTLSError: "忽略 HTTPS 站点的 TLS/SSL 错误",
upsideDownModeDescription: "反转状态监控,如果服务可访问,则认为是故障。",
maxRedirectDescription: "允许的最大重定向次数。设置为 0 禁用重定向。",
acceptedStatusCodesDescription: "选择被视为成功响应的状态码。",
passwordNotMatchMsg: "两次输入的密码不一致。",
notificationDescription: "通知必须被分配给监控项才能正常工作。",
keywordDescription: "在纯 HTML 或 JSON 响应中搜索关键字,区分大小写。",
pauseDashboardHome: "暂停",
deleteMonitorMsg: "确定要删除此监控吗?",
deleteNotificationMsg: "确定要删除此消息通知吗?这将对所有监控生效。",
resoverserverDescription: "可自定义要使用的DNS服务器",
deleteMonitorMsg: "确定要删除此监控吗?",
deleteNotificationMsg: "确定要为所有监控项删除此通知吗?",
resolverserverDescription: "默认服务器是 Cloudflare。您随时可以修改解析服务器。",
rrtypeDescription: "选择要监控的资源记录类型",
pauseMonitorMsg: "确定要暂停吗?",
enableDefaultNotificationDescription: "新的监控项将默认启用此通知,您仍然为每个监控项单独禁用。",
clearEventsMsg: "确定要删除此监控项的所有事件吗?",
clearHeartbeatsMsg: "确定要删除此监控项的所有心跳状态吗?",
confirmClearStatisticsMsg: "确定要删除所有统计信息吗?",
importHandleDescription: "如果想跳过同名的监控项或消息通知,请选择“跳过已存在”。“覆盖”将删除所有现有的监控项和通知。",
confirmImportMsg: "确定要导入备份吗?请确保已经选择了正确的导入选项。",
twoFAVerifyLabel: "请输入令牌码以确认二次验证:",
tokenValidSettingsMsg: "令牌码有效!您现在可以保存二次验证设置了。",
confirmEnableTwoFAMsg: "确定要启用二次验证吗?",
confirmDisableTwoFAMsg: "确定要禁用二次验证吗?",
Settings: "设置",
Dashboard: "仪表盘",
"New Update": "有新版本更新",
"New Update": "有新版本",
Language: "语言",
Appearance: "外观设置",
Appearance: "外观",
Theme: "主题",
General: "基本设置",
"Primary Base URL": "站点地址URL",
General: "常规",
"Primary Base URL": "站点主 URL",
About: "关于",
Version: "版本",
"Check Update On GitHub": "检查更新",
"Check Update On GitHub": "检查 GitHub 上的更新",
List: "列表",
Add: "添加",
"Add New Monitor": "创建监控项",
"Add New Monitor": "添加监控项",
"Quick Stats": "状态速览",
Up: "正常",
Down: "故障",
Pending: "检测失败",
Pending: "正在检测",
Unknown: "未知",
Pause: "暂停",
Name: "名称",
Status: "状态",
DateTime: "时间",
Message: "事件",
DateTime: "日期时间",
Message: "消息",
"No important events": "暂无重要事件",
Resume: "恢复",
Edit: "修改",
Edit: "编辑",
Delete: "删除",
Current: "当前",
Uptime: "可用率",
Uptime: "在线时间",
"Cert Exp.": "证书有效期",
days: "天",
day: "天",
"-day": " 天",
hour: "小时",
"-hour": " 小时",
Response: "响应时长",
Response: "响应",
Ping: "Ping",
"Monitor Type": "监控类型",
Keyword: "关键字",
"Friendly Name": "自定义名称",
URL: "网址URL",
"Friendly Name": "显示名称",
URL: "URL",
Hostname: "主机名",
Port: "端口号",
"Heartbeat Interval": "心跳间隔",
Retries: "重试次数",
Advanced: "高级选项",
"Upside Down Mode": "反向监控",
"Max. Redirects": "重定向次数",
"Heartbeat Retry Interval": "心跳重试间隔",
Advanced: "高级",
"Upside Down Mode": "反转监控",
"Max. Redirects": "最大重定向次数",
"Accepted Status Codes": "有效状态码",
"Push URL": "推送链接",
needPushEvery: "你需要每 {0} 秒调用一次",
"Push URL": "推送 URL",
needPushEvery: "您需要每 {0} 秒调用一次该 URL",
pushOptionalParams: "可选参数:{0}",
Save: "保存",
Notifications: "消息通知",
"Not available, please setup.": "无可用通道,请先设置",
Notifications: "通知",
"Not available, please setup.": "暂不可用,请先设置",
"Setup Notification": "设置通知",
Light: "明亮",
Dark: "黑暗",
Auto: "自动",
"Theme - Heartbeat Bar": "状态显示",
"Theme - Heartbeat Bar": "主题 - 心跳栏",
Normal: "正常显示",
Bottom: "靠下显示",
None: "不显示",
Timezone: "时区",
"Search Engine Visibility": "搜索引擎设置",
"Search Engine Visibility": "搜索引擎可见性",
"Allow indexing": "允许索引",
"Discourage search engines from indexing site": "阻止搜索引擎索引网站",
"Change Password": "修改密码",
"Current Password": "当前密码",
"New Password": "新密码",
"Repeat New Password": "重复新密码",
"New Password": "新密码",
"Repeat New Password": "重复新密码",
"Update Password": "更新密码",
"Disable Auth": "禁用身份验证",
"Enable Auth": "启用身份验证",
@ -93,73 +106,61 @@ export default {
Leave: "离开",
"I understand, please disable": "我已了解,继续禁用",
Confirm: "确认",
Yes: "确定",
No: "取消",
Yes: "",
No: "",
Username: "用户名",
Password: "密码",
"Remember me": "记住登录",
"Remember me": "记住",
Login: "登录",
"No Monitors, please": "还没有监控项,",
"add one": "点击新增",
"Notification Type": "消息类型",
"add one": "点击添加",
"Notification Type": "通知类型",
Email: "邮件",
Test: "测试一下",
Test: "测试",
"Certificate Info": "证书信息",
"Resolver Server": "解析服务器",
"Resource Record Type": "资源记录类型",
"Last Result": "最后结果",
"Create your admin account": "创建管理员账",
"Last Result": "上次结果",
"Create your admin account": "创建管理员账",
"Repeat Password": "重复密码",
Backup: "备份",
"Import Backup": "导入备份",
"Export Backup": "导出备份",
Export: "导出",
Import: "导入",
respTime: "响应时间(毫秒)",
notAvailableShort: "N/A",
"Default enabled": "默认开启",
"Apply on all existing monitors": "应用到所有现有监控项",
Create: "创建",
clearEventsMsg: "确定要删除此监控项的所有事件吗?",
clearHeartbeatsMsg: "确定要删除此监控项的所有状态吗?",
confirmClearStatisticsMsg: "确定要删除所有统计信息吗?",
"Clear Data": "清除数据",
Events: "事件",
Heartbeats: "心跳",
"Auto Get": "自动获取",
enableDefaultNotificationDescription: "新的监控项将默认启用,你也可以在每个监控项中分别设置",
"Default enabled": "默认开启",
"Also apply to existing monitors": "应用到所有监控项",
Export: "导出",
Import: "导入",
backupDescription: "你可以将所有的监控项和消息通知备份到一个 JSON 文件中",
backupDescription2: "注意: 不包括历史状态和事件数据",
backupDescription3: "导出的文件中可能包含敏感信息,如消息通知的 Token 信息,请小心存放!",
alertNoFile: "请选择一个文件导入",
alertWrongFileType: "请选择一个 JSON 格式的文件",
twoFAVerifyLabel: "请输入Token以验证 2FA二次验证是否正常工作",
tokenValidSettingsMsg: "Token有效您现在可以保存 2FA二次验证设置",
confirmEnableTwoFAMsg: "确定要启用 2FA二次验证吗?",
confirmDisableTwoFAMsg: "确定要禁用 2FA二次验证吗?",
"Apply on all existing monitors": "应用到所有监控项",
"Verify Token": "验证 Token",
"Setup 2FA": "设置 2FA",
"Enable 2FA": "启用 2FA",
"Disable 2FA": "禁用 2FA",
"2FA Settings": "2FA 设置",
"Two Factor Authentication": "双因素认证",
Active: "生效",
Inactive: "未生效",
Token: "Token",
"Show URI": "显示链接",
backupDescription: "您可以将所有监控项和通知备份到 JSON 文件。",
backupDescription2: "注意: 不包括历史状态和事件数据。",
backupDescription3: "导出的文件可能包含敏感信息,例如通知的令牌信息,请小心存放!",
alertNoFile: "请选择要导入的文件",
alertWrongFileType: "请选择一个 JSON 文件",
"Clear all statistics": "清除所有统计数据",
retryCheckEverySecond: "重试间隔 {0} 秒",
importHandleDescription: "如果想跳过同名的监控项或通知,请选择“跳过”;“覆盖”将删除所有现有的监控项和通知。",
confirmImportMsg: "确定要导入备份吗?请确保已经选择了正确的导入选项。",
"Heartbeat Retry Interval": "心跳重试间隔",
"Import Backup": "导入备份",
"Export Backup": "导出备份",
"Skip existing": "跳过",
"Skip existing": "跳过已存在",
Overwrite: "覆盖",
Options: "选项",
"Keep both": "全部保留",
"Verify Token": "验证令牌",
"Setup 2FA": "设置二次验证",
"Enable 2FA": "启用二次验证",
"Disable 2FA": "禁用二次验证",
"2FA Settings": "二次验证设置",
"Two Factor Authentication": "二次验证",
Active: "激活",
Inactive: "停用",
Token: "令牌",
"Show URI": "显示 URI",
Tags: "标签",
"Add New below or Select...": "在下面新增或选择...",
"Tag with this name already exist.": "相同名称的标签已存在",
"Tag with this value already exist.": "相同内容的标签已存在",
"Add New below or Select...": "在下面添加或选择...",
"Tag with this name already exist.": "相同名称的标签已存在。",
"Tag with this value already exist.": "相同内容的标签已存在。",
color: "颜色",
"value (optional)": "值(可选)",
Gray: "灰色",
@ -179,141 +180,142 @@ export default {
"All Systems Operational": "所有服务运行正常",
"Partially Degraded Service": "部分服务出现故障",
"Degraded Service": "全部服务出现故障",
"Add Group": "新建分组",
"Add Group": "添加分组",
"Add a monitor": "添加监控项",
"Edit Status Page": "编辑状态页",
"Edit Status Page": "编辑状态页",
"Go to Dashboard": "前往仪表盘",
"Status Page": "状态页",
telegram: "Telegram",
webhook: "Webhook",
smtp: "电子邮件SMTP",
discord: "Discord",
teams: "Microsoft Teams",
signal: "Signal",
gotify: "Gotify",
slack: "Slack",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (支持50+种通知服务)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"Feishu WebHookUrl": "飞书 WebHook 地址",
"Status Page": "状态页面",
"Status Pages": "状态页面",
defaultNotificationName: "{notification} 通知({number}",
here: "这里",
Required: "必填",
telegram: "Telegram",
"Bot Token": "Bot Token",
wayToGetTelegramToken: "可以从 {0} 获取 Token。",
wayToGetTelegramToken: "您可以从 {0} 获取 Token。",
"Chat ID": "Chat ID",
supportTelegramChatID: "支持对话/群组/频道的 ID",
wayToGetTelegramChatID: "你可以发送一条消息给你的机器人然后到下面的链接来查看你的 chat_id",
"YOUR BOT TOKEN HERE": "这里替换成你的 BOT TOKEN",
chatIDNotFound: "没有找到 Chat ID请先给你的机器人发送一条消息。",
"Post URL": "目标链接",
supportTelegramChatID: "支持对话/群组/频道的 Chat ID",
wayToGetTelegramChatID: "您可以发送一条消息给您的机器人,然后访问此链接来查看 chat_id",
"YOUR BOT TOKEN HERE": "这里替换成您的 BOT TOKEN",
chatIDNotFound: "未找到 Chat ID请先给您的机器人发送一条消息。",
webhook: "Webhook",
"Post URL": "Post URL",
"Content Type": "Content Type",
webhookJsonDesc: "{0} 适合现代的服务,比如 express.js",
webhookFormDataDesc: "{multipart} 适合PHP解码使用 {decodeFunction}",
secureOptionNone: "无 / STARTTLS25587",
secureOptionTLS: "TLS465",
webhookJsonDesc: "{0} 适合现代的 HTTP 服务器,例如 Express.js",
webhookFormDataDesc: "{multipart} 适合 PHP其中 JSON 需要使用 {decodeFunction} 解码",
smtp: "电子邮件SMTP",
secureOptionNone: "无 / STARTTLS常用端口 25、587",
secureOptionTLS: "TLS常用端口 465",
"Ignore TLS Error": "忽略 TLS 错误",
"From Email": "发信人",
emailCustomSubject: "邮件主题",
"To Email": "收信人",
smtpCC: "抄送",
smtpBCC: "密送",
"Discord Webhook URL": "Discord Webhook 链接",
wayToGetDiscordURL: "获取方式:服务器设置 -> 整合 -> 创建 Webhook",
discord: "Discord",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "要获取,可以前往服务器设置 -> 整合 -> 创建 Webhook",
"Bot Display Name": "机器人显示名称",
"Prefix Custom Message": "自定义消息前缀",
"Hello @everyone is...": "{'@'}所有人,……",
"Webhook URL": "Webhook 链接",
wayToGetTeamsURL: "你可以在 {0} 获取 Webhook 链接。",
"Hello @everyone is...": "{'@'}everyone……",
teams: "Microsoft Teams",
"Webhook URL": "Webhook URL",
wayToGetTeamsURL: "您可以在 {0} 了解如何获取 Webhook URL。",
signal: "Signal",
Number: "号码",
Recipients: "收件人",
needSignalAPI: "你需要有一个带 REST API 的 Signal 客户端。",
wayToCheckSignalURL: "你可以通过下面的链接来了解如何设置:",
signalImportant: "重要:你不能混合设定收件人的分组和号码!",
needSignalAPI: "您需要有一个支持 REST API 的 Signal 客户端。",
wayToCheckSignalURL: "您可以通过下面的 URL 了解如何设置:",
signalImportant: "重要:您不能混合设定收件人的分组和号码!",
gotify: "Gotify",
"Application Token": "Application Token",
"Server URL": "服务器链接",
"Server URL": "服务器 URL",
Priority: "优先级",
slack: "Slack",
"Icon Emoji": "Emoji 图标",
"Channel Name": "频道名称",
"Uptime Kuma URL": "Uptime Kuma 链接",
"Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "关于 Webhook 的更多信息:{0}",
aboutChannelName: "如果你想绕过 Webhook 设定的频道,请在设定 {0} 的频道名称字段为你想要的频道。例:#other-channel",
aboutKumaURL: "如果保留 Uptime Kuma 链接为空,将会默认指向项目的 Github 页面。",
emojiCheatSheet: "Emoji 参考表:{0}",
aboutChannelName: "如果您想绕过 Webhook 频道,请在 {0} 字段输入所需的频道名称。例如:#other-channel",
aboutKumaURL: "如果保留 Uptime Kuma URL 为空,将会默认指向项目的 GitHub 页面。",
emojiCheatSheet: "Emoji 速查:{0}",
"rocket.chat": "Rocket.Chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
clicksendsms: "ClickSend SMS",
lunasea: "LunaSea",
apprise: "Apprise (支持 50+ 种通知服务)",
GoogleChat: "Google Chat仅 Google Workspace",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"User Key": "User Key",
Device: "设备",
"Message Title": "消息标题",
"Notification Sound": "通知铃声",
"More info on:": "更多信息:{0}",
pushoverDesc1: "紧急优先级2会在一小时内每30秒重试一次。",
pushoverDesc2: "如果你想发送通知给不同的设备,请填写“设备”字段。",
pushoverDesc1: "紧急优先级2会在一小时内每30 秒重试一次。",
pushoverDesc2: "如果想发送通知给不同的设备,请填写“设备”字段。",
"SMS Type": "短信类型",
octopushTypePremium: "Premium快 - 推荐用于警报)",
octopushTypeLowCost: "Low Cost慢 - 有时会被运营商屏蔽)",
checkPrice: "查看 {0} 的价格:",
apiCredentials: "API Credentials",
octopushLegacyHint: "您是否在使用旧版本的 Octopush2011-2020",
"Check octopush prices": "查看 Octopush 的价格 {0}。",
octopushPhoneNumber: "电话号码(国际格式,例:+33612345678",
octopushSMSSender: "短信发送名称3-11位大小写字母、数字和空格a-zA-Z0-9",
octopushPhoneNumber: "电话号码(国际格式,例+33612345678",
octopushSMSSender: "短信发送名称3-11 位大小写字母、数字和空格a-zA-Z0-9",
"LunaSea Device ID": "LunaSea 设备 ID",
"Apprise URL": "Apprise 链接",
"Example:": "例{0}",
"Apprise URL": "Apprise URL",
"Example:": "例{0}",
"Read more:": "了解更多:{0}",
"Status:": "状态:{0}",
"Read more": "了解更多",
appriseInstalled: "Apprise 已安装",
appriseNotInstalled: "Apprise 未安装。{0}",
"Access Token": "Access Token",
"Channel access token": "频道 access token",
"Line Developers Console": "Line Developers Console",
lineDevConsoleTo: "Line Developers Console - {0}",
"Basic Settings": "Basic Settings",
"User ID": "User ID",
"Channel access token": "频道 Access Token",
"Line Developers Console": "Line 开发者控制台",
lineDevConsoleTo: "Line 开发者控制台 - {0}",
"Basic Settings": "基本设置",
"User ID": "用户 ID",
"Messaging API": "Messaging API",
wayToGetLineChannelToken: "首先访问 {0}创建一个提供者和频道Messaging API然后你就可以从上面提到的地方获取频道的 access token 和用户 ID。",
"Icon URL": "图标链接",
aboutIconURL: "你可以在“Icon URL”中提供一个图片地址来覆盖默认的资料图片。如果设置了 Emoji 图标此字段会被忽略。",
aboutMattermostChannelName: "如果你想覆盖 Webhook 设定的频道,请在“频道名称”字段为你想要的频道。这需要在 Mattermost 的 Webhook 设定中启用。例#other-channel",
wayToGetLineChannelToken: "首先访问 {0}创建一个提供者和频道Messaging API然后您就可以从上面提到的菜单获取频道的 Access Token 和用户 ID。",
"Icon URL": "图标 URL",
aboutIconURL: "您可以在“图标 URL”中提供一个图片链接来覆盖默认的资料图片。如果设置了 Emoji 图标此字段会被忽略。",
aboutMattermostChannelName: "您可以覆盖 Webhook 发送消息的默认频道,只需在“频道名称”字段中输入您想要的频道名。这需要在 Mattermost 的 Webhook 设置中启用。例如#other-channel",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - 便宜但是慢,并且容易超负荷。仅限波兰地区的收件人。",
promosmsTypeFlash: "SMS FLASH - 消息会自动显示在收件人设备上。仅限波兰地区的收件人。",
promosmsTypeFull: "SMS FULL - 高等级,你可以使用你自己的发件人名称(你需要先注册一个). 对于警报来说更可靠。",
promosmsTypeSpeed: "SMS SPEED - 最高优先级。非常快速可靠,但更贵(越两倍 SMS FULL 等级的价格)。",
promosmsPhoneNumber: "电话号码(波兰地区收件人可以不填区号)",
promosmsSMSSender: "短信发件人名称已注册的名称或以下默认值之一InfoSMSSMS InfoMaxSMSINFOSMS",
checkPrice: "查看 {0} 的价格:",
octopushLegacyHint: "你是否在使用旧版本的 Octopush2011-2020",
matrixHomeserverURL: "服务器链接(开头带 http(s):// 和可能的需要的端口号)",
"Internal Room Id": "Internal Room Id",
matrixDesc1: "你可以在 Matrix 客户端房间设置的高级选项找到 Internal Room Id。格式类似于 !QMdRCpUIfLwsfjxye6:home.server。",
matrixDesc2: "请不要使用你自己的 Access Token这将开放你所有的账户权限和你加入的房间权限。你可以创建一个新的用户并邀请它至你允许的的房间中。你可以运行以下命令来获取 Access Token{0}",
emailCustomSubject: "邮件主题",
clicksendsms: "ClickSend SMS",
GoogleChat: "Google Chat (Google Workspace only)",
apiCredentials: "API credentials",
promosmsTypeEco: "SMS ECO - 便宜但是慢,并且容易超负荷。仅限波兰地区的收信人。",
promosmsTypeFlash: "SMS FLASH - 消息会自动显示在收信人设备上。仅限波兰地区的收信人。",
promosmsTypeFull: "SMS FULL - 高级短信,您可以使用您自己的发信人名称(需要先注册)。对于警报来说更可靠。",
promosmsTypeSpeed: "SMS SPEED - 最高优先级。非常快速可靠,但更贵(大约两倍 SMS FULL 的价格)。",
promosmsPhoneNumber: "电话号码(波兰地区收信人可以不填区号)",
promosmsSMSSender: "短信发信人名称已注册的名称或以下默认值之一InfoSMS、SMS Info、MaxSMS、INFO、SMS",
"Feishu WebHookUrl": "飞书 WebHook URL",
matrixHomeserverURL: "服务器 URL包含 http(s):// 和可选的端口号)",
"Internal Room Id": "内部房间 ID",
matrixDesc1: "您可以在 Matrix 客户端房间设置的高级选项内找到内部房间 ID。格式类似于 !QMdRCpUIfLwsfjxye6:home.server。",
matrixDesc2: "请不要使用您自己的 Access Token这将开放您所有的账户权限和您已加入房间的权限。我们强烈建议您创建一个新用户并邀请它至您接收通知的房间中。您可以运行以下命令来获取 Access Token{0}",
Method: "方法",
Body: "请求体",
Headers: "请求头",
PushUrl: "Push URL",
HeadersInvalidFormat: "请求头不是有效的JSON: ",
BodyInvalidFormat: "请求体不是有效的JSON: ",
"Monitor History": "监控历史数据",
clearDataOlderThan: "保留监控历史数据 {0} 天",
PushUrl: "推送 URL",
HeadersInvalidFormat: "请求头不是有效的 JSON: ",
BodyInvalidFormat: "请求体不是有效的 JSON: ",
"Monitor History": "监控历史",
clearDataOlderThan: "保留监控历史数据 {0} 天",
PasswordsDoNotMatch: "密码不匹配",
records: "records",
"One record": "One record",
steamApiKeyDescription: "为了监控Steam游戏服务器你需要一个Steam Web-API key。你可以在这里注册你的API密钥: ",
records: "记录",
"One record": "一条记录",
steamApiKeyDescription: "要监控 Steam 游戏服务器,您需要 Steam Web-API 密钥。您可以在这里注册您的 API 密钥: ",
"Current User": "当前用户",
recent: "最近",
Done: "完成",
Info: "信息",
Security: "安全性",
"Steam API Key": "Steam API Key",
"Shrink Database": "数据库",
"Steam API Key": "Steam API 密钥",
"Shrink Database": "缩数据库",
"Pick a RR-Type...": "选择资源记录类型...",
"Pick Accepted Status Codes...": "选择有效的状态码...",
Default: "默认",
@ -322,44 +324,53 @@ export default {
Title: "标题",
Content: "内容",
Style: "类型",
info: "info",
warning: "warning",
danger: "danger",
primary: "primary",
light: "light",
dark: "dark",
info: "信息",
warning: "警告",
danger: "危险",
primary: "主要",
light: "明亮",
dark: "黑暗",
Post: "发布",
"Please input title and content": "请输入标题和内容",
Created: "创建",
"Last Updated": "最后更新",
Unpin: "删除",
Created: "创建时间",
"Last Updated": "更新时间",
Unpin: "取消钉选",
"Switch to Light Theme": "切换到浅色主题",
"Switch to Dark Theme": "切换到深色主题",
"Show Tags": "显示标签",
"Hide Tags": "隐藏标签",
Description: "描述",
"No monitors available.": "没有可用的监控项",
"Add one": "Add one",
"No monitors available.": "没有可用的监控项",
"Add one": "添加一个",
"No Monitors": "没有监控项",
"Untitled Group": "无标题分组",
"Untitled Group": "无标题分组",
Services: "服务",
Discard: "取消",
Discard: "放弃",
Cancel: "取消",
"Powered by": "Powered by",
shrinkDatabaseDescription: "这将触发SQLite数据库的 VACUUM 命令,如果您的数据库是在1.10.0版本之后创建的AUTO_VACUUM已经启用了则不需要再使用此功能",
shrinkDatabaseDescription: "触发 SQLite 数据库的 VACUUM 命令,如果您的数据库是在 1.10.0 版本之后创建的,则已启用 AUTO_VACUUM不再需要此操作。",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API Username (incl. webapi_ prefix)",
serwersmsAPIPassword: "API Password",
serwersmsPhoneNumber: "Phone number",
serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
serwersmsAPIUser: "API 用户名(包括 webapi_ 前缀)",
serwersmsAPIPassword: "API 密码",
serwersmsPhoneNumber: "电话号码",
serwersmsSenderName: "SMS 发信人名称(需要在客户中心注册)",
stackfield: "Stackfield",
smtpDkimSettings: "DKIM Settings",
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
documentation: "documentation",
smtpDkimSettings: "DKIM 设置",
smtpDkimDesc: "请访问 Nodemailer DKIM {0} 了解配置方法。",
documentation: "文档",
smtpDkimDomain: "域名",
smtpDkimKeySelector: "Key Selector",
smtpDkimPrivateKey: "Private Key",
smtpDkimHashAlgo: "Hash Algorithm (可选)",
smtpDkimheaderFieldNames: "Header Keys to sign (可选)",
smtpDkimskipFields: "Header Keys not to sign (可选)",
smtpDkimKeySelector: "前缀选择器",
smtpDkimPrivateKey: "密钥",
smtpDkimHashAlgo: "哈希算法(可选)",
smtpDkimheaderFieldNames: "包含在哈希计算对象内的 Header 列表(可选)",
smtpDkimskipFields: "不包含在哈希计算对象内的 Header 列表(可选)",
Feishu: "飞书",
AliyunSMS: "阿里云短信服务",
"Sms template must contain parameters: ": "短信模板必须包含以下变量:",
DingDing: "钉钉自定义机器人",
WebHookUrl: "钉钉自定义机器人 Webhook 地址",
SecretKey: "钉钉自定义机器人加签密钥",
"For safety, must use secret key": "出于安全考虑,必须使用加签密钥",
WeCom: "企业微信群机器人",
"WeCom Bot Key": "企业微信群机器人 Key",
};

@ -96,11 +96,11 @@ export default {
Test: "測試",
keywordDescription: "搜索 HTML 或 JSON 裡是否有出現關鍵字(注意英文大細階)",
"Certificate Info": "憑證詳細資料",
deleteMonitorMsg: "是否確定刪除這個監測器",
deleteMonitorMsg: "是否確定刪除這個監測器",
deleteNotificationMsg: "是否確定刪除這個通知設定?如監測器啟用了這個通知,將會收不到通知。",
"Resolver Server": "DNS 伺服器",
"Resource Record Type": "DNS 記錄類型",
resoverserverDescription: "預設值為 Cloudflare DNS 伺服器,你可以轉用其他 DNS 伺服器。",
resolverserverDescription: "預設值為 Cloudflare DNS 伺服器,你可以轉用其他 DNS 伺服器。",
rrtypeDescription: "請選擇 DNS 記錄類型",
pauseMonitorMsg: "是否確定暫停?",
"Last Result": "最後結果",
@ -180,6 +180,7 @@ export default {
"Edit Status Page": "編輯 Status Page",
"Go to Dashboard": "前往主控台",
"Status Page": "Status Page",
"Status Pages": "Status Pages",
telegram: "Telegram",
webhook: "Webhook",
smtp: "電郵 (SMTP)",
@ -198,4 +199,5 @@ export default {
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
deleteStatusPageMsg: "是否確定刪除這個 Status Page",
};

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "暫停",
deleteMonitorMsg: "您確定要刪除此監測器嗎?",
deleteNotificationMsg: "您確定要為所有監測器刪除此通知嗎?",
resoverserverDescription: "Cloudflare 為預設伺服器。您可以隨時更換解析伺服器。",
resolverserverDescription: "Cloudflare 為預設伺服器。您可以隨時更換解析伺服器。",
rrtypeDescription: "選擇您想要監測的資源記錄類型",
pauseMonitorMsg: "您確定要暫停嗎?",
enableDefaultNotificationDescription: "預設情況下,新監測器將啟用此通知。您仍可分別停用各監測器的通知。",
@ -183,6 +183,7 @@ export default {
"Edit Status Page": "編輯狀態頁",
"Go to Dashboard": "前往儀表板",
"Status Page": "狀態頁",
"Status Pages": "狀態頁",
defaultNotificationName: "我的 {notification} 通知 ({number})",
here: "此處",
Required: "必填",
@ -340,7 +341,6 @@ export default {
"No monitors available.": "沒有可用的監測器。",
"Add one": "新增一個",
"No Monitors": "無監測器",
"Add one": "新增一個",
"Untitled Group": "未命名群組",
Services: "服務",
Discard: "捨棄",

@ -3,6 +3,9 @@
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
<div class="container-fluid">
{{ $root.connectionErrorMsg }}
<div v-if="$root.showReverseProxyGuide">
Using a Reverse Proxy? <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">Check how to config it for WebSocket</a>
</div>
</div>
</div>
@ -18,10 +21,10 @@
</a>
<ul class="nav nav-pills">
<li class="nav-item me-2">
<a href="/status" class="nav-link status-page">
<font-awesome-icon icon="stream" /> {{ $t("Status Page") }}
</a>
<li v-if="$root.loggedIn" class="nav-item me-2">
<router-link to="/manage-status-page" class="nav-link">
<font-awesome-icon icon="stream" /> {{ $t("Status Pages") }}
</router-link>
</li>
<li v-if="$root.loggedIn" class="nav-item me-2">
<router-link to="/dashboard" class="nav-link">
@ -48,7 +51,7 @@
</header>
<main>
<router-view v-if="$root.loggedIn" />
<router-view v-if="$root.loggedIn || forceShowContent" />
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
</main>
@ -165,7 +168,7 @@ export default {
text-decoration: none;
vertical-align: top;
&.router-link-exact-active {
&.router-link-exact-active, &.active {
color: $primary;
font-weight: bold;
}
@ -198,6 +201,8 @@ main {
padding: 5px;
background-color: crimson;
color: white;
position: fixed;
width: 100%;
}
.dark {

@ -1,16 +1,21 @@
import { io } from "socket.io-client";
import { useToast } from "vue-toastification";
import jwt_decode from "jwt-decode";
import Favico from "favico.js";
const toast = useToast();
let socket;
const noSocketIOPages = [
"/status-page",
"/status",
"/"
/^\/status-page$/, // /status-page
/^\/status/, // /status**
/^\/$/ // /
];
const favicon = new Favico({
animation: "none"
});
export default {
data() {
@ -33,7 +38,10 @@ export default {
uptimeList: { },
tlsInfoList: {},
notificationList: [],
statusPageListLoaded: false,
statusPageList: [],
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
showReverseProxyGuide: true,
};
},
@ -51,8 +59,12 @@ export default {
}
// No need to connect to the socket.io for status page
if (! bypass && noSocketIOPages.includes(location.pathname)) {
return;
if (! bypass && location.pathname) {
for (let page of noSocketIOPages) {
if (location.pathname.match(page)) {
return;
}
}
}
this.socket.initedSocketIO = true;
@ -103,6 +115,11 @@ export default {
this.notificationList = data;
});
socket.on("statusPageList", (data) => {
this.statusPageListLoaded = true;
this.statusPageList = data;
});
socket.on("heartbeat", (data) => {
if (! (data.monitorID in this.heartbeatList)) {
this.heartbeatList[data.monitorID] = [];
@ -169,6 +186,7 @@ export default {
socket.on("connect_error", (err) => {
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`;
this.showReverseProxyGuide = true;
this.socket.connected = false;
this.socket.firstConnect = false;
});
@ -183,6 +201,7 @@ export default {
console.log("Connected to the socket server");
this.socket.connectCount++;
this.socket.connected = true;
this.showReverseProxyGuide = false;
// Reset Heartbeat list if it is re-connect
if (this.socket.connectCount >= 2) {
@ -239,6 +258,14 @@ export default {
}
},
toastSuccess(msg) {
toast.success(msg);
},
toastError(msg) {
toast.error(msg);
},
login(username, password, token, callback) {
socket.emit("login", {
username,
@ -392,10 +419,49 @@ export default {
return result;
},
stats() {
let result = {
up: 0,
down: 0,
unknown: 0,
pause: 0,
};
for (let monitorID in this.$root.monitorList) {
let beat = this.$root.lastHeartbeatList[monitorID];
let monitor = this.$root.monitorList[monitorID];
if (monitor && ! monitor.active) {
result.pause++;
} else if (beat) {
if (beat.status === 1) {
result.up++;
} else if (beat.status === 0) {
result.down++;
} else if (beat.status === 2) {
result.up++;
} else {
result.unknown++;
}
} else {
result.unknown++;
}
}
return result;
},
},
watch: {
// Update Badge
"stats.down"(to, from) {
if (to !== from) {
favicon.badge(to);
}
},
// Reload the SPA if the server version is changed.
"info.version"(to, from) {
if (from && from !== to) {
@ -409,9 +475,15 @@ export default {
// Reconnect the socket io, if status-page to dashboard
"$route.fullPath"(newValue, oldValue) {
if (noSocketIOPages.includes(newValue)) {
return;
if (newValue) {
for (let page of noSocketIOPages) {
if (newValue.match(page)) {
return;
}
}
}
this.initSocketIO();
},

@ -33,7 +33,7 @@ export default {
return "light";
}
if (this.path === "/status-page" || this.path === "/status") {
if (this.path.startsWith("/status-page") || this.path.startsWith("/status")) {
return this.statusPageTheme;
} else {
if (this.userTheme === "auto") {

@ -0,0 +1,79 @@
<template>
<transition name="slide-fade" appear>
<div>
<h1 class="mb-3">
{{ $t("Add New Status Page") }}
</h1>
<form @submit.prevent="submit">
<div class="shadow-box">
<div class="mb-3">
<label for="name" class="form-label">{{ $t("Name") }}</label>
<input id="name" v-model="title" type="text" class="form-control" required>
</div>
<div class="mb-4">
<label for="slug" class="form-label">{{ $t("Slug") }}</label>
<div class="input-group">
<span id="basic-addon3" class="input-group-text">/status/</span>
<input id="slug" v-model="slug" type="text" class="form-control" required>
</div>
<div class="form-text">
<ul>
<li>{{ $t("Accept characters:") }} <mark>a-z</mark> <mark>0-9</mark> <mark>-</mark></li>
<li>{{ $t("Start or end with") }} <mark>a-z</mark> <mark>0-9</mark> only</li>
<li>{{ $t("No consecutive dashes") }} <mark>--</mark></li>
</ul>
</div>
</div>
<div class="mt-2 mb-1">
<button id="monitor-submit-btn" class="btn btn-primary w-100" type="submit" :disabled="processing">{{ $t("Next") }}</button>
</div>
</div>
</form>
</div>
</transition>
</template>
<script>
export default {
components: {
},
data() {
return {
title: "",
slug: "",
processing: false,
};
},
methods: {
async submit() {
this.processing = true;
this.$root.getSocket().emit("addStatusPage", this.title, this.slug, (res) => {
this.processing = false;
if (res.ok) {
location.href = "/status/" + this.slug + "?edit";
} else {
if (res.msg.includes("UNIQUE constraint")) {
this.$root.toastError(this.$t("The slug is already taken. Please choose another slug."));
} else {
this.$root.toastRes(res);
}
}
});
}
}
};
</script>
<style lang="scss" scoped>
.shadow-box {
padding: 20px;
}
</style>

@ -9,19 +9,19 @@
<div class="row">
<div class="col">
<h3>{{ $t("Up") }}</h3>
<span class="num">{{ stats.up }}</span>
<span class="num">{{ $root.stats.up }}</span>
</div>
<div class="col">
<h3>{{ $t("Down") }}</h3>
<span class="num text-danger">{{ stats.down }}</span>
<span class="num text-danger">{{ $root.stats.down }}</span>
</div>
<div class="col">
<h3>{{ $t("Unknown") }}</h3>
<span class="num text-secondary">{{ stats.unknown }}</span>
<span class="num text-secondary">{{ $root.stats.unknown }}</span>
</div>
<div class="col">
<h3>{{ $t("pauseDashboardHome") }}</h3>
<span class="num text-secondary">{{ stats.pause }}</span>
<span class="num text-secondary">{{ $root.stats.pause }}</span>
</div>
</div>
</div>
@ -89,37 +89,6 @@ export default {
};
},
computed: {
stats() {
let result = {
up: 0,
down: 0,
unknown: 0,
pause: 0,
};
for (let monitorID in this.$root.monitorList) {
let beat = this.$root.lastHeartbeatList[monitorID];
let monitor = this.$root.monitorList[monitorID];
if (monitor && ! monitor.active) {
result.pause++;
} else if (beat) {
if (beat.status === 1) {
result.up++;
} else if (beat.status === 0) {
result.down++;
} else if (beat.status === 2) {
result.up++;
} else {
result.unknown++;
}
} else {
result.unknown++;
}
}
return result;
},
importantHeartBeatList() {
let result = [];

@ -87,7 +87,7 @@
<label for="dns_resolve_server" class="form-label">{{ $t("Resolver Server") }}</label>
<input id="dns_resolve_server" v-model="monitor.dns_resolve_server" type="text" class="form-control" :pattern="ipRegex" required>
<div class="form-text">
{{ $t("resoverserverDescription") }}
{{ $t("resolverserverDescription") }}
</div>
</div>

@ -0,0 +1,118 @@
<template>
<transition name="slide-fade" appear>
<div>
<h1 class="mb-3">
{{ $t("Status Pages") }}
</h1>
<div>
<router-link to="/add-status-page" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("New Status Page") }}</router-link>
</div>
<div class="shadow-box">
<template v-if="$root.statusPageListLoaded">
<span v-if="Object.keys($root.statusPageList).length === 0" class="d-flex align-items-center justify-content-center my-3">
No status pages
</span>
<!-- use <a> instead of <router-link>, because the heartbeat won't load. -->
<a v-for="statusPage in $root.statusPageList" :key="statusPage.slug" :href="'/status/' + statusPage.slug" class="item">
<img :src="icon(statusPage.icon)" alt class="logo me-2" />
<div class="info">
<div class="title">{{ statusPage.title }}</div>
<div class="slug">/status/{{ statusPage.slug }}</div>
</div>
</a>
</template>
<div v-else class="d-flex align-items-center justify-content-center my-3 spinner">
<font-awesome-icon icon="spinner" size="2x" spin />
</div>
</div>
</div>
</transition>
</template>
<script>
import { getResBaseURL } from "../util-frontend";
export default {
components: {
},
data() {
return {
};
},
computed: {
},
mounted() {
},
methods: {
icon(icon) {
if (icon === "/icon.svg") {
return icon;
} else {
return getResBaseURL() + icon;
}
}
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.item {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
border-radius: 10px;
transition: all ease-in-out 0.15s;
padding: 10px;
&:hover {
background-color: $highlight-white;
}
&.active {
background-color: #cdf8f4;
}
$logo-width: 70px;
.logo {
width: $logo-width;
// Better when the image is loading
min-height: 1px;
}
.info {
.title {
font-weight: bold;
font-size: 20px;
}
.slug {
font-size: 14px;
}
}
}
.dark {
.item {
&:hover {
background-color: $dark-bg2;
}
&.active {
background-color: $dark-bg2;
}
}
}
</style>

@ -0,0 +1,99 @@
<template>
<div>
<!-- Desktop header -->
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
<router-link to="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" />
<span class="fs-4 title">Uptime Kuma</span>
</router-link>
</header>
<!-- Mobile header -->
<header v-else class="d-flex flex-wrap justify-content-center pt-2 pb-2 mb-3">
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
<object class="bi" width="40" height="40" data="/icon.svg" />
<span class="fs-4 title ms-2">Uptime Kuma</span>
</router-link>
</header>
<div class="content">
<div>
<strong>🐻 {{ $t("Page Not Found") }}</strong>
</div>
<div class="guide">
Most likely causes:
<ul>
<li>The resource is no longer available.</li>
<li>There might be a typing error in the address.</li>
</ul>
What you can try:<br />
<ul>
<li>Retype the address.</li>
<li><a href="#" class="go-back" @click="goBack()">Go back to the previous page.</a></li>
</ul>
</div>
</div>
</div>
</template>
<script>
export default {
async mounted() {
},
methods: {
goBack() {
history.back();
}
}
};
</script>
<style scoped lang="scss">
@import "../assets/vars.scss";
.go-back {
text-decoration: none;
color: $primary !important;
}
.content {
display: flex;
justify-content: center;
align-content: center;
align-items: center;
flex-direction: column;
gap: 50px;
padding-top: 30px;
strong {
font-size: 24px;
}
}
.guide {
max-width: 800px;
font-size: 14px;
}
.title {
font-weight: bold;
}
.dark {
header {
background-color: $dark-header-bg;
border-bottom-color: $dark-header-bg !important;
span {
color: #f0f6fc;
}
}
.bottom-nav {
background-color: $dark-bg;
}
}
</style>

@ -6,7 +6,7 @@
<div class="shadow-box">
<div class="row">
<div class="settings-menu">
<div v-if="showSubMenu" class="settings-menu col-lg-3 col-md-5">
<router-link
v-for="(item, key) in subMenus"
:key="key"
@ -17,8 +17,8 @@
</div>
</router-link>
</div>
<div class="settings-content">
<div class="settings-content-header">
<div class="settings-content col-lg-9 col-md-7">
<div v-if="currentPage" class="settings-content-header">
{{ subMenus[currentPage].title }}
</div>
<div class="mx-3">
@ -41,7 +41,6 @@ export default {
data() {
return {
show: true,
settings: {},
settingsLoaded: false,
};
@ -52,11 +51,19 @@ export default {
let pathSplit = useRoute().path.split("/");
let pathEnd = pathSplit[pathSplit.length - 1];
if (!pathEnd || pathEnd === "settings") {
return "general";
return null;
}
return pathEnd;
},
showSubMenu() {
if (this.$root.isMobile) {
return !this.currentPage;
} else {
return true;
}
},
subMenus() {
return {
general: {
@ -84,11 +91,26 @@ export default {
},
},
watch: {
"$root.isMobile"() {
this.loadGeneralPage();
}
},
mounted() {
this.loadSettings();
this.loadGeneralPage();
},
methods: {
// For desktop only, mobile do nothing
loadGeneralPage() {
if (!this.currentPage && !this.$root.isMobile) {
this.$router.push("/settings/general");
}
},
loadSettings() {
this.$root.getSocket().emit("getSettings", (res) => {
this.settings = res.data;
@ -115,7 +137,7 @@ export default {
this.loadSettings();
});
},
},
}
};
</script>
@ -136,9 +158,6 @@ footer {
}
.settings-menu {
flex: 0 0 auto;
width: 300px;
a {
text-decoration: none !important;
}
@ -148,6 +167,8 @@ footer {
margin: 0.5em;
padding: 0.7em 1em;
cursor: pointer;
border-left-width: 0;
transition: all ease-in-out 0.1s;
}
.menu-item:hover {
@ -171,9 +192,6 @@ footer {
}
.settings-content {
flex: 0 0 auto;
width: calc(100% - 300px);
.settings-content-header {
width: calc(100% + 20px);
border-bottom: 1px solid #dee2e6;
@ -187,6 +205,14 @@ footer {
background: $dark-header-bg;
border-bottom: 0;
}
.mobile & {
padding: 15px 0 0 0;
.dark & {
background-color: transparent;
}
}
}
}
</style>

@ -1,215 +1,239 @@
<template>
<div v-if="loadedTheme" class="container mt-3">
<!-- Logo & Title -->
<h1 class="mb-4">
<!-- Logo -->
<span class="logo-wrapper" @click="showImageCropUploadMethod">
<img :src="logoURL" alt class="logo me-2" :class="logoClass" />
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
</span>
<!-- Uploader -->
<!-- url="/api/status-page/upload-logo" -->
<ImageCropUpload v-model="showImageCropUpload"
field="img"
:width="128"
:height="128"
:langType="$i18n.locale"
img-format="png"
:noCircle="true"
:noSquare="false"
@crop-success="cropSuccess"
/>
<!-- Title -->
<Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" />
</h1>
<!-- Admin functions -->
<div v-if="hasToken" class="mb-4">
<div v-if="!enableEditMode">
<button class="btn btn-info me-2" @click="edit">
<font-awesome-icon icon="edit" />
{{ $t("Edit Status Page") }}
</button>
<!-- Sidebar for edit mode -->
<div v-if="enableEditMode" class="sidebar">
<div class="my-3">
<label for="slug" class="form-label">{{ $t("Slug") }}</label>
<div class="input-group">
<span id="basic-addon3" class="input-group-text">/status/</span>
<input id="slug" v-model="config.slug" type="text" class="form-control">
</div>
</div>
<a href="/dashboard" class="btn btn-info">
<font-awesome-icon icon="tachometer-alt" />
{{ $t("Go to Dashboard") }}
</a>
<div class="my-3">
<label for="title" class="form-label">{{ $t("Title") }}</label>
<input id="title" v-model="config.title" type="text" class="form-control">
</div>
<div v-else>
<button class="btn btn-success me-2" @click="save">
<font-awesome-icon icon="save" />
{{ $t("Save") }}
</button>
<div class="my-3">
<label for="description" class="form-label">{{ $t("Description") }}</label>
<textarea id="description" v-model="config.description" class="form-control"></textarea>
</div>
<button class="btn btn-danger me-2" @click="discard">
<font-awesome-icon icon="save" />
{{ $t("Discard") }}
</button>
<div class="my-3 form-check form-switch">
<input id="switch-theme" v-model="config.theme" class="form-check-input" type="checkbox" true-value="dark" false-value="light">
<label class="form-check-label" for="switch-theme">{{ $t("Switch to Dark Theme") }}</label>
</div>
<button class="btn btn-primary btn-add-group me-2" @click="createIncident">
<font-awesome-icon icon="bullhorn" />
{{ $t("Create Incident") }}
</button>
<div class="my-3 form-check form-switch">
<input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox">
<label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label>
</div>
<!--
<button v-if="isPublished" class="btn btn-light me-2" @click="">
<font-awesome-icon icon="save" />
{{ $t("Unpublish") }}
</button>
<div v-if="false" class="my-3">
<label for="password" class="form-label">{{ $t("Password") }} <sup>Coming Soon</sup></label>
<input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control">
</div>
<button v-if="!isPublished" class="btn btn-info me-2" @click="">
<font-awesome-icon icon="save" />
{{ $t("Publish") }}
</button>-->
<div v-if="false" class="my-3">
<label for="cname" class="form-label">Domain Names <sup>Coming Soon</sup></label>
<textarea id="cname" v-model="config.domanNames" rows="3" disabled class="form-control" :placeholder="domainNamesPlaceholder"></textarea>
</div>
<!-- Set Default Language -->
<!-- Set theme -->
<button v-if="theme == 'dark'" class="btn btn-light me-2" @click="changeTheme('light')">
<font-awesome-icon icon="save" />
{{ $t("Switch to Light Theme") }}
<div class="danger-zone">
<button class="btn btn-danger me-2" @click="deleteDialog">
<font-awesome-icon icon="trash" />
{{ $t("Delete") }}
</button>
</div>
<button v-if="theme == 'light'" class="btn btn-dark me-2" @click="changeTheme('dark')">
<!-- Sidebar Footer -->
<div class="sidebar-footer">
<button class="btn btn-success me-2" @click="save">
<font-awesome-icon icon="save" />
{{ $t("Switch to Dark Theme") }}
{{ $t("Save") }}
</button>
<button class="btn btn-secondary me-2" @click="changeTagsVisibilty(!tagsVisible)">
<template v-if="tagsVisible">
<font-awesome-icon icon="eye-slash" />
{{ $t("Hide Tags") }}
</template>
<template v-else>
<font-awesome-icon icon="eye" />
{{ $t("Show Tags") }}
</template>
<button class="btn btn-danger me-2" @click="discard">
<font-awesome-icon icon="save" />
{{ $t("Discard") }}
</button>
</div>
</div>
<!-- Incident -->
<div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass">
<strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
<Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" />
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
<Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
<!-- Incident Date -->
<div class="date mt-3">
{{ $t("Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br />
<span v-if="incident.lastUpdatedDate">
{{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }})
<!-- Main Status Page -->
<div :class="{ edit: enableEditMode}" class="main">
<!-- Logo & Title -->
<h1 class="mb-4 title-flex">
<!-- Logo -->
<span class="logo-wrapper" @click="showImageCropUploadMethod">
<img :src="logoURL" alt class="logo me-2" :class="logoClass" />
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
</span>
</div>
<div v-if="editMode" class="mt-3">
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident">
<font-awesome-icon icon="bullhorn" />
{{ $t("Post") }}
</button>
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident">
<font-awesome-icon icon="edit" />
{{ $t("Edit") }}
</button>
<!-- Uploader -->
<!-- url="/api/status-page/upload-logo" -->
<ImageCropUpload v-model="showImageCropUpload"
field="img"
:width="128"
:height="128"
:langType="$i18n.locale"
img-format="png"
:noCircle="true"
:noSquare="false"
@crop-success="cropSuccess"
/>
<!-- Title -->
<Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" />
</h1>
<!-- Admin functions -->
<div v-if="hasToken" class="mb-4">
<div v-if="!enableEditMode">
<button class="btn btn-info me-2" @click="edit">
<font-awesome-icon icon="edit" />
{{ $t("Edit Status Page") }}
</button>
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="cancelIncident">
<font-awesome-icon icon="times" />
{{ $t("Cancel") }}
</button>
<a href="/manage-status-page" class="btn btn-info">
<font-awesome-icon icon="tachometer-alt" />
{{ $t("Go to Dashboard") }}
</a>
</div>
<div v-if="editIncidentMode" class="dropdown d-inline-block me-2">
<button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ $t("Style") }}: {{ $t(incident.style) }}
<div v-else>
<button class="btn btn-primary btn-add-group me-2" @click="createIncident">
<font-awesome-icon icon="bullhorn" />
{{ $t("Create Incident") }}
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a></li>
</ul>
</div>
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="unpinIncident">
<font-awesome-icon icon="unlink" />
{{ $t("Unpin") }}
</button>
</div>
</div>
<!-- Overall Status -->
<div class="shadow-box list p-4 overall-status mb-4">
<div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData">
<font-awesome-icon icon="question-circle" class="ok" />
{{ $t("No Services") }}
</div>
<!-- Incident -->
<div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass">
<strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
<Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" />
<template v-else>
<div v-if="allUp">
<font-awesome-icon icon="check-circle" class="ok" />
{{ $t("All Systems Operational") }}
</div>
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
<Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
<div v-else-if="partialDown">
<font-awesome-icon icon="exclamation-circle" class="warning" />
{{ $t("Partially Degraded Service") }}
<!-- Incident Date -->
<div class="date mt-3">
{{ $t("Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br />
<span v-if="incident.lastUpdatedDate">
{{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }})
</span>
</div>
<div v-else-if="allDown">
<font-awesome-icon icon="times-circle" class="danger" />
{{ $t("Degraded Service") }}
</div>
<div v-if="editMode" class="mt-3">
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident">
<font-awesome-icon icon="bullhorn" />
{{ $t("Post") }}
</button>
<div v-else>
<font-awesome-icon icon="question-circle" style="color: #efefef;" />
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident">
<font-awesome-icon icon="edit" />
{{ $t("Edit") }}
</button>
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="cancelIncident">
<font-awesome-icon icon="times" />
{{ $t("Cancel") }}
</button>
<div v-if="editIncidentMode" class="dropdown d-inline-block me-2">
<button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ $t("Style") }}: {{ $t(incident.style) }}
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a></li>
</ul>
</div>
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="unpinIncident">
<font-awesome-icon icon="unlink" />
{{ $t("Unpin") }}
</button>
</div>
</template>
</div>
</div>
<!-- Description -->
<strong v-if="editMode">{{ $t("Description") }}:</strong>
<Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" />
<!-- Overall Status -->
<div class="shadow-box list p-4 overall-status mb-4">
<div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData">
<font-awesome-icon icon="question-circle" class="ok" />
{{ $t("No Services") }}
</div>
<div v-if="editMode" class="mb-4">
<div>
<button class="btn btn-primary btn-add-group me-2" @click="addGroup">
<font-awesome-icon icon="plus" />
{{ $t("Add Group") }}
</button>
<template v-else>
<div v-if="allUp">
<font-awesome-icon icon="check-circle" class="ok" />
{{ $t("All Systems Operational") }}
</div>
<div v-else-if="partialDown">
<font-awesome-icon icon="exclamation-circle" class="warning" />
{{ $t("Partially Degraded Service") }}
</div>
<div v-else-if="allDown">
<font-awesome-icon icon="times-circle" class="danger" />
{{ $t("Degraded Service") }}
</div>
<div v-else>
<font-awesome-icon icon="question-circle" style="color: #efefef;" />
</div>
</template>
</div>
<div class="mt-3">
<div v-if="allMonitorList.length > 0 && loadedData">
<label>{{ $t("Add a monitor") }}:</label>
<select v-model="selectedMonitor" class="form-control">
<option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option>
</select>
<!-- Description -->
<strong v-if="editMode">{{ $t("Description") }}:</strong>
<Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" />
<div v-if="editMode" class="mb-4">
<div>
<button class="btn btn-primary btn-add-group me-2" @click="addGroup">
<font-awesome-icon icon="plus" />
{{ $t("Add Group") }}
</button>
</div>
<div v-else class="text-center">
{{ $t("No monitors available.") }} <router-link to="/add">{{ $t("Add one") }}</router-link>
<div class="mt-3">
<div v-if="allMonitorList.length > 0 && loadedData">
<label>{{ $t("Add a monitor") }}:</label>
<select v-model="selectedMonitor" class="form-control">
<option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option>
</select>
</div>
<div v-else class="text-center">
{{ $t("No monitors available.") }} <router-link to="/add">{{ $t("Add one") }}</router-link>
</div>
</div>
</div>
</div>
<div class="mb-4">
<div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center">
<!-- 👀 Nothing here, please add a group or a monitor. -->
👀 {{ $t("statusPageNothing") }}
<div class="mb-4">
<div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center">
<!-- 👀 Nothing here, please add a group or a monitor. -->
👀 {{ $t("statusPageNothing") }}
</div>
<PublicGroupList :edit-mode="enableEditMode" :show-tags="config.showTags" />
</div>
<PublicGroupList :edit-mode="enableEditMode" />
<footer class="mt-5 mb-4">
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
</footer>
</div>
<footer class="mt-5 mb-4">
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
</footer>
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteStatusPage">
{{ $t("deleteStatusPageMsg") }}
</Confirm>
</div>
</template>
@ -220,16 +244,25 @@ import ImageCropUpload from "vue-image-crop-upload";
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts";
import { useToast } from "vue-toastification";
import dayjs from "dayjs";
import Favico from "favico.js";
import { getResBaseURL } from "../util-frontend";
import Confirm from "../components/Confirm.vue";
const toast = useToast();
const leavePageMsg = "Do you really want to leave? you have unsaved changes!";
let feedInterval;
const favicon = new Favico({
animation: "none"
});
export default {
components: {
PublicGroupList,
ImageCropUpload
ImageCropUpload,
Confirm,
},
// Leave Page for vue route change
@ -247,6 +280,7 @@ export default {
data() {
return {
slug: null,
enableEditMode: false,
enableEditIncidentMode: false,
hasToken: false,
@ -259,6 +293,8 @@ export default {
loadedTheme: false,
loadedData: false,
baseURL: "",
clickedEditButton: false,
domainNamesPlaceholder: "domain1.com\ndomain2.com\n..."
};
},
computed: {
@ -296,15 +332,7 @@ export default {
},
isPublished() {
return this.config.statusPagePublished;
},
theme() {
return this.config.statusPageTheme;
},
tagsVisible() {
return this.config.statusPageTags
return this.config.published;
},
logoClass() {
@ -378,13 +406,28 @@ export default {
},
// Set Theme
"config.statusPageTheme"() {
this.$root.statusPageTheme = this.config.statusPageTheme;
"config.theme"() {
this.$root.statusPageTheme = this.config.theme;
this.loadedTheme = true;
},
"config.title"(title) {
document.title = title;
},
"$root.monitorList"() {
let count = Object.keys(this.$root.monitorList).length;
// Since publicGroupList is getting from public rest api, monitors' tags may not present if showTags = false
if (count > 0) {
for (let group of this.$root.publicGroupList) {
for (let monitor of group.monitorList) {
if (monitor.tags === undefined && this.$root.monitorList[monitor.id]) {
monitor.tags = this.$root.monitorList[monitor.id].tags;
}
}
}
}
}
},
@ -403,28 +446,24 @@ export default {
});
// Special handle for dev
const env = process.env.NODE_ENV;
if (env === "development" || localStorage.dev === "dev") {
this.baseURL = location.protocol + "//" + location.hostname + ":3001";
}
this.baseURL = getResBaseURL();
},
async mounted() {
axios.get("/api/status-page/config").then((res) => {
this.config = res.data;
this.slug = this.$route.params.slug;
if (this.config.logo) {
this.imgDataUrl = this.config.logo;
}
});
if (!this.slug) {
this.slug = "default";
}
axios.get("/api/status-page/" + this.slug).then((res) => {
this.config = res.data.config;
axios.get("/api/status-page/incident").then((res) => {
if (res.data.ok) {
this.incident = res.data.incident;
if (this.config.icon) {
this.imgDataUrl = this.config.icon;
}
});
axios.get("/api/status-page/monitor-list").then((res) => {
this.$root.publicGroupList = res.data;
this.incident = res.data.incident;
this.$root.publicGroupList = res.data.publicGroupList;
});
// 5mins a loop
@ -432,31 +471,87 @@ export default {
feedInterval = setInterval(() => {
this.updateHeartbeatList();
}, (300 + 10) * 1000);
// Go to edit page if ?edit present
// null means ?edit present, but no value
if (this.$route.query.edit || this.$route.query.edit === null) {
this.edit();
}
},
methods: {
updateHeartbeatList() {
// If editMode, it will use the data from websocket.
if (! this.editMode) {
axios.get("/api/status-page/heartbeat").then((res) => {
this.$root.heartbeatList = res.data.heartbeatList;
this.$root.uptimeList = res.data.uptimeList;
axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => {
const { heartbeatList, uptimeList } = res.data;
this.$root.heartbeatList = heartbeatList;
this.$root.uptimeList = uptimeList;
const heartbeatIds = Object.keys(heartbeatList);
const downMonitors = heartbeatIds.reduce((downMonitorsAmount, currentId) => {
const monitorHeartbeats = heartbeatList[currentId];
const lastHeartbeat = monitorHeartbeats.at(-1);
if (lastHeartbeat) {
return lastHeartbeat.status === 0 ? downMonitorsAmount + 1 : downMonitorsAmount;
} else {
return downMonitorsAmount;
}
}, 0);
favicon.badge(downMonitors);
this.loadedData = true;
});
}
},
edit() {
this.$root.initSocketIO(true);
this.enableEditMode = true;
if (this.hasToken) {
this.$root.initSocketIO(true);
this.enableEditMode = true;
this.clickedEditButton = true;
}
},
save() {
this.$root.getSocket().emit("saveStatusPage", this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
let startTime = new Date();
this.config.slug = this.config.slug.trim().toLowerCase();
this.$root.getSocket().emit("saveStatusPage", this.slug, this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
if (res.ok) {
this.enableEditMode = false;
this.$root.publicGroupList = res.publicGroupList;
location.reload();
// Add some delay, so that the side menu animation would be better
let endTime = new Date();
let time = 100 - (endTime - startTime) / 1000;
if (time < 0) {
time = 0;
}
setTimeout(() => {
location.href = "/status/" + this.config.slug;
}, time);
} else {
toast.error(res.msg);
}
});
},
deleteDialog() {
this.$refs.confirmDelete.show();
},
deleteStatusPage() {
this.$root.getSocket().emit("deleteStatusPage", this.slug, (res) => {
if (res.ok) {
this.enableEditMode = false;
location.href = "/manage-status-page";
} else {
toast.error(res.msg);
}
@ -481,30 +576,7 @@ export default {
},
discard() {
location.reload();
},
changeTheme(name) {
this.config.statusPageTheme = name;
},
changeTagsVisibilty(newState) {
this.config.statusPageTags = newState;
// On load, the status page will not include tags if it's not enabled for security reasons
// Which means if we enable tags, it won't show in the UI until saved
// So we have this to enhance UX and load in the tags from the authenticated source instantly
this.$root.publicGroupList = this.$root.publicGroupList.map((group) => {
return {
...group,
monitorList: group.monitorList.map((monitor) => {
// We only include the tags if visible so we can reuse the logic to hide the tags on disable
return {
...monitor,
tags: newState ? this.$root.monitorList[monitor.id].tags : []
}
})
}
});
location.href = "/status/" + this.slug;
},
/**
@ -540,7 +612,7 @@ export default {
return;
}
this.$root.getSocket().emit("postIncident", this.incident, (res) => {
this.$root.getSocket().emit("postIncident", this.slug, this.incident, (res) => {
if (res.ok) {
this.enableEditIncidentMode = false;
@ -571,7 +643,7 @@ export default {
},
unpinIncident() {
this.$root.getSocket().emit("unpinIncident", () => {
this.$root.getSocket().emit("unpinIncident", this.slug, () => {
this.incident = null;
});
},
@ -614,6 +686,40 @@ h1 {
}
}
.main {
transition: all ease-in-out 0.1s;
&.edit {
margin-left: 300px;
}
}
.sidebar {
position: fixed;
left: 0;
top: 0;
width: 300px;
height: 100vh;
padding: 15px 15px 68px 15px;
overflow-x: hidden;
overflow-y: auto;
border-right: 1px solid #ededed;
.danger-zone {
border-top: 1px solid #ededed;
padding-top: 15px;
}
.sidebar-footer {
width: 100%;
bottom: 0;
left: 0;
padding: 15px;
position: absolute;
border-top: 1px solid #ededed;
}
}
footer {
text-align: center;
font-size: 14px;
@ -623,6 +729,12 @@ footer {
min-width: 50px;
}
.title-flex {
display: flex;
align-items: center;
gap: 10px;
}
.logo-wrapper {
display: inline-block;
position: relative;
@ -681,4 +793,19 @@ footer {
}
}
.dark {
.sidebar {
background-color: $dark-header-bg;
border-right-color: $dark-border-color;
.danger-zone {
border-top-color: $dark-border-color;
}
.sidebar-footer {
border-top-color: $dark-border-color;
}
}
}
</style>

@ -18,6 +18,9 @@ import MonitorHistory from "./components/settings/MonitorHistory.vue";
import Security from "./components/settings/Security.vue";
import Backup from "./components/settings/Backup.vue";
import About from "./components/settings/About.vue";
import ManageStatusPage from "./pages/ManageStatusPage.vue";
import AddStatusPage from "./pages/AddStatusPage.vue";
import NotFound from "./pages/NotFound.vue";
const routes = [
{
@ -70,7 +73,6 @@ const routes = [
children: [
{
path: "general",
alias: "",
component: General,
},
{
@ -99,6 +101,14 @@ const routes = [
},
]
},
{
path: "/manage-status-page",
component: ManageStatusPage,
},
{
path: "/add-status-page",
component: AddStatusPage,
},
],
},
],
@ -115,6 +125,14 @@ const routes = [
path: "/status",
component: StatusPage,
},
{
path: "/status/:slug",
component: StatusPage,
},
{
path: "/:pathMatch(.*)*",
component: NotFound,
},
];
export const router = createRouter({

@ -51,7 +51,19 @@ export function timezoneList() {
}
export function setPageLocale() {
const html = document.documentElement
html.setAttribute('lang', currentLocale() )
html.setAttribute('dir', localeDirection() )
const html = document.documentElement;
html.setAttribute("lang", currentLocale() );
html.setAttribute("dir", localeDirection() );
}
/**
* Mainly used for dev, because the backend and the frontend are in different ports.
*/
export function getResBaseURL() {
const env = process.env.NODE_ENV;
if (env === "development" || localStorage.dev === "dev") {
return location.protocol + "//" + location.hostname + ":3001";
} else {
return "";
}
}

Loading…
Cancel
Save