Merge branch 'master' into master

pull/1348/head
AnnAngela 3 years ago committed by GitHub
commit 742c6bcaa3
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: 0
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,'
exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,feature-request'
exempt-issue-assignees: 'louislam'
exempt-pr-assignees: 'louislam'

@ -196,14 +196,13 @@ https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
### Release Procedures ### Release Procedures
1. Draft a release note 1. Draft a release note
1. Make sure the repo is cleared 2. Make sure the repo is cleared
1. `npm run update-version 1.X.X` 3. `npm run release-final with env vars: `VERSION` and `GITHUB_TOKEN`
1. `npm run build` 4. Wait until the `Press any key to continue`
1. `npm run build-docker` 5. `git push`
1. `git push` 6. Publish the release note as 1.X.X
1. Publish the release note as 1.X.X 7. Press any key to continue
1. `npm run upload-artifacts` with env vars VERSION=1.X.X;GITHUB_TOKEN=XXXX 8. SSH to demo site server and update to 1.X.X
1. SSH to demo site server and update to 1.X.X
Checking: Checking:
@ -211,6 +210,15 @@ Checking:
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7) - Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
- Try clean installation with Node.js - 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 ### Release Wiki
#### Setup Repo #### Setup Repo

@ -61,8 +61,14 @@ npm run setup
node server/server.js node server/server.js
# (Recommended) Option 2. Run in background using PM2 # (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 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. Browse to http://localhost:3001 after starting.
@ -115,7 +121,7 @@ Telegram Notification Sample:
## Motivation ## 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. * Want to build a fancy UI.
* Learn Vue 3 and vite.js. * Learn Vue 3 and vite.js.
* Show the power of Bootstrap 5. * 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 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,5 +1,5 @@
# DON'T UPDATE TO alpine3.13, 1.14, see #41. # DON'T UPDATE TO alpine3.13, 1.14, see #41.
FROM node:14-alpine3.12 FROM node:16-alpine3.12
WORKDIR /app WORKDIR /app
# Install apprise, iputils for non-root ping, setpriv # Install apprise, iputils for non-root ping, setpriv

@ -1,6 +1,6 @@
# DON'T UPDATE TO node:14-bullseye-slim, see #372. # DON'T UPDATE TO node:14-bullseye-slim, see #372.
# If the image changed, the second stage image should be changed too # If the image changed, the second stage image should be changed too
FROM node:14-buster-slim FROM node:16-buster-slim
WORKDIR /app WORKDIR /app
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv # Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv

@ -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)"); bash("check=$(pm2 --version)");
if (check == "") { if (check == "") {
println("Installing PM2"); println("Installing PM2");
bash("npm install pm2 -g"); bash("npm install pm2 -g && pm2 install pm2-logrotate");
bash("pm2 startup"); 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));

@ -6,10 +6,8 @@ const util = require("../src/util");
util.polyfill(); util.polyfill();
const oldVersion = pkg.version; const newVersion = process.env.VERSION;
const newVersion = process.argv[2];
console.log("Old Version: " + oldVersion);
console.log("New Version: " + newVersion); console.log("New Version: " + newVersion);
if (!newVersion) { if (!newVersion) {
@ -23,23 +21,20 @@ if (!exists) {
// Process package.json // Process package.json
pkg.version = newVersion; pkg.version = newVersion;
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion); // Replace the version: https://regex101.com/r/hmj2Bc/1
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion); pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion);
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
commit(newVersion); commit(newVersion);
tag(newVersion); tag(newVersion);
updateWiki(oldVersion, newVersion);
} else { } else {
console.log("version exists"); console.log("version exists");
} }
function commit(version) { function commit(version) {
let msg = "update to " + version; let msg = "Update to " + version;
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]); let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
let stdout = res.stdout.toString().trim(); let stdout = res.stdout.toString().trim();

@ -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) check=$(pm2 --version)
if [ "$check" == "" ]; then if [ "$check" == "" ]; then
"echo" "-e" "Installing PM2" "echo" "-e" "Installing PM2"
npm install pm2 -g npm install pm2 -g && pm2 install pm2-logrotate
pm2 startup pm2 startup
fi fi
mkdir -p $installPath mkdir -p $installPath

17414
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.12.1", "version": "1.13.1",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,15 +30,14 @@
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine", "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-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-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.12.1-alpine --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": "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.12.1 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.12.1-debian --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": "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-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", "build-docker-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", "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.12.1 && 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", "download-dist": "node extra/download-dist.js",
"update-version": "node extra/update-version.js",
"mark-as-nightly": "node extra/mark-as-nightly.js", "mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js", "reset-password": "node extra/reset-password.js",
"remove-2fa": "node extra/remove-2fa.js", "remove-2fa": "node extra/remove-2fa.js",
@ -51,7 +50,10 @@
"simple-dns-server": "node extra/simple-dns-server.js", "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-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", "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": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "~1.2.36", "@fortawesome/fontawesome-svg-core": "~1.2.36",
@ -61,21 +63,22 @@
"@louislam/sqlite3": "~6.0.1", "@louislam/sqlite3": "~6.0.1",
"@popperjs/core": "~2.10.2", "@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0", "args-parser": "~1.3.0",
"axios": "~0.26.0", "axios": "~0.26.1",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"bootstrap": "5.1.3", "bootstrap": "5.1.3",
"bree": "~7.1.0", "bree": "~7.1.5",
"chardet": "^1.3.0", "chardet": "^1.3.0",
"chart.js": "~3.6.0", "chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0", "chartjs-adapter-dayjs": "~1.0.0",
"check-password-strength": "^2.0.3", "check-password-strength": "^2.0.5",
"command-exists": "~1.2.9", "command-exists": "~1.2.9",
"compare-versions": "~3.6.0", "compare-versions": "~3.6.0",
"dayjs": "~1.10.7", "dayjs": "~1.10.8",
"express": "~4.17.1", "express": "~4.17.3",
"express-basic-auth": "~1.2.0", "express-basic-auth": "~1.2.1",
"favico.js": "^0.3.10",
"form-data": "~4.0.0", "form-data": "~4.0.0",
"http-graceful-shutdown": "~3.1.5", "http-graceful-shutdown": "~3.1.7",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"jsonwebtoken": "~8.5.1", "jsonwebtoken": "~8.5.1",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
@ -84,9 +87,9 @@
"notp": "~2.0.3", "notp": "~2.0.3",
"password-hash": "~1.2.2", "password-hash": "~1.2.2",
"postcss-rtlcss": "~3.4.1", "postcss-rtlcss": "~3.4.1",
"postcss-scss": "~4.0.2", "postcss-scss": "~4.0.3",
"prom-client": "~13.2.0", "prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.0", "prometheus-api-metrics": "~3.2.1",
"qrcode": "~1.5.0", "qrcode": "~1.5.0",
"redbean-node": "0.1.3", "redbean-node": "0.1.3",
"socket.io": "~4.4.1", "socket.io": "~4.4.1",
@ -104,7 +107,7 @@
"vue-image-crop-upload": "~3.0.3", "vue-image-crop-upload": "~3.0.3",
"vue-multiselect": "~3.0.0-alpha.2", "vue-multiselect": "~3.0.0-alpha.2",
"vue-qrcode": "~1.0.0", "vue-qrcode": "~1.0.0",
"vue-router": "~4.0.12", "vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5", "vue-toastification": "~2.0.0-rc.5",
"vuedraggable": "~4.1.0" "vuedraggable": "~4.1.0"
}, },
@ -112,10 +115,10 @@
"@actions/github": "~5.0.0", "@actions/github": "~5.0.0",
"@babel/eslint-parser": "~7.15.8", "@babel/eslint-parser": "~7.15.8",
"@babel/preset-env": "^7.15.8", "@babel/preset-env": "^7.15.8",
"@types/bootstrap": "~5.1.6", "@types/bootstrap": "~5.1.9",
"@vitejs/plugin-legacy": "~1.6.3", "@vitejs/plugin-legacy": "~1.6.4",
"@vitejs/plugin-vue": "~1.9.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", "babel-plugin-rewire": "~1.2.0",
"core-js": "~3.18.3", "core-js": "~3.18.3",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
@ -123,7 +126,8 @@
"eslint": "~7.32.0", "eslint": "~7.32.0",
"eslint-plugin-vue": "~7.18.0", "eslint-plugin-vue": "~7.18.0",
"jest": "~27.2.5", "jest": "~27.2.5",
"jest-puppeteer": "~6.0.0", "jest-puppeteer": "~6.0.3",
"npm-check-updates": "^12.5.4",
"puppeteer": "~13.1.3", "puppeteer": "~13.1.3",
"sass": "~1.42.1", "sass": "~1.42.1",
"stylelint": "~14.2.0", "stylelint": "~14.2.0",

@ -31,31 +31,34 @@ exports.login = async function (username, password) {
}; };
function myAuthorizer(username, password, callback) { function myAuthorizer(username, password, callback) {
setting("disableAuth").then((result) => { // Login Rate Limit
if (result) { loginRateLimiter.pass(null, 0).then((pass) => {
callback(null, true); if (pass) {
} else { exports.login(username, password).then((user) => {
// Login Rate Limit callback(null, user != null);
loginRateLimiter.pass(null, 0).then((pass) => {
if (pass) { if (user == null) {
exports.login(username, password).then((user) => { loginRateLimiter.removeTokens(1);
callback(null, user != null);
if (user == null) {
loginRateLimiter.removeTokens(1);
}
});
} else {
callback(null, false);
} }
}); });
} else {
callback(null, false);
} }
}); });
} }
exports.basicAuth = basicAuth({ exports.basicAuth = async function (req, res, next) {
authorizer: myAuthorizer, const middleware = basicAuth({
authorizeAsync: true, authorizer: myAuthorizer,
challenge: true, 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 axios = require("axios");
const compareVersions = require("compare-versions");
exports.version = require("../package.json").version; exports.version = require("../package.json").version;
exports.latestVersion = null; exports.latestVersion = null;
@ -16,6 +17,19 @@ exports.startInterval = () => {
res.data.slow = "1000.0.0"; 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) { if (res.data.slow) {
exports.latestVersion = res.data.slow; exports.latestVersion = res.data.slow;
} }

@ -53,6 +53,7 @@ class Database {
"patch-2fa-invalidate-used-token.sql": true, "patch-2fa-invalidate-used-token.sql": true,
"patch-notification_sent_history.sql": true, "patch-notification_sent_history.sql": true,
"patch-monitor-basic-auth.sql": true, "patch-monitor-basic-auth.sql": true,
"patch-status-page.sql": true,
} }
/** /**
@ -170,6 +171,7 @@ class Database {
} }
await this.patch2(); await this.patch2();
await this.migrateNewStatusPage();
} }
/** /**
@ -211,6 +213,74 @@ class Database {
await setSetting("databasePatchedFiles", databasePatchedFiles); 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 * Used it patch2() only
* @param sqlFilename * @param sqlFilename

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

@ -24,18 +24,22 @@ const apicache = require("../modules/apicache");
class Monitor extends BeanModel { 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 * Only show necessary data to public
*/ */
async toPublicJSON() { async toPublicJSON(showTags = false) {
return { let obj = {
id: this.id, id: this.id,
name: this.name, 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() { async toJSON() {
@ -49,7 +53,7 @@ class Monitor extends BeanModel {
notificationIDList[bean.notification_id] = true; 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 { return {
id: this.id, 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 * Encode user and password to Base64 encoding
* for HTTP "basic" auth, as per RFC-7617 * for HTTP "basic" auth, as per RFC-7617
@ -469,6 +477,12 @@ class Monitor extends BeanModel {
stop() { stop() {
clearTimeout(this.heartbeatInterval); clearTimeout(this.heartbeatInterval);
this.isStop = true; 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;

@ -9,36 +9,31 @@ class Pushover extends NotificationProvider {
let okMsg = "Sent Successfully."; let okMsg = "Sent Successfully.";
let pushoverlink = "https://api.pushover.net/1/messages.json"; 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 { try {
if (heartbeatJSON == null) { if (heartbeatJSON == null) {
let data = { await axios.post(pushoverlink, data);
"message": msg, return okMsg;
"user": notification.pushoveruserkey, } else {
"token": notification.pushoverapptoken, data.message += "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"];
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
};
await axios.post(pushoverlink, data); await axios.post(pushoverlink, data);
return okMsg; 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) { } catch (error) {
this.throwGeneralAxiosError(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 Pushbullet = require("./notification-providers/pushbullet");
const Pushover = require("./notification-providers/pushover"); const Pushover = require("./notification-providers/pushover");
const Pushy = require("./notification-providers/pushy"); const Pushy = require("./notification-providers/pushy");
const TechulusPush = require("./notification-providers/techulus-push");
const RocketChat = require("./notification-providers/rocket-chat"); const RocketChat = require("./notification-providers/rocket-chat");
const Signal = require("./notification-providers/signal"); const Signal = require("./notification-providers/signal");
const Slack = require("./notification-providers/slack"); const Slack = require("./notification-providers/slack");
@ -27,6 +28,8 @@ const SerwerSMS = require("./notification-providers/serwersms");
const Stackfield = require("./notification-providers/stackfield"); const Stackfield = require("./notification-providers/stackfield");
const WeCom = require("./notification-providers/wecom"); const WeCom = require("./notification-providers/wecom");
const GoogleChat = require("./notification-providers/google-chat"); const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
const Alerta = require("./notification-providers/alerta");
class Notification { class Notification {
@ -55,6 +58,7 @@ class Notification {
new Pushbullet(), new Pushbullet(),
new Pushover(), new Pushover(),
new Pushy(), new Pushy(),
new TechulusPush(),
new RocketChat(), new RocketChat(),
new Signal(), new Signal(),
new Slack(), new Slack(),
@ -65,7 +69,9 @@ class Notification {
new SerwerSMS(), new SerwerSMS(),
new Stackfield(), new Stackfield(),
new WeCom(), new WeCom(),
new GoogleChat() new GoogleChat(),
new Gorush(),
new Alerta(),
]; ];
for (let item of list) { 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 = { module.exports = {

@ -6,6 +6,7 @@ const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor"); const Monitor = require("../model/monitor");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const { UP, flipStatus, debug } = require("../../src/util"); const { UP, flipStatus, debug } = require("../../src/util");
const StatusPage = require("../model/status_page");
let router = express.Router(); let router = express.Router();
let cache = apicache.middleware; let cache = apicache.middleware;
@ -82,110 +83,80 @@ router.get("/api/push/:pushToken", async (request, response) => {
} }
}); });
// Status Page Config // Status page config, incident, monitor list
router.get("/api/status-page/config", async (_request, response) => { router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(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) { if (!statusPage) {
config.statusPageTheme = "light"; response.statusCode = 404;
} response.json({
msg: "Not Found"
if (! config.statusPagePublished) { });
config.statusPagePublished = true; return;
}
if (! config.statusPageTags) {
config.statusPageTags = false;
}
if (! config.title) {
config.title = "Uptime Kuma";
} }
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 { try {
await checkPublished(); // Incident
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
let incident = await R.findOne("incident", " pin = 1 AND active = 1"); statusPage.id,
]);
if (incident) { if (incident) {
incident = incident.toPublicJSON(); incident = incident.toPublicJSON();
} }
response.json({ // Public Group List
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();
const publicGroupList = []; const publicGroupList = [];
const tagsVisible = (await getSettings("statusPage")).statusPageTags; const showTags = !!statusPage.show_tags;
const list = await R.find("group", " public = 1 ORDER BY weight "); debug("Show Tags???" + showTags);
for (let groupBean of list) { const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
let monitorGroup = await groupBean.toPublicJSON(); statusPage.id
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
};
}));
}
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags);
publicGroupList.push(monitorGroup); publicGroupList.push(monitorGroup);
} }
response.json(publicGroupList); // Response
response.json({
config: await statusPage.toPublicJSON(),
incident,
publicGroupList
});
} catch (error) { } catch (error) {
send403(response, error.message); send403(response, error.message);
} }
}); });
// Status Page Polling Data // Status Page Polling Data
// Can fetch only if published // 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); allowDevAllOrigin(response);
try { try {
await checkPublished();
let heartbeatList = {}; let heartbeatList = {};
let uptimeList = {}; let uptimeList = {};
let slug = request.params.slug;
let statusPageID = await StatusPage.slugToID(slug);
let monitorIDList = await R.getCol(` let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\` SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id WHERE monitor_group.group_id = \`group\`.id
AND public = 1 AND public = 1
`); AND \`group\`.status_page_id = ?
`, [
statusPageID
]);
for (let monitorID of monitorIDList) { for (let monitorID of monitorIDList) {
let list = await R.getAll(` 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 * Default is published
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
*/ */
async function isPublished() { async function isPublished() {
const value = await setting("statusPagePublished"); return true;
if (value === null) {
return true;
}
return value;
} }
function send403(res, msg = "") { function send403(res, msg = "") {

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

@ -1,25 +1,36 @@
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { checkLogin, setSettings } = require("../util-server"); const { checkLogin, setSettings, setSetting } = require("../util-server");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const { debug } = require("../../src/util"); const { debug } = require("../../src/util");
const ImageDataURI = require("../image-data-uri"); const ImageDataURI = require("../image-data-uri");
const Database = require("../database"); const Database = require("../database");
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const StatusPage = require("../model/status_page");
const server = require("../server");
module.exports.statusPageSocketHandler = (socket) => { module.exports.statusPageSocketHandler = (socket) => {
// Post or edit incident // Post or edit incident
socket.on("postIncident", async (incident, callback) => { socket.on("postIncident", async (slug, incident, callback) => {
try { try {
checkLogin(socket); 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; let incidentBean;
if (incident.id) { if (incident.id) {
incidentBean = await R.findOne("incident", " id = ?", [ incidentBean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [
incident.id incident.id,
statusPageID
]); ]);
} }
@ -31,6 +42,7 @@ module.exports.statusPageSocketHandler = (socket) => {
incidentBean.content = incident.content; incidentBean.content = incident.content;
incidentBean.style = incident.style; incidentBean.style = incident.style;
incidentBean.pin = true; incidentBean.pin = true;
incidentBean.status_page_id = statusPageID;
if (incident.id) { if (incident.id) {
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc()); 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 { try {
checkLogin(socket); 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({ callback({
ok: true, ok: true,
@ -71,13 +87,23 @@ module.exports.statusPageSocketHandler = (socket) => {
// Save Status Page // Save Status Page
// imgDataUrl Only Accept PNG! // imgDataUrl Only Accept PNG!
socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => { socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
try { try {
checkLogin(socket); checkSlug(config.slug);
checkLogin(socket);
apicache.clear(); 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,"; const header = "data:image/png;base64,";
// Check logo format // Check logo format
@ -88,16 +114,28 @@ module.exports.statusPageSocketHandler = (socket) => {
throw new Error("Only allowed PNG logo."); throw new Error("Only allowed PNG logo.");
} }
const filename = `logo${statusPage.id}.png`;
// Convert to file // Convert to file
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png"); await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + filename);
config.logo = "/upload/logo.png?t=" + Date.now(); config.logo = `/upload/${filename}?t=` + Date.now();
} else { } else {
config.icon = imgDataUrl; config.icon = imgDataUrl;
} }
// Save Config statusPage.slug = config.slug;
await setSettings("statusPage", config); 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 // Save Public Group List
const groupIDList = []; const groupIDList = [];
@ -106,13 +144,15 @@ module.exports.statusPageSocketHandler = (socket) => {
for (let group of publicGroupList) { for (let group of publicGroupList) {
let groupBean; let groupBean;
if (group.id) { if (group.id) {
groupBean = await R.findOne("group", " id = ? AND public = 1 ", [ groupBean = await R.findOne("group", " id = ? AND public = 1 AND status_page_id = ? ", [
group.id group.id,
statusPage.id
]); ]);
} else { } else {
groupBean = R.dispense("group"); groupBean = R.dispense("group");
} }
groupBean.status_page_id = statusPage.id;
groupBean.name = group.name; groupBean.name = group.name;
groupBean.public = true; groupBean.public = true;
groupBean.weight = groupOrder++; groupBean.weight = groupOrder++;
@ -124,7 +164,6 @@ module.exports.statusPageSocketHandler = (socket) => {
]); ]);
let monitorOrder = 1; let monitorOrder = 1;
console.log(group.monitorList);
for (let monitor of group.monitorList) { for (let monitor of group.monitorList) {
let relationBean = R.dispense("monitor_group"); let relationBean = R.dispense("monitor_group");
@ -141,7 +180,18 @@ module.exports.statusPageSocketHandler = (socket) => {
// Delete groups that not in the list // Delete groups that not in the list
debug("Delete groups that not in the list"); debug("Delete groups that not in the list");
const slots = groupIDList.map(() => "?").join(","); 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({ callback({
ok: true, ok: true,
@ -149,7 +199,7 @@ module.exports.statusPageSocketHandler = (socket) => {
}); });
} catch (error) { } catch (error) {
console.log(error); console.error(error);
callback({ callback({
ok: false, 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) { @media (max-width: 550px) {
.table-shadow-box { .table-shadow-box {
padding: 10px !important; padding: 10px !important;
@ -144,6 +148,10 @@ textarea.form-control {
background-color: #090c10; background-color: #090c10;
color: $dark-font-color; color: $dark-font-color;
mark, .mark {
background-color: #b6ad86;
}
&::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
background: $dark-border-color; background: $dark-border-color;
} }
@ -156,13 +164,24 @@ textarea.form-control {
.form-check-input { .form-check-input {
background-color: $dark-bg2; 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 { .form-switch .form-check-input {
background-color: #232f3b; background-color: #232f3b;
} }
a, a:not(.btn),
.table, .table,
.nav-link { .nav-link {
color: $dark-font-color; color: $dark-font-color;
@ -329,11 +348,8 @@ textarea.form-control {
.monitor-list { .monitor-list {
&.scrollbar { &.scrollbar {
min-height: calc(100vh - 240px);
max-height: calc(100vh - 30px);
overflow-y: auto; overflow-y: auto;
position: sticky; height: calc(100% - 65px);
top: 10px;
} }
.item { .item {
@ -433,6 +449,10 @@ textarea.form-control {
border-radius: 10px !important; border-radius: 10px !important;
} }
.spinner {
color: $primary;
}
// Localization // Localization
@import "localization.scss"; @import "localization.scss";

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

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

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

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

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

@ -4,14 +4,39 @@
<object class="my-4" width="200" height="200" data="/icon.svg" /> <object class="my-4" width="200" height="200" data="/icon.svg" />
<div class="fs-4 fw-bold">Uptime Kuma</div> <div class="fs-4 fw-bold">Uptime Kuma</div>
<div>{{ $t("Version") }}: {{ $root.info.version }}</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>
</div> </div>
</template> </template>
<script> <script>
export default { 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> </script>

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

@ -34,6 +34,9 @@ import {
faAward, faAward,
faLink, faLink,
faChevronDown, faChevronDown,
faPen,
faExternalLinkSquareAlt,
faSpinner,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
library.add( library.add(
@ -67,6 +70,9 @@ library.add(
faAward, faAward,
faLink, faLink,
faChevronDown, faChevronDown,
faPen,
faExternalLinkSquareAlt,
faSpinner,
); );
export { FontAwesomeIcon }; export { FontAwesomeIcon };

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

@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Upravit stavovou stránku", "Edit Status Page": "Upravit stavovou stránku",
"Go to Dashboard": "Přejít na nástěnku", "Go to Dashboard": "Přejít na nástěnku",
"Status Page": "Stavová stránka", "Status Page": "Stavová stránka",
"Status Pages": "Stavová stránka",
defaultNotificationName: "Moje {notification} upozornění ({číslo})", defaultNotificationName: "Moje {notification} upozornění ({číslo})",
here: "sem", here: "sem",
Required: "Vyžadováno", Required: "Vyžadováno",

@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Rediger Statusside", "Edit Status Page": "Rediger Statusside",
"Go to Dashboard": "Gå til Betjeningspanel", "Go to Dashboard": "Gå til Betjeningspanel",
"Status Page": "Statusside", "Status Page": "Statusside",
"Status Pages": "Statusside",
telegram: "Telegram", telegram: "Telegram",
webhook: "Webhook", webhook: "Webhook",
smtp: "Email (SMTP)", smtp: "Email (SMTP)",

@ -4,14 +4,14 @@ export default {
Dashboard: "Dashboard", Dashboard: "Dashboard",
"New Update": "Update verfügbar", "New Update": "Update verfügbar",
Language: "Sprache", Language: "Sprache",
Appearance: "Erscheinung", Appearance: "Erscheinungsbild",
Theme: "Thema", Theme: "Erscheinungsbild",
General: "Allgemein", General: "Allgemein",
Version: "Version", Version: "Version",
"Check Update On GitHub": "Auf GitHub nach Updates suchen", "Check Update On GitHub": "Auf GitHub nach Updates suchen",
List: "Liste", List: "Liste",
Add: "Hinzufügen", Add: "Hinzufügen",
"Add New Monitor": "Neuer Monitor", "Add New Monitor": "Neuen Monitor hinzufügen",
"Quick Stats": "Übersicht", "Quick Stats": "Übersicht",
Up: "Aktiv", Up: "Aktiv",
Down: "Inaktiv", Down: "Inaktiv",
@ -49,20 +49,20 @@ export default {
retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.", retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
Advanced: "Erweitert", Advanced: "Erweitert",
ignoreTLSError: "Ignoriere TLS-/SSL-Fehler von Webseiten", ignoreTLSError: "Ignoriere TLS-/SSL-Fehler von Webseiten",
"Upside Down Mode": "Invertierter Modus", "Upside Down Mode": "Umgekehrter Modus",
upsideDownModeDescription: "Im invertierten Modus wird der Dienst als inaktiv angezeigt, wenn er erreichbar ist.", upsideDownModeDescription: "Im umgekehrten Modus wird der Dienst als inaktiv angezeigt, wenn er erreichbar ist.",
"Max. Redirects": "Max. Weiterleitungen", "Max. Redirects": "Max. Weiterleitungen",
maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Auf 0 setzen, um Weiterleitungen zu deaktivieren.", maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Auf 0 setzen, um Weiterleitungen zu deaktivieren.",
"Accepted Status Codes": "Erlaubte HTTP-Statuscodes", "Accepted Status Codes": "Erlaubte HTTP-Statuscodes",
acceptedStatusCodesDescription: "Wähle die Statuscodes aus, welche als erfolgreich gewertet werden sollen.", acceptedStatusCodesDescription: "Statuscodes auswählen, die als erfolgreiche Verbindung gelten sollen.",
Save: "Speichern", Save: "Speichern",
Notifications: "Benachrichtigungen", Notifications: "Benachrichtigungen",
"Not available, please setup.": "Keine verfügbar, bitte einrichten.", "Not available, please setup.": "Nicht verfügbar, bitte einrichten.",
"Setup Notification": "Benachrichtigung einrichten", "Setup Notification": "Benachrichtigung einrichten",
Light: "Hell", Light: "Hell",
Dark: "Dunkel", Dark: "Dunkel",
Auto: "Auto", Auto: "Auto",
"Theme - Heartbeat Bar": "Thema - Zeitleiste", "Theme - Heartbeat Bar": "Erscheinungsbild - Zeitleiste",
Normal: "Normal", Normal: "Normal",
Bottom: "Unten", Bottom: "Unten",
None: "Keine", None: "Keine",
@ -71,7 +71,7 @@ export default {
"Allow indexing": "Indizierung zulassen", "Allow indexing": "Indizierung zulassen",
"Discourage search engines from indexing site": "Halte Suchmaschinen von der Indexierung der Seite ab", "Discourage search engines from indexing site": "Halte Suchmaschinen von der Indexierung der Seite ab",
"Change Password": "Passwort ändern", "Change Password": "Passwort ändern",
"Current Password": "Derzeitiges Passwort", "Current Password": "Aktuelles Passwort",
"New Password": "Neues Passwort", "New Password": "Neues Passwort",
"Repeat New Password": "Neues Passwort wiederholen", "Repeat New Password": "Neues Passwort wiederholen",
passwordNotMatchMsg: "Passwörter stimmen nicht überein.", passwordNotMatchMsg: "Passwörter stimmen nicht überein.",
@ -87,7 +87,7 @@ export default {
No: "Nein", No: "Nein",
Username: "Benutzername", Username: "Benutzername",
Password: "Passwort", Password: "Passwort",
"Remember me": "Passwort merken", "Remember me": "Angemeldet bleiben",
Login: "Einloggen", Login: "Einloggen",
"No Monitors, please": "Keine Monitore, bitte", "No Monitors, please": "Keine Monitore, bitte",
"add one": "hinzufügen", "add one": "hinzufügen",
@ -110,8 +110,8 @@ export default {
Heartbeats: "Statistiken", Heartbeats: "Statistiken",
confirmClearStatisticsMsg: "Bist du dir sicher, dass du ALLE Statistiken löschen möchtest?", confirmClearStatisticsMsg: "Bist du dir sicher, dass du ALLE Statistiken löschen möchtest?",
"Create your admin account": "Erstelle dein Admin-Konto", "Create your admin account": "Erstelle dein Admin-Konto",
"Repeat Password": "Wiederhole das Passwort", "Repeat Password": "Passwort erneut eingeben",
"Resource Record Type": "Resource Record Type", "Resource Record Type": "Ressourcen Record Typ",
Export: "Export", Export: "Export",
Import: "Import", Import: "Import",
respTime: "Antw.-Zeit (ms)", respTime: "Antw.-Zeit (ms)",
@ -162,8 +162,8 @@ export default {
Purple: "Lila", Purple: "Lila",
Pink: "Pink", Pink: "Pink",
"Search...": "Suchen...", "Search...": "Suchen...",
"Heartbeat Retry Interval": "Heartbeat-Wiederholungsintervall", "Heartbeat Retry Interval": "Überprüfungsintervall",
retryCheckEverySecond: "Versuche alle {0} Sekunden", retryCheckEverySecond: "Alle {0} Sekunden neu versuchen",
"Import Backup": "Backup importieren", "Import Backup": "Backup importieren",
"Export Backup": "Backup exportieren", "Export Backup": "Backup exportieren",
"Avg. Ping": "Durchschn. Ping", "Avg. Ping": "Durchschn. Ping",
@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Bearbeite Status-Seite", "Edit Status Page": "Bearbeite Status-Seite",
"Go to Dashboard": "Gehe zum Dashboard", "Go to Dashboard": "Gehe zum Dashboard",
"Status Page": "Status-Seite", "Status Page": "Status-Seite",
"Status Pages": "Status-Seite",
telegram: "Telegram", telegram: "Telegram",
webhook: "Webhook", webhook: "Webhook",
smtp: "E-Mail (SMTP)", smtp: "E-Mail (SMTP)",
@ -214,8 +215,8 @@ export default {
chatIDNotFound: "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot", chatIDNotFound: "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
"Post URL": "Post URL", "Post URL": "Post URL",
"Content Type": "Content Type", "Content Type": "Content Type",
webhookJsonDesc: "{0} ist gut für alle modernen HTTP-Server sowie Express.js", webhookJsonDesc: "{0} ist gut für alle modernen HTTP-Server, wie z.B. Express.js, geeignet",
webhookFormDataDesc: "{multipart} ist gut für PHP. Die JSON muss mit {decodeFunction} geparst werden", webhookFormDataDesc: "{multipart} ist gut für PHP. Das JSON muss mit {decodeFunction} verarbeitet werden",
secureOptionNone: "Keine / STARTTLS (25, 587)", secureOptionNone: "Keine / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)", secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "TLS-Fehler ignorieren", "Ignore TLS Error": "TLS-Fehler ignorieren",
@ -279,7 +280,7 @@ 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.", 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", "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.", 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", matrix: "Matrix",
promosmsTypeEco: "SMS ECO - billig, aber langsam und oft überladen. 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.", promosmsTypeFlash: "SMS FLASH - Die Nachricht wird automatisch auf dem Empfängergerät angezeigt. Auf polnische Empfänger beschränkt.",

@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Edit Status Page", "Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard", "Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page", "Status Page": "Status Page",
"Status Pages": "Status Pages",
defaultNotificationName: "My {notification} Alert ({number})", defaultNotificationName: "My {notification} Alert ({number})",
here: "here", here: "here",
Required: "Required", Required: "Required",
@ -238,6 +239,7 @@ export default {
"rocket.chat": "Rocket.Chat", "rocket.chat": "Rocket.Chat",
pushover: "Pushover", pushover: "Pushover",
pushy: "Pushy", pushy: "Pushy",
PushByTechulus: "Push by Techulus",
octopush: "Octopush", octopush: "Octopush",
promosms: "PromoSMS", promosms: "PromoSMS",
clicksendsms: "ClickSend SMS", clicksendsms: "ClickSend SMS",
@ -329,21 +331,21 @@ export default {
dark: "dark", dark: "dark",
Post: "Post", Post: "Post",
"Please input title and content": "Please input title and content", "Please input title and content": "Please input title and content",
Created: "Created", "Created": "Created",
"Last Updated": "Last Updated", "Last Updated": "Last Updated",
Unpin: "Unpin", "Unpin": "Unpin",
"Switch to Light Theme": "Switch to Light Theme", "Switch to Light Theme": "Switch to Light Theme",
"Switch to Dark Theme": "Switch to Dark Theme", "Switch to Dark Theme": "Switch to Dark Theme",
"Show Tags": "Show Tags", "Show Tags": "Show Tags",
"Hide Tags": "Hide Tags", "Hide Tags": "Hide Tags",
Description: "Description", "Description": "Description",
"No monitors available.": "No monitors available.", "No monitors available.": "No monitors available.",
"Add one": "Add one", "Add one": "Add one",
"No Monitors": "No Monitors", "No Monitors": "No Monitors",
"Untitled Group": "Untitled Group", "Untitled Group": "Untitled Group",
Services: "Services", "Services": "Services",
Discard: "Discard", "Discard": "Discard",
Cancel: "Cancel", "Cancel": "Cancel",
"Powered by": "Powered by", "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.", 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", serwersms: "SerwerSMS.pl",
@ -351,7 +353,7 @@ export default {
serwersmsAPIPassword: "API Password", serwersmsAPIPassword: "API Password",
serwersmsPhoneNumber: "Phone number", serwersmsPhoneNumber: "Phone number",
serwersmsSenderName: "SMS Sender Name (registered via customer portal)", serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
"stackfield": "Stackfield", stackfield: "Stackfield",
smtpDkimSettings: "DKIM Settings", smtpDkimSettings: "DKIM Settings",
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.", smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
documentation: "documentation", documentation: "documentation",
@ -361,4 +363,12 @@ export default {
smtpDkimHashAlgo: "Hash Algorithm (Optional)", smtpDkimHashAlgo: "Hash Algorithm (Optional)",
smtpDkimheaderFieldNames: "Header Keys to sign (Optional)", smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
smtpDkimskipFields: "Header Keys not 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?",
}; };

@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Editar página de estado", "Edit Status Page": "Editar página de estado",
"Go to Dashboard": "Ir al panel de control", "Go to Dashboard": "Ir al panel de control",
"Status Page": "Página de estado", "Status Page": "Página de estado",
"Status Pages": "Página de estado",
telegram: "Telegram", telegram: "Telegram",
webhook: "Webhook", webhook: "Webhook",
smtp: "Email (SMTP)", smtp: "Email (SMTP)",

@ -17,6 +17,7 @@ export default {
pauseMonitorMsg: "Kas soovid peatada seire?", pauseMonitorMsg: "Kas soovid peatada seire?",
Settings: "Seaded", Settings: "Seaded",
"Status Page": "Ülevaade", "Status Page": "Ülevaade",
"Status Pages": "Ülevaated",
Dashboard: "Töölaud", Dashboard: "Töölaud",
"New Update": "Uuem tarkvara versioon on saadaval.", "New Update": "Uuem tarkvara versioon on saadaval.",
Language: "Keel", Language: "Keel",
@ -197,4 +198,10 @@ export default {
pushbullet: "Pushbullet", pushbullet: "Pushbullet",
line: "LINE", line: "LINE",
mattermost: "Mattermost", mattermost: "Mattermost",
alerta: "Alerta",
alertaApiEndpoint: "API otsik",
alertaEnvironment: "Keskkond",
alertaApiKey: "API võti",
alertaAlertState: "Häireseisund",
alertaRecoverState: "Taasta algolek",
}; };

@ -178,6 +178,7 @@ export default {
"Add a monitor": "اضافه کردن مانیتور", "Add a monitor": "اضافه کردن مانیتور",
"Edit Status Page": "ویرایش صفحه وضعیت", "Edit Status Page": "ویرایش صفحه وضعیت",
"Status Page": "صفحه وضعیت", "Status Page": "صفحه وضعیت",
"Status Pages": "صفحه وضعیت",
"Go to Dashboard": "رفتن به پیشخوان", "Go to Dashboard": "رفتن به پیشخوان",
"Uptime Kuma": "آپتایم کوما", "Uptime Kuma": "آپتایم کوما",
records: "مورد", records: "مورد",

@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Modifier la page de statut", "Edit Status Page": "Modifier la page de statut",
"Go to Dashboard": "Accéder au tableau de bord", "Go to Dashboard": "Accéder au tableau de bord",
"Status Page": "Status Page", "Status Page": "Status Page",
"Status Pages": "Status Pages",
defaultNotificationName: "Ma notification {notification} numéro ({number})", defaultNotificationName: "Ma notification {notification} numéro ({number})",
here: "ici", here: "ici",
Required: "Requis", 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 : ", 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", "Current User": "Utilisateur actuel",
recent: "Récent", recent: "Récent",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "Environement",
alertaApiKey: "Clé de l'API",
alertaAlertState: "État de l'Alerte",
alertaRecoverState: "État de récupération",
}; };

@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Uredi Statusnu stranicu", "Edit Status Page": "Uredi Statusnu stranicu",
"Go to Dashboard": "Na Kontrolnu ploču", "Go to Dashboard": "Na Kontrolnu ploču",
"Status Page": "Statusna stranica", "Status Page": "Statusna stranica",
"Status Pages": "Statusne stranice",
defaultNotificationName: "Moja {number}. {notification} obavijest", defaultNotificationName: "Moja {number}. {notification} obavijest",
here: "ovdje", here: "ovdje",
Required: "Potrebno", Required: "Potrebno",
@ -346,4 +347,30 @@ export default {
Cancel: "Otkaži", Cancel: "Otkaži",
"Powered by": "Pokreće", "Powered by": "Pokreće",
Saved: "Spremljeno", 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?",
}; };

@ -197,6 +197,7 @@ export default {
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
"Status Page": "Státusz oldal", "Status Page": "Státusz oldal",
"Status Pages": "Státusz oldal",
"Primary Base URL": "Elsődleges URL", "Primary Base URL": "Elsődleges URL",
"Push URL": "Meghívandó URL", "Push URL": "Meghívandó URL",
needPushEvery: "Ezt az URL-t kell meghívni minden {0} másodpercben.", needPushEvery: "Ezt az URL-t kell meghívni minden {0} másodpercben.",
@ -361,4 +362,12 @@ export default {
smtpDkimHashAlgo: "Hash algoritmus (nem kötelező)", smtpDkimHashAlgo: "Hash algoritmus (nem kötelező)",
smtpDkimheaderFieldNames: "Fejléc kulcsok a bejelentkezéshez (nem kötelező)", smtpDkimheaderFieldNames: "Fejléc kulcsok a bejelentkezéshez (nem kötelező)",
smtpDkimskipFields: "Fejléc kulcsok egyéb esetben (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",
}; };

@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Edit Halaman Status", "Edit Status Page": "Edit Halaman Status",
"Go to Dashboard": "Pergi ke Dasbor", "Go to Dashboard": "Pergi ke Dasbor",
"Status Page": "Halaman Status", "Status Page": "Halaman Status",
"Status Pages": "Halaman Status",
defaultNotificationName: "{notification} saya Peringatan ({number})", defaultNotificationName: "{notification} saya Peringatan ({number})",
here: "di sini", here: "di sini",
Required: "Dibutuhkan", Required: "Dibutuhkan",

@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Modifica pagina di stato", "Edit Status Page": "Modifica pagina di stato",
"Go to Dashboard": "Vai alla dashboard", "Go to Dashboard": "Vai alla dashboard",
"Status Page": "Pagina di stato", "Status Page": "Pagina di stato",
"Status Pages": "Pagina di stato",
defaultNotificationName: "Notifica {notification} ({number})", defaultNotificationName: "Notifica {notification} ({number})",
here: "qui", here: "qui",
Required: "Obbligatorio", Required: "Obbligatorio",

@ -180,6 +180,7 @@ export default {
"Edit Status Page": "ステータスページ編集", "Edit Status Page": "ステータスページ編集",
"Go to Dashboard": "ダッシュボード", "Go to Dashboard": "ダッシュボード",
"Status Page": "ステータスページ", "Status Page": "ステータスページ",
"Status Pages": "ステータスページ",
telegram: "Telegram", telegram: "Telegram",
webhook: "Webhook", webhook: "Webhook",
smtp: "Email (SMTP)", smtp: "Email (SMTP)",

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

@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Rediger statusside", "Edit Status Page": "Rediger statusside",
"Go to Dashboard": "Gå til Dashboard", "Go to Dashboard": "Gå til Dashboard",
"Status Page": "Statusside", "Status Page": "Statusside",
"Status Pages": "Statusside",
defaultNotificationName: "Min {notification} varsling ({number})", defaultNotificationName: "Min {notification} varsling ({number})",
here: "her", here: "her",
Required: "Obligatorisk", Required: "Obligatorisk",

@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Wijzig status pagina", "Edit Status Page": "Wijzig status pagina",
"Go to Dashboard": "Ga naar Dashboard", "Go to Dashboard": "Ga naar Dashboard",
"Status Page": "Status Pagina", "Status Page": "Status Pagina",
"Status Pages": "Status Pagina",
telegram: "Telegram", telegram: "Telegram",
webhook: "Webhook", webhook: "Webhook",
smtp: "Email (SMTP)", smtp: "Email (SMTP)",

@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Edytuj stronę statusu", "Edit Status Page": "Edytuj stronę statusu",
"Go to Dashboard": "Idź do panelu", "Go to Dashboard": "Idź do panelu",
"Status Page": "Strona statusu", "Status Page": "Strona statusu",
"Status Pages": "Strona statusu",
defaultNotificationName: "Moje powiadomienie {notification} ({number})", defaultNotificationName: "Moje powiadomienie {notification} ({number})",
here: "tutaj", here: "tutaj",
Required: "Wymagane", Required: "Wymagane",

@ -169,6 +169,7 @@ export default {
"Avg. Ping": "Ping Médio.", "Avg. Ping": "Ping Médio.",
"Avg. Response": "Resposta Média. ", "Avg. Response": "Resposta Média. ",
"Status Page": "Página de Status", "Status Page": "Página de Status",
"Status Pages": "Página de Status",
"Entry Page": "Página de entrada", "Entry Page": "Página de entrada",
statusPageNothing: "Nada aqui, por favor, adicione um grupo ou monitor.", statusPageNothing: "Nada aqui, por favor, adicione um grupo ou monitor.",
"No Services": "Nenhum Serviço", "No Services": "Nenhum Serviço",

@ -181,6 +181,7 @@ export default {
"Edit Status Page": "Редактировать", "Edit Status Page": "Редактировать",
"Go to Dashboard": "Панель управления", "Go to Dashboard": "Панель управления",
"Status Page": "Мониторинг", "Status Page": "Мониторинг",
"Status Pages": "Панель мониторингов",
Discard: "Отмена", Discard: "Отмена",
"Create Incident": "Создать инцидент", "Create Incident": "Создать инцидент",
"Switch to Dark Theme": "Тёмная тема", "Switch to Dark Theme": "Тёмная тема",
@ -312,9 +313,9 @@ export default {
"Certificate Chain": "Цепочка сертификатов", "Certificate Chain": "Цепочка сертификатов",
"Valid": "Действительный", "Valid": "Действительный",
"Hide Tags": "Скрыть тэги", "Hide Tags": "Скрыть тэги",
"Title": "Название инцидента:", Title: "Название инцидента:",
"Content": "Содержание инцидента:", Content: "Содержание инцидента:",
"Post": "Опубликовать", Post: "Опубликовать",
"Cancel": "Отмена", "Cancel": "Отмена",
"Created": "Создано", "Created": "Создано",
"Unpin": "Открепить", "Unpin": "Открепить",
@ -334,4 +335,22 @@ export default {
"Description": "Описание", "Description": "Описание",
"Powered by": "Работает на основе скрипта от", "Powered by": "Работает на основе скрипта от",
shrinkDatabaseDescription: "Включает VACUUM для базы данных SQLite. Если ваша база данных была создана на версии 1.10.0 и более, AUTO_VACUUM уже включен и это действие не требуется.", 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 Авторизация",
}; };

@ -182,7 +182,8 @@ export default {
"Add a monitor": "Dodaj monitor", "Add a monitor": "Dodaj monitor",
"Edit Status Page": "Uredi statusno stran", "Edit Status Page": "Uredi statusno stran",
"Go to Dashboard": "Pojdi na nadzorno ploščo", "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})", defaultNotificationName: "Moje {notification} Obvestilo ({number})",
here: "tukaj", here: "tukaj",
Required: "Obvezno", Required: "Obvezno",

@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Edit Status Page", "Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard", "Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page", "Status Page": "Status Page",
"Status Pages": "Status Pages",
telegram: "Telegram", telegram: "Telegram",
webhook: "Webhook", webhook: "Webhook",
smtp: "Email (SMTP)", smtp: "Email (SMTP)",

@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Edit Status Page", "Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard", "Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page", "Status Page": "Status Page",
"Status Pages": "Status Pages",
telegram: "Telegram", telegram: "Telegram",
webhook: "Webhook", webhook: "Webhook",
smtp: "Email (SMTP)", smtp: "Email (SMTP)",

@ -108,94 +108,4 @@ export default {
"Repeat Password": "Upprepa Lösenord", "Repeat Password": "Upprepa Lösenord",
respTime: "Svarstid (ms)", respTime: "Svarstid (ms)",
notAvailableShort: "Ej Tillg.", 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",
}; };

@ -124,7 +124,7 @@ export default {
tokenValidSettingsMsg: "Token geçerli! Şimdi 2FA ayarlarını kaydedebilirsiniz. ", tokenValidSettingsMsg: "Token geçerli! Şimdi 2FA ayarlarını kaydedebilirsiniz. ",
confirmEnableTwoFAMsg: "2FA'ı etkinleştirmek istediğinizden emin misiniz?", confirmEnableTwoFAMsg: "2FA'ı etkinleştirmek istediğinizden emin misiniz?",
confirmDisableTwoFAMsg: "2FA'ı devre dışı bırakmak istediğinize 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", "Import Backup": "Yedeği içe aktar",
"Export Backup": "Yedeği dışa aktar", "Export Backup": "Yedeği dışa aktar",
Export: "Dışa aktar", Export: "Dışa aktar",
@ -149,52 +149,4 @@ export default {
"Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)", "Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)",
Active: "Aktif", Active: "Aktif",
Inactive: "İnaktif", 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",
}; };

@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Sửa trang trạng thái", "Edit Status Page": "Sửa trang trạng thái",
"Go to Dashboard": "Đi tới Dashboard", "Go to Dashboard": "Đi tới Dashboard",
"Status Page": "Trang trạng thái", "Status Page": "Trang trạng thái",
"Status Pages": "Trang trạng thái",
defaultNotificationName: "My {notification} Alerts ({number})", defaultNotificationName: "My {notification} Alerts ({number})",
here: "tại đây", here: "tại đây",
Required: "Bắt buộc", Required: "Bắt buộc",

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

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

@ -183,6 +183,7 @@ export default {
"Edit Status Page": "編輯狀態頁", "Edit Status Page": "編輯狀態頁",
"Go to Dashboard": "前往儀表板", "Go to Dashboard": "前往儀表板",
"Status Page": "狀態頁", "Status Page": "狀態頁",
"Status Pages": "狀態頁",
defaultNotificationName: "我的 {notification} 通知 ({number})", defaultNotificationName: "我的 {notification} 通知 ({number})",
here: "此處", here: "此處",
Required: "必填", Required: "必填",

@ -3,6 +3,9 @@
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection"> <div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
<div class="container-fluid"> <div class="container-fluid">
{{ $root.connectionErrorMsg }} {{ $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>
</div> </div>
@ -18,10 +21,10 @@
</a> </a>
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li class="nav-item me-2"> <li v-if="$root.loggedIn" class="nav-item me-2">
<a href="/status" class="nav-link status-page"> <router-link to="/manage-status-page" class="nav-link">
<font-awesome-icon icon="stream" /> {{ $t("Status Page") }} <font-awesome-icon icon="stream" /> {{ $t("Status Pages") }}
</a> </router-link>
</li> </li>
<li v-if="$root.loggedIn" class="nav-item me-2"> <li v-if="$root.loggedIn" class="nav-item me-2">
<router-link to="/dashboard" class="nav-link"> <router-link to="/dashboard" class="nav-link">
@ -45,7 +48,7 @@
</header> </header>
<main> <main>
<router-view v-if="$root.loggedIn" /> <router-view v-if="$root.loggedIn || forceShowContent" />
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" /> <Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
</main> </main>
@ -157,7 +160,7 @@ export default {
overflow: hidden; overflow: hidden;
text-decoration: none; text-decoration: none;
&.router-link-exact-active { &.router-link-exact-active, &.active {
color: $primary; color: $primary;
font-weight: bold; font-weight: bold;
} }
@ -184,6 +187,8 @@ main {
padding: 5px; padding: 5px;
background-color: crimson; background-color: crimson;
color: white; color: white;
position: fixed;
width: 100%;
} }
.dark { .dark {

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

@ -33,7 +33,7 @@ export default {
return "light"; return "light";
} }
if (this.path === "/status-page" || this.path === "/status") { if (this.path.startsWith("/status-page") || this.path.startsWith("/status")) {
return this.statusPageTheme; return this.statusPageTheme;
} else { } else {
if (this.userTheme === "auto") { 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="row">
<div class="col"> <div class="col">
<h3>{{ $t("Up") }}</h3> <h3>{{ $t("Up") }}</h3>
<span class="num">{{ stats.up }}</span> <span class="num">{{ $root.stats.up }}</span>
</div> </div>
<div class="col"> <div class="col">
<h3>{{ $t("Down") }}</h3> <h3>{{ $t("Down") }}</h3>
<span class="num text-danger">{{ stats.down }}</span> <span class="num text-danger">{{ $root.stats.down }}</span>
</div> </div>
<div class="col"> <div class="col">
<h3>{{ $t("Unknown") }}</h3> <h3>{{ $t("Unknown") }}</h3>
<span class="num text-secondary">{{ stats.unknown }}</span> <span class="num text-secondary">{{ $root.stats.unknown }}</span>
</div> </div>
<div class="col"> <div class="col">
<h3>{{ $t("pauseDashboardHome") }}</h3> <h3>{{ $t("pauseDashboardHome") }}</h3>
<span class="num text-secondary">{{ stats.pause }}</span> <span class="num text-secondary">{{ $root.stats.pause }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -89,37 +89,6 @@ export default {
}; };
}, },
computed: { 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() { importantHeartBeatList() {
let result = []; let result = [];

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

@ -1,215 +1,239 @@
<template> <template>
<div v-if="loadedTheme" class="container mt-3"> <div v-if="loadedTheme" class="container mt-3">
<!-- Logo & Title --> <!-- Sidebar for edit mode -->
<h1 class="mb-4"> <div v-if="enableEditMode" class="sidebar">
<!-- Logo --> <div class="my-3">
<span class="logo-wrapper" @click="showImageCropUploadMethod"> <label for="slug" class="form-label">{{ $t("Slug") }}</label>
<img :src="logoURL" alt class="logo me-2" :class="logoClass" /> <div class="input-group">
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" /> <span id="basic-addon3" class="input-group-text">/status/</span>
</span> <input id="slug" v-model="config.slug" type="text" class="form-control">
</div>
<!-- Uploader --> </div>
<!-- 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>
<a href="/dashboard" class="btn btn-info"> <div class="my-3">
<font-awesome-icon icon="tachometer-alt" /> <label for="title" class="form-label">{{ $t("Title") }}</label>
{{ $t("Go to Dashboard") }} <input id="title" v-model="config.title" type="text" class="form-control">
</a>
</div> </div>
<div v-else> <div class="my-3">
<button class="btn btn-success me-2" @click="save"> <label for="description" class="form-label">{{ $t("Description") }}</label>
<font-awesome-icon icon="save" /> <textarea id="description" v-model="config.description" class="form-control"></textarea>
{{ $t("Save") }} </div>
</button>
<button class="btn btn-danger me-2" @click="discard"> <div class="my-3 form-check form-switch">
<font-awesome-icon icon="save" /> <input id="switch-theme" v-model="config.theme" class="form-check-input" type="checkbox" true-value="dark" false-value="light">
{{ $t("Discard") }} <label class="form-check-label" for="switch-theme">{{ $t("Switch to Dark Theme") }}</label>
</button> </div>
<button class="btn btn-primary btn-add-group me-2" @click="createIncident"> <div class="my-3 form-check form-switch">
<font-awesome-icon icon="bullhorn" /> <input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox">
{{ $t("Create Incident") }} <label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label>
</button> </div>
<!-- <div v-if="false" class="my-3">
<button v-if="isPublished" class="btn btn-light me-2" @click=""> <label for="password" class="form-label">{{ $t("Password") }} <sup>Coming Soon</sup></label>
<font-awesome-icon icon="save" /> <input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control">
{{ $t("Unpublish") }} </div>
</button>
<button v-if="!isPublished" class="btn btn-info me-2" @click=""> <div v-if="false" class="my-3">
<font-awesome-icon icon="save" /> <label for="cname" class="form-label">Domain Names <sup>Coming Soon</sup></label>
{{ $t("Publish") }} <textarea id="cname" v-model="config.domanNames" rows="3" disabled class="form-control" :placeholder="domainNamesPlaceholder"></textarea>
</button>--> </div>
<!-- Set Default Language --> <div class="danger-zone">
<!-- Set theme --> <button class="btn btn-danger me-2" @click="deleteDialog">
<button v-if="theme == 'dark'" class="btn btn-light me-2" @click="changeTheme('light')"> <font-awesome-icon icon="trash" />
<font-awesome-icon icon="save" /> {{ $t("Delete") }}
{{ $t("Switch to Light Theme") }}
</button> </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" /> <font-awesome-icon icon="save" />
{{ $t("Switch to Dark Theme") }} {{ $t("Save") }}
</button> </button>
<button class="btn btn-secondary me-2" @click="changeTagsVisibilty(!tagsVisible)"> <button class="btn btn-danger me-2" @click="discard">
<template v-if="tagsVisible"> <font-awesome-icon icon="save" />
<font-awesome-icon icon="eye-slash" /> {{ $t("Discard") }}
{{ $t("Hide Tags") }}
</template>
<template v-else>
<font-awesome-icon icon="eye" />
{{ $t("Show Tags") }}
</template>
</button> </button>
</div> </div>
</div> </div>
<!-- Incident --> <!-- Main Status Page -->
<div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass"> <div :class="{ edit: enableEditMode}" class="main">
<strong v-if="editIncidentMode">{{ $t("Title") }}:</strong> <!-- Logo & Title -->
<Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" /> <h1 class="mb-4 title-flex">
<!-- Logo -->
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong> <span class="logo-wrapper" @click="showImageCropUploadMethod">
<Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" /> <img :src="logoURL" alt class="logo me-2" :class="logoClass" />
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
<!-- 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> </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"> <!-- Uploader -->
<font-awesome-icon icon="edit" /> <!-- url="/api/status-page/upload-logo" -->
{{ $t("Edit") }} <ImageCropUpload v-model="showImageCropUpload"
</button> 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"> <a href="/manage-status-page" class="btn btn-info">
<font-awesome-icon icon="times" /> <font-awesome-icon icon="tachometer-alt" />
{{ $t("Cancel") }} {{ $t("Go to Dashboard") }}
</button> </a>
</div>
<div v-if="editIncidentMode" class="dropdown d-inline-block me-2"> <div v-else>
<button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <button class="btn btn-primary btn-add-group me-2" @click="createIncident">
{{ $t("Style") }}: {{ $t(incident.style) }} <font-awesome-icon icon="bullhorn" />
{{ $t("Create Incident") }}
</button> </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> </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>
</div>
<!-- Overall Status --> <!-- Incident -->
<div class="shadow-box list p-4 overall-status mb-4"> <div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass">
<div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData"> <strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
<font-awesome-icon icon="question-circle" class="ok" /> <Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" />
{{ $t("No Services") }}
</div>
<template v-else> <strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
<div v-if="allUp"> <Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
<font-awesome-icon icon="check-circle" class="ok" />
{{ $t("All Systems Operational") }}
</div>
<div v-else-if="partialDown"> <!-- Incident Date -->
<font-awesome-icon icon="exclamation-circle" class="warning" /> <div class="date mt-3">
{{ $t("Partially Degraded Service") }} {{ $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>
<div v-else-if="allDown"> <div v-if="editMode" class="mt-3">
<font-awesome-icon icon="times-circle" class="danger" /> <button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident">
{{ $t("Degraded Service") }} <font-awesome-icon icon="bullhorn" />
</div> {{ $t("Post") }}
</button>
<div v-else> <button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident">
<font-awesome-icon icon="question-circle" style="color: #efefef;" /> <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> </div>
</template> </div>
</div>
<!-- Description --> <!-- Overall Status -->
<strong v-if="editMode">{{ $t("Description") }}:</strong> <div class="shadow-box list p-4 overall-status mb-4">
<Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" /> <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"> <template v-else>
<div> <div v-if="allUp">
<button class="btn btn-primary btn-add-group me-2" @click="addGroup"> <font-awesome-icon icon="check-circle" class="ok" />
<font-awesome-icon icon="plus" /> {{ $t("All Systems Operational") }}
{{ $t("Add Group") }} </div>
</button>
<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>
<div class="mt-3"> <!-- Description -->
<div v-if="allMonitorList.length > 0 && loadedData"> <strong v-if="editMode">{{ $t("Description") }}:</strong>
<label>{{ $t("Add a monitor") }}:</label> <Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" />
<select v-model="selectedMonitor" class="form-control">
<option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option> <div v-if="editMode" class="mb-4">
</select> <div>
<button class="btn btn-primary btn-add-group me-2" @click="addGroup">
<font-awesome-icon icon="plus" />
{{ $t("Add Group") }}
</button>
</div> </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>
</div>
<div class="mb-4"> <div class="mb-4">
<div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center"> <div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center">
<!-- 👀 Nothing here, please add a group or a monitor. --> <!-- 👀 Nothing here, please add a group or a monitor. -->
👀 {{ $t("statusPageNothing") }} 👀 {{ $t("statusPageNothing") }}
</div>
<PublicGroupList :edit-mode="enableEditMode" :show-tags="config.showTags" />
</div> </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> </div>
<footer class="mt-5 mb-4"> <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteStatusPage">
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a> {{ $t("deleteStatusPageMsg") }}
</footer> </Confirm>
</div> </div>
</template> </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 { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
import dayjs from "dayjs"; import dayjs from "dayjs";
import Favico from "favico.js";
import { getResBaseURL } from "../util-frontend";
import Confirm from "../components/Confirm.vue";
const toast = useToast(); const toast = useToast();
const leavePageMsg = "Do you really want to leave? you have unsaved changes!"; const leavePageMsg = "Do you really want to leave? you have unsaved changes!";
let feedInterval; let feedInterval;
const favicon = new Favico({
animation: "none"
});
export default { export default {
components: { components: {
PublicGroupList, PublicGroupList,
ImageCropUpload ImageCropUpload,
Confirm,
}, },
// Leave Page for vue route change // Leave Page for vue route change
@ -247,6 +280,7 @@ export default {
data() { data() {
return { return {
slug: null,
enableEditMode: false, enableEditMode: false,
enableEditIncidentMode: false, enableEditIncidentMode: false,
hasToken: false, hasToken: false,
@ -259,6 +293,8 @@ export default {
loadedTheme: false, loadedTheme: false,
loadedData: false, loadedData: false,
baseURL: "", baseURL: "",
clickedEditButton: false,
domainNamesPlaceholder: "domain1.com\ndomain2.com\n..."
}; };
}, },
computed: { computed: {
@ -296,15 +332,7 @@ export default {
}, },
isPublished() { isPublished() {
return this.config.statusPagePublished; return this.config.published;
},
theme() {
return this.config.statusPageTheme;
},
tagsVisible() {
return this.config.statusPageTags
}, },
logoClass() { logoClass() {
@ -378,13 +406,28 @@ export default {
}, },
// Set Theme // Set Theme
"config.statusPageTheme"() { "config.theme"() {
this.$root.statusPageTheme = this.config.statusPageTheme; this.$root.statusPageTheme = this.config.theme;
this.loadedTheme = true; this.loadedTheme = true;
}, },
"config.title"(title) { "config.title"(title) {
document.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 // Special handle for dev
const env = process.env.NODE_ENV; this.baseURL = getResBaseURL();
if (env === "development" || localStorage.dev === "dev") {
this.baseURL = location.protocol + "//" + location.hostname + ":3001";
}
}, },
async mounted() { async mounted() {
axios.get("/api/status-page/config").then((res) => { this.slug = this.$route.params.slug;
this.config = res.data;
if (this.config.logo) { if (!this.slug) {
this.imgDataUrl = this.config.logo; 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 (this.config.icon) {
if (res.data.ok) { this.imgDataUrl = this.config.icon;
this.incident = res.data.incident;
} }
});
axios.get("/api/status-page/monitor-list").then((res) => { this.incident = res.data.incident;
this.$root.publicGroupList = res.data; this.$root.publicGroupList = res.data.publicGroupList;
}); });
// 5mins a loop // 5mins a loop
@ -432,31 +471,87 @@ export default {
feedInterval = setInterval(() => { feedInterval = setInterval(() => {
this.updateHeartbeatList(); this.updateHeartbeatList();
}, (300 + 10) * 1000); }, (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: { methods: {
updateHeartbeatList() { updateHeartbeatList() {
// If editMode, it will use the data from websocket. // If editMode, it will use the data from websocket.
if (! this.editMode) { if (! this.editMode) {
axios.get("/api/status-page/heartbeat").then((res) => { axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => {
this.$root.heartbeatList = res.data.heartbeatList; const { heartbeatList, uptimeList } = res.data;
this.$root.uptimeList = res.data.uptimeList;
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; this.loadedData = true;
}); });
} }
}, },
edit() { edit() {
this.$root.initSocketIO(true); if (this.hasToken) {
this.enableEditMode = true; this.$root.initSocketIO(true);
this.enableEditMode = true;
this.clickedEditButton = true;
}
}, },
save() { 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) { if (res.ok) {
this.enableEditMode = false; this.enableEditMode = false;
this.$root.publicGroupList = res.publicGroupList; 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 { } else {
toast.error(res.msg); toast.error(res.msg);
} }
@ -481,30 +576,7 @@ export default {
}, },
discard() { discard() {
location.reload(); location.href = "/status/" + this.slug;
},
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 : []
}
})
}
});
}, },
/** /**
@ -540,7 +612,7 @@ export default {
return; return;
} }
this.$root.getSocket().emit("postIncident", this.incident, (res) => { this.$root.getSocket().emit("postIncident", this.slug, this.incident, (res) => {
if (res.ok) { if (res.ok) {
this.enableEditIncidentMode = false; this.enableEditIncidentMode = false;
@ -571,7 +643,7 @@ export default {
}, },
unpinIncident() { unpinIncident() {
this.$root.getSocket().emit("unpinIncident", () => { this.$root.getSocket().emit("unpinIncident", this.slug, () => {
this.incident = null; 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 { footer {
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
@ -623,6 +729,12 @@ footer {
min-width: 50px; min-width: 50px;
} }
.title-flex {
display: flex;
align-items: center;
gap: 10px;
}
.logo-wrapper { .logo-wrapper {
display: inline-block; display: inline-block;
position: relative; 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> </style>

@ -18,6 +18,9 @@ import MonitorHistory from "./components/settings/MonitorHistory.vue";
import Security from "./components/settings/Security.vue"; import Security from "./components/settings/Security.vue";
import Backup from "./components/settings/Backup.vue"; import Backup from "./components/settings/Backup.vue";
import About from "./components/settings/About.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 = [ const routes = [
{ {
@ -70,7 +73,6 @@ const routes = [
children: [ children: [
{ {
path: "general", path: "general",
alias: "",
component: General, 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", path: "/status",
component: StatusPage, component: StatusPage,
}, },
{
path: "/status/:slug",
component: StatusPage,
},
{
path: "/:pathMatch(.*)*",
component: NotFound,
},
]; ];
export const router = createRouter({ export const router = createRouter({

@ -51,7 +51,19 @@ export function timezoneList() {
} }
export function setPageLocale() { export function setPageLocale() {
const html = document.documentElement const html = document.documentElement;
html.setAttribute('lang', currentLocale() ) html.setAttribute("lang", currentLocale() );
html.setAttribute('dir', localeDirection() ) 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