Merge branch 'master' into master
This commit is contained in:
commit
742c6bcaa3
|
@ -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
|
||||
|
||||
1. Draft a release note
|
||||
1. Make sure the repo is cleared
|
||||
1. `npm run update-version 1.X.X`
|
||||
1. `npm run build`
|
||||
1. `npm run build-docker`
|
||||
1. `git push`
|
||||
1. Publish the release note as 1.X.X
|
||||
1. `npm run upload-artifacts` with env vars VERSION=1.X.X;GITHUB_TOKEN=XXXX
|
||||
1. SSH to demo site server and update to 1.X.X
|
||||
2. Make sure the repo is cleared
|
||||
3. `npm run release-final with env vars: `VERSION` and `GITHUB_TOKEN`
|
||||
4. Wait until the `Press any key to continue`
|
||||
5. `git push`
|
||||
6. Publish the release note as 1.X.X
|
||||
7. Press any key to continue
|
||||
8. SSH to demo site server and update to 1.X.X
|
||||
|
||||
Checking:
|
||||
|
||||
|
@ -211,6 +210,15 @@ Checking:
|
|||
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
|
||||
- Try clean installation with Node.js
|
||||
|
||||
### Release Beta Procedures
|
||||
|
||||
1. Draft a release note, check "This is a pre-release"
|
||||
2. Make sure the repo is cleared
|
||||
3. `npm run release-beta` with env vars: `VERSION` and `GITHUB_TOKEN`
|
||||
4. Wait until the `Press any key to continue`
|
||||
5. Publish the release note as 1.X.X-beta.X
|
||||
6. Press any key to continue
|
||||
|
||||
### Release Wiki
|
||||
|
||||
#### Setup Repo
|
||||
|
|
12
README.md
12
README.md
|
@ -61,8 +61,14 @@ npm run setup
|
|||
node server/server.js
|
||||
|
||||
# (Recommended) Option 2. Run in background using PM2
|
||||
# Install PM2 if you don't have it: npm install pm2 -g
|
||||
# Install PM2 if you don't have it:
|
||||
npm install pm2 -g && pm2 install pm2-logrotate
|
||||
|
||||
# Start Server
|
||||
pm2 start server/server.js --name uptime-kuma
|
||||
|
||||
# If you want to see the current console output
|
||||
pm2 monit
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after starting.
|
||||
|
@ -115,7 +121,7 @@ Telegram Notification Sample:
|
|||
|
||||
## Motivation
|
||||
|
||||
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and unmaintained.
|
||||
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and no longer maintained.
|
||||
* Want to build a fancy UI.
|
||||
* Learn Vue 3 and vite.js.
|
||||
* Show the power of Bootstrap 5.
|
||||
|
@ -144,4 +150,4 @@ If you want to translate Uptime Kuma into your language, please read: https://gi
|
|||
|
||||
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||
|
||||
English proofreading is needed too because my grammar is not that great, sadly. Feel free to correct my grammar in this README, source code, or wiki.
|
||||
Unfortunately, English proofreading is needed too because my grammar is not that great. Feel free to correct my grammar in this README, source code, or wiki.
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE [status_page](
|
||||
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
[slug] VARCHAR(255) NOT NULL UNIQUE,
|
||||
[title] VARCHAR(255) NOT NULL,
|
||||
[description] TEXT,
|
||||
[icon] VARCHAR(255) NOT NULL,
|
||||
[theme] VARCHAR(30) NOT NULL,
|
||||
[published] BOOLEAN NOT NULL DEFAULT 1,
|
||||
[search_engine_index] BOOLEAN NOT NULL DEFAULT 1,
|
||||
[show_tags] BOOLEAN NOT NULL DEFAULT 0,
|
||||
[password] VARCHAR,
|
||||
[created_date] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
[modified_date] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX [slug] ON [status_page]([slug]);
|
||||
|
||||
|
||||
CREATE TABLE [status_page_cname](
|
||||
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
[status_page_id] INTEGER NOT NULL REFERENCES [status_page]([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
[domain] VARCHAR NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
ALTER TABLE incident ADD status_page_id INTEGER;
|
||||
ALTER TABLE [group] ADD status_page_id INTEGER;
|
||||
|
||||
COMMIT;
|
|
@ -1,5 +1,5 @@
|
|||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||
FROM node:14-alpine3.12
|
||||
FROM node:16-alpine3.12
|
||||
WORKDIR /app
|
||||
|
||||
# Install apprise, iputils for non-root ping, setpriv
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||
# If the image changed, the second stage image should be changed too
|
||||
FROM node:14-buster-slim
|
||||
FROM node:16-buster-slim
|
||||
WORKDIR /app
|
||||
|
||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
const pkg = require("../../package.json");
|
||||
const fs = require("fs");
|
||||
const child_process = require("child_process");
|
||||
const util = require("../../src/util");
|
||||
|
||||
util.polyfill();
|
||||
|
||||
const oldVersion = pkg.version;
|
||||
const version = process.env.VERSION;
|
||||
|
||||
console.log("Beta Version: " + version);
|
||||
|
||||
if (!oldVersion || oldVersion.includes("-beta.")) {
|
||||
console.error("Error: old version should not be a beta version?");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!version || !version.includes("-beta.")) {
|
||||
console.error("invalid version, beta version only");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const exists = tagExists(version);
|
||||
|
||||
if (! exists) {
|
||||
// Process package.json
|
||||
pkg.version = version;
|
||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||
commit(version);
|
||||
tag(version);
|
||||
|
||||
} else {
|
||||
console.log("version tag exists, please delete the tag or use another tag");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function commit(version) {
|
||||
let msg = "Update to " + version;
|
||||
|
||||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||
let stdout = res.stdout.toString().trim();
|
||||
console.log(stdout);
|
||||
|
||||
if (stdout.includes("no changes added to commit")) {
|
||||
throw new Error("commit error");
|
||||
}
|
||||
|
||||
res = child_process.spawnSync("git", ["push", "origin", "master"]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
function tag(version) {
|
||||
let res = child_process.spawnSync("git", ["tag", version]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
|
||||
res = child_process.spawnSync("git", ["push", "origin", version]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
function tagExists(version) {
|
||||
if (! version) {
|
||||
throw new Error("invalid version");
|
||||
}
|
||||
|
||||
let res = child_process.spawnSync("git", ["tag", "-l", version]);
|
||||
|
||||
return res.stdout.toString().trim() === version;
|
||||
}
|
||||
|
||||
function safeDelete(dir) {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmdirSync(dir, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const childProcess = require("child_process");
|
||||
let env = process.env;
|
||||
|
||||
let cmd = process.argv[2];
|
||||
let args = process.argv.slice(3);
|
||||
let replacedArgs = [];
|
||||
|
||||
for (let arg of args) {
|
||||
for (let key in env) {
|
||||
arg = arg.replaceAll(`$${key}`, env[key]);
|
||||
}
|
||||
replacedArgs.push(arg);
|
||||
}
|
||||
|
||||
let child = childProcess.spawn(cmd, replacedArgs);
|
||||
child.stdout.pipe(process.stdout);
|
||||
child.stderr.pipe(process.stderr);
|
|
@ -189,7 +189,7 @@ if (type == "local") {
|
|||
bash("check=$(pm2 --version)");
|
||||
if (check == "") {
|
||||
println("Installing PM2");
|
||||
bash("npm install pm2 -g");
|
||||
bash("npm install pm2 -g && pm2 install pm2-logrotate");
|
||||
bash("pm2 startup");
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
console.log("Git Push and Publish the release note on github, then press any key to continue");
|
||||
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.resume();
|
||||
process.stdin.on("data", process.exit.bind(process, 0));
|
||||
|
|
@ -6,10 +6,8 @@ const util = require("../src/util");
|
|||
|
||||
util.polyfill();
|
||||
|
||||
const oldVersion = pkg.version;
|
||||
const newVersion = process.argv[2];
|
||||
const newVersion = process.env.VERSION;
|
||||
|
||||
console.log("Old Version: " + oldVersion);
|
||||
console.log("New Version: " + newVersion);
|
||||
|
||||
if (!newVersion) {
|
||||
|
@ -23,23 +21,20 @@ if (!exists) {
|
|||
|
||||
// Process package.json
|
||||
pkg.version = newVersion;
|
||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
|
||||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
|
||||
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion);
|
||||
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion);
|
||||
|
||||
// Replace the version: https://regex101.com/r/hmj2Bc/1
|
||||
pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||
|
||||
commit(newVersion);
|
||||
tag(newVersion);
|
||||
|
||||
updateWiki(oldVersion, newVersion);
|
||||
|
||||
} else {
|
||||
console.log("version exists");
|
||||
}
|
||||
|
||||
function commit(version) {
|
||||
let msg = "update to " + version;
|
||||
let msg = "Update to " + version;
|
||||
|
||||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||
let stdout = res.stdout.toString().trim();
|
||||
|
@ -98,4 +93,4 @@ function safeDelete(dir) {
|
|||
recursive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
const child_process = require("child_process");
|
||||
const fs = require("fs");
|
||||
|
||||
const newVersion = process.env.VERSION;
|
||||
|
||||
if (!newVersion) {
|
||||
console.log("Missing version");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
updateWiki(newVersion);
|
||||
|
||||
function updateWiki(newVersion) {
|
||||
const wikiDir = "./tmp/wiki";
|
||||
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
|
||||
|
||||
safeDelete(wikiDir);
|
||||
|
||||
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
|
||||
let content = fs.readFileSync(howToUpdateFilename).toString();
|
||||
|
||||
// Replace the version: https://regex101.com/r/hmj2Bc/1
|
||||
content = content.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
||||
fs.writeFileSync(howToUpdateFilename, content);
|
||||
|
||||
child_process.spawnSync("git", ["add", "-A"], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion}`], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
console.log("Pushing to Github");
|
||||
child_process.spawnSync("git", ["push"], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
safeDelete(wikiDir);
|
||||
}
|
||||
|
||||
function safeDelete(dir) {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmdirSync(dir, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -159,7 +159,7 @@ fi
|
|||
check=$(pm2 --version)
|
||||
if [ "$check" == "" ]; then
|
||||
"echo" "-e" "Installing PM2"
|
||||
npm install pm2 -g
|
||||
npm install pm2 -g && pm2 install pm2-logrotate
|
||||
pm2 startup
|
||||
fi
|
||||
mkdir -p $installPath
|
||||
|
|
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.12.1",
|
||||
"version": "1.13.1",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -30,15 +30,14 @@
|
|||
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
|
||||
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
||||
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
||||
"build-docker-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.12.1-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-alpine": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:$VERSION-alpine --target release . --push",
|
||||
"build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
|
||||
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||
"setup": "git checkout 1.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",
|
||||
"update-version": "node extra/update-version.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
"remove-2fa": "node extra/remove-2fa.js",
|
||||
|
@ -51,7 +50,10 @@
|
|||
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
||||
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
|
||||
"ncu-patch": "ncu -u -t patch"
|
||||
"ncu-patch": "npm-check-updates -u -t patch",
|
||||
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
||||
"git-remove-tag": "git tag -d"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||
|
@ -61,21 +63,22 @@
|
|||
"@louislam/sqlite3": "~6.0.1",
|
||||
"@popperjs/core": "~2.10.2",
|
||||
"args-parser": "~1.3.0",
|
||||
"axios": "~0.26.0",
|
||||
"axios": "~0.26.1",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"bootstrap": "5.1.3",
|
||||
"bree": "~7.1.0",
|
||||
"bree": "~7.1.5",
|
||||
"chardet": "^1.3.0",
|
||||
"chart.js": "~3.6.0",
|
||||
"chart.js": "~3.6.2",
|
||||
"chartjs-adapter-dayjs": "~1.0.0",
|
||||
"check-password-strength": "^2.0.3",
|
||||
"check-password-strength": "^2.0.5",
|
||||
"command-exists": "~1.2.9",
|
||||
"compare-versions": "~3.6.0",
|
||||
"dayjs": "~1.10.7",
|
||||
"express": "~4.17.1",
|
||||
"express-basic-auth": "~1.2.0",
|
||||
"dayjs": "~1.10.8",
|
||||
"express": "~4.17.3",
|
||||
"express-basic-auth": "~1.2.1",
|
||||
"favico.js": "^0.3.10",
|
||||
"form-data": "~4.0.0",
|
||||
"http-graceful-shutdown": "~3.1.5",
|
||||
"http-graceful-shutdown": "~3.1.7",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
|
@ -84,9 +87,9 @@
|
|||
"notp": "~2.0.3",
|
||||
"password-hash": "~1.2.2",
|
||||
"postcss-rtlcss": "~3.4.1",
|
||||
"postcss-scss": "~4.0.2",
|
||||
"postcss-scss": "~4.0.3",
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.0",
|
||||
"prometheus-api-metrics": "~3.2.1",
|
||||
"qrcode": "~1.5.0",
|
||||
"redbean-node": "0.1.3",
|
||||
"socket.io": "~4.4.1",
|
||||
|
@ -104,7 +107,7 @@
|
|||
"vue-image-crop-upload": "~3.0.3",
|
||||
"vue-multiselect": "~3.0.0-alpha.2",
|
||||
"vue-qrcode": "~1.0.0",
|
||||
"vue-router": "~4.0.12",
|
||||
"vue-router": "~4.0.14",
|
||||
"vue-toastification": "~2.0.0-rc.5",
|
||||
"vuedraggable": "~4.1.0"
|
||||
},
|
||||
|
@ -112,10 +115,10 @@
|
|||
"@actions/github": "~5.0.0",
|
||||
"@babel/eslint-parser": "~7.15.8",
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"@types/bootstrap": "~5.1.6",
|
||||
"@vitejs/plugin-legacy": "~1.6.3",
|
||||
"@types/bootstrap": "~5.1.9",
|
||||
"@vitejs/plugin-legacy": "~1.6.4",
|
||||
"@vitejs/plugin-vue": "~1.9.4",
|
||||
"@vue/compiler-sfc": "~3.2.22",
|
||||
"@vue/compiler-sfc": "~3.2.31",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"core-js": "~3.18.3",
|
||||
"cross-env": "~7.0.3",
|
||||
|
@ -123,7 +126,8 @@
|
|||
"eslint": "~7.32.0",
|
||||
"eslint-plugin-vue": "~7.18.0",
|
||||
"jest": "~27.2.5",
|
||||
"jest-puppeteer": "~6.0.0",
|
||||
"jest-puppeteer": "~6.0.3",
|
||||
"npm-check-updates": "^12.5.4",
|
||||
"puppeteer": "~13.1.3",
|
||||
"sass": "~1.42.1",
|
||||
"stylelint": "~14.2.0",
|
||||
|
|
|
@ -31,31 +31,34 @@ exports.login = async function (username, password) {
|
|||
};
|
||||
|
||||
function myAuthorizer(username, password, callback) {
|
||||
setting("disableAuth").then((result) => {
|
||||
if (result) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
// Login Rate Limit
|
||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||
if (pass) {
|
||||
exports.login(username, password).then((user) => {
|
||||
callback(null, user != null);
|
||||
// Login Rate Limit
|
||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||
if (pass) {
|
||||
exports.login(username, password).then((user) => {
|
||||
callback(null, user != null);
|
||||
|
||||
if (user == null) {
|
||||
loginRateLimiter.removeTokens(1);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(null, false);
|
||||
if (user == null) {
|
||||
loginRateLimiter.removeTokens(1);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
callback(null, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exports.basicAuth = basicAuth({
|
||||
authorizer: myAuthorizer,
|
||||
authorizeAsync: true,
|
||||
challenge: true,
|
||||
});
|
||||
exports.basicAuth = async function (req, res, next) {
|
||||
const middleware = basicAuth({
|
||||
authorizer: myAuthorizer,
|
||||
authorizeAsync: true,
|
||||
challenge: true,
|
||||
});
|
||||
|
||||
const disabledAuth = await setting("disableAuth");
|
||||
|
||||
if (!disabledAuth) {
|
||||
middleware(req, res, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const { setSetting } = require("./util-server");
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
const axios = require("axios");
|
||||
const compareVersions = require("compare-versions");
|
||||
|
||||
exports.version = require("../package.json").version;
|
||||
exports.latestVersion = null;
|
||||
|
@ -16,6 +17,19 @@ exports.startInterval = () => {
|
|||
res.data.slow = "1000.0.0";
|
||||
}
|
||||
|
||||
if (!await setting("checkUpdate")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let checkBeta = await setting("checkBeta");
|
||||
|
||||
if (checkBeta && res.data.beta) {
|
||||
if (compareVersions.compare(res.data.beta, res.data.beta, ">")) {
|
||||
exports.latestVersion = res.data.beta;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (res.data.slow) {
|
||||
exports.latestVersion = res.data.slow;
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ class Database {
|
|||
"patch-2fa-invalidate-used-token.sql": true,
|
||||
"patch-notification_sent_history.sql": true,
|
||||
"patch-monitor-basic-auth.sql": true,
|
||||
"patch-status-page.sql": true,
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -170,6 +171,7 @@ class Database {
|
|||
}
|
||||
|
||||
await this.patch2();
|
||||
await this.migrateNewStatusPage();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -211,6 +213,74 @@ class Database {
|
|||
await setSetting("databasePatchedFiles", databasePatchedFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate status page value in setting to "status_page" table
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async migrateNewStatusPage() {
|
||||
|
||||
// Fix 1.13.0 empty slug bug
|
||||
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
|
||||
|
||||
let title = await setting("title");
|
||||
|
||||
if (title) {
|
||||
console.log("Migrating Status Page");
|
||||
|
||||
let statusPageCheck = await R.findOne("status_page", " slug = 'default' ");
|
||||
|
||||
if (statusPageCheck !== null) {
|
||||
console.log("Migrating Status Page - Skip, default slug record is already existing");
|
||||
return;
|
||||
}
|
||||
|
||||
let statusPage = R.dispense("status_page");
|
||||
statusPage.slug = "default";
|
||||
statusPage.title = title;
|
||||
statusPage.description = await setting("description");
|
||||
statusPage.icon = await setting("icon");
|
||||
statusPage.theme = await setting("statusPageTheme");
|
||||
statusPage.published = !!await setting("statusPagePublished");
|
||||
statusPage.search_engine_index = !!await setting("searchEngineIndex");
|
||||
statusPage.show_tags = !!await setting("statusPageTags");
|
||||
statusPage.password = null;
|
||||
|
||||
if (!statusPage.title) {
|
||||
statusPage.title = "My Status Page";
|
||||
}
|
||||
|
||||
if (!statusPage.icon) {
|
||||
statusPage.icon = "";
|
||||
}
|
||||
|
||||
if (!statusPage.theme) {
|
||||
statusPage.theme = "light";
|
||||
}
|
||||
|
||||
let id = await R.store(statusPage);
|
||||
|
||||
await R.exec("UPDATE incident SET status_page_id = ? WHERE status_page_id IS NULL", [
|
||||
id
|
||||
]);
|
||||
|
||||
await R.exec("UPDATE [group] SET status_page_id = ? WHERE status_page_id IS NULL", [
|
||||
id
|
||||
]);
|
||||
|
||||
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
|
||||
|
||||
// Migrate Entry Page if it is status page
|
||||
let entryPage = await setting("entryPage");
|
||||
|
||||
if (entryPage === "statusPage") {
|
||||
await setSetting("entryPage", "statusPage-default", "general");
|
||||
}
|
||||
|
||||
console.log("Migrating Status Page - Done");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Used it patch2() only
|
||||
* @param sqlFilename
|
||||
|
|
|
@ -3,12 +3,12 @@ const { R } = require("redbean-node");
|
|||
|
||||
class Group extends BeanModel {
|
||||
|
||||
async toPublicJSON() {
|
||||
async toPublicJSON(showTags = false) {
|
||||
let monitorBeanList = await this.getMonitorList();
|
||||
let monitorList = [];
|
||||
|
||||
for (let bean of monitorBeanList) {
|
||||
monitorList.push(await bean.toPublicJSON());
|
||||
monitorList.push(await bean.toPublicJSON(showTags));
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -24,18 +24,22 @@ const apicache = require("../modules/apicache");
|
|||
class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return a object that ready to parse to JSON for public
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
*/
|
||||
async toPublicJSON() {
|
||||
return {
|
||||
async toPublicJSON(showTags = false) {
|
||||
let obj = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
};
|
||||
if (showTags) {
|
||||
obj.tags = await this.getTags();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a object that ready to parse to JSON
|
||||
* Return an object that ready to parse to JSON
|
||||
*/
|
||||
async toJSON() {
|
||||
|
||||
|
@ -49,7 +53,7 @@ class Monitor extends BeanModel {
|
|||
notificationIDList[bean.notification_id] = true;
|
||||
}
|
||||
|
||||
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
||||
const tags = await this.getTags();
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
|
@ -82,6 +86,10 @@ class Monitor extends BeanModel {
|
|||
};
|
||||
}
|
||||
|
||||
async getTags() {
|
||||
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode user and password to Base64 encoding
|
||||
* for HTTP "basic" auth, as per RFC-7617
|
||||
|
@ -469,6 +477,12 @@ class Monitor extends BeanModel {
|
|||
stop() {
|
||||
clearTimeout(this.heartbeatInterval);
|
||||
this.isStop = true;
|
||||
|
||||
this.prometheus().remove();
|
||||
}
|
||||
|
||||
prometheus() {
|
||||
return new Prometheus(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { R } = require("redbean-node");
|
||||
|
||||
class StatusPage extends BeanModel {
|
||||
|
||||
static async sendStatusPageList(io, socket) {
|
||||
let result = {};
|
||||
|
||||
let list = await R.findAll("status_page", " ORDER BY title ");
|
||||
|
||||
for (let item of list) {
|
||||
result[item.id] = await item.toJSON();
|
||||
}
|
||||
|
||||
io.to(socket.userID).emit("statusPageList", result);
|
||||
return list;
|
||||
}
|
||||
|
||||
async toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
slug: this.slug,
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
icon: this.getIcon(),
|
||||
theme: this.theme,
|
||||
published: !!this.published,
|
||||
showTags: !!this.show_tags,
|
||||
};
|
||||
}
|
||||
|
||||
async toPublicJSON() {
|
||||
return {
|
||||
slug: this.slug,
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
icon: this.getIcon(),
|
||||
theme: this.theme,
|
||||
published: !!this.published,
|
||||
showTags: !!this.show_tags,
|
||||
};
|
||||
}
|
||||
|
||||
static async slugToID(slug) {
|
||||
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
if (!this.icon) {
|
||||
return "/icon.svg";
|
||||
} else {
|
||||
return this.icon;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = StatusPage;
|
|
@ -0,0 +1,67 @@
|
|||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const axios = require("axios");
|
||||
|
||||
class Alerta extends NotificationProvider {
|
||||
|
||||
name = "alerta";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let alertaUrl = `${notification.alertaApiEndpoint}`;
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
"Authorization": "Key " + notification.alertaapiKey,
|
||||
}
|
||||
};
|
||||
let data = {
|
||||
environment: notification.alertaEnvironment,
|
||||
severity: "critical",
|
||||
correlate: [],
|
||||
service: [ "UptimeKuma" ],
|
||||
value: "Timeout",
|
||||
tags: [ "uptimekuma" ],
|
||||
attributes: {},
|
||||
origin: "uptimekuma",
|
||||
type: "exceptionAlert",
|
||||
};
|
||||
|
||||
if (heartbeatJSON == null) {
|
||||
let postData = Object.assign({
|
||||
event: "msg",
|
||||
text: msg,
|
||||
group: "uptimekuma-msg",
|
||||
resource: "Message",
|
||||
}, data);
|
||||
|
||||
await axios.post(alertaUrl, postData, config);
|
||||
} else {
|
||||
let datadup = Object.assign( {
|
||||
correlate: ["service_up", "service_down"],
|
||||
event: monitorJSON["type"],
|
||||
group: "uptimekuma-" + monitorJSON["type"],
|
||||
resource: monitorJSON["name"],
|
||||
}, data );
|
||||
|
||||
if (heartbeatJSON["status"] == DOWN) {
|
||||
datadup.severity = notification.alertaAlertState; // critical
|
||||
datadup.text = "Service " + monitorJSON["type"] + " is down.";
|
||||
await axios.post(alertaUrl, datadup, config);
|
||||
} else if (heartbeatJSON["status"] == UP) {
|
||||
datadup.severity = notification.alertaRecoverState; // cleaned
|
||||
datadup.text = "Service " + monitorJSON["type"] + " is up.";
|
||||
await axios.post(alertaUrl, datadup, config);
|
||||
}
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Alerta;
|
|
@ -0,0 +1,42 @@
|
|||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Gorush extends NotificationProvider {
|
||||
|
||||
name = "gorush";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
let platformMapping = {
|
||||
"ios": 1,
|
||||
"android": 2,
|
||||
"huawei": 3,
|
||||
};
|
||||
|
||||
try {
|
||||
let data = {
|
||||
"notifications": [
|
||||
{
|
||||
"tokens": [notification.gorushDeviceToken],
|
||||
"platform": platformMapping[notification.gorushPlatform],
|
||||
"message": msg,
|
||||
// Optional
|
||||
"title": notification.gorushTitle,
|
||||
"priority": notification.gorushPriority,
|
||||
"retry": parseInt(notification.gorushRetry) || 0,
|
||||
"topic": notification.gorushTopic,
|
||||
}
|
||||
]
|
||||
};
|
||||
let config = {};
|
||||
|
||||
await axios.post(`${notification.gorushServerURL}/api/push`, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Gorush;
|
|
@ -9,36 +9,31 @@ class Pushover extends NotificationProvider {
|
|||
let okMsg = "Sent Successfully.";
|
||||
let pushoverlink = "https://api.pushover.net/1/messages.json";
|
||||
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg,
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
};
|
||||
|
||||
if (notification.pushoverdevice) {
|
||||
data.device = notification.pushoverdevice;
|
||||
}
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"message": msg,
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
};
|
||||
await axios.post(pushoverlink, data);
|
||||
return okMsg;
|
||||
} else {
|
||||
data.message += "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"];
|
||||
await axios.post(pushoverlink, data);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
};
|
||||
await axios.post(pushoverlink, data);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class TechulusPush extends NotificationProvider {
|
||||
|
||||
name = "PushByTechulus";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, {
|
||||
"title": "Uptime-Kuma",
|
||||
"body": msg,
|
||||
})
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TechulusPush;
|
|
@ -12,6 +12,7 @@ const ClickSendSMS = require("./notification-providers/clicksendsms");
|
|||
const Pushbullet = require("./notification-providers/pushbullet");
|
||||
const Pushover = require("./notification-providers/pushover");
|
||||
const Pushy = require("./notification-providers/pushy");
|
||||
const TechulusPush = require("./notification-providers/techulus-push");
|
||||
const RocketChat = require("./notification-providers/rocket-chat");
|
||||
const Signal = require("./notification-providers/signal");
|
||||
const Slack = require("./notification-providers/slack");
|
||||
|
@ -27,6 +28,8 @@ const SerwerSMS = require("./notification-providers/serwersms");
|
|||
const Stackfield = require("./notification-providers/stackfield");
|
||||
const WeCom = require("./notification-providers/wecom");
|
||||
const GoogleChat = require("./notification-providers/google-chat");
|
||||
const Gorush = require("./notification-providers/gorush");
|
||||
const Alerta = require("./notification-providers/alerta");
|
||||
|
||||
class Notification {
|
||||
|
||||
|
@ -55,6 +58,7 @@ class Notification {
|
|||
new Pushbullet(),
|
||||
new Pushover(),
|
||||
new Pushy(),
|
||||
new TechulusPush(),
|
||||
new RocketChat(),
|
||||
new Signal(),
|
||||
new Slack(),
|
||||
|
@ -65,7 +69,9 @@ class Notification {
|
|||
new SerwerSMS(),
|
||||
new Stackfield(),
|
||||
new WeCom(),
|
||||
new GoogleChat()
|
||||
new GoogleChat(),
|
||||
new Gorush(),
|
||||
new Alerta(),
|
||||
];
|
||||
|
||||
for (let item of list) {
|
||||
|
|
|
@ -86,6 +86,16 @@ class Prometheus {
|
|||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
try {
|
||||
monitor_cert_days_remaining.remove(this.monitorLabelValues);
|
||||
monitor_cert_is_valid.remove(this.monitorLabelValues);
|
||||
monitor_response_time.remove(this.monitorLabelValues);
|
||||
monitor_status.remove(this.monitorLabelValues);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -6,6 +6,7 @@ const apicache = require("../modules/apicache");
|
|||
const Monitor = require("../model/monitor");
|
||||
const dayjs = require("dayjs");
|
||||
const { UP, flipStatus, debug } = require("../../src/util");
|
||||
const StatusPage = require("../model/status_page");
|
||||
let router = express.Router();
|
||||
|
||||
let cache = apicache.middleware;
|
||||
|
@ -82,110 +83,80 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Status Page Config
|
||||
router.get("/api/status-page/config", async (_request, response) => {
|
||||
// Status page config, incident, monitor list
|
||||
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
let slug = request.params.slug;
|
||||
|
||||
let config = await getSettings("statusPage");
|
||||
// Get Status Page
|
||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
|
||||
if (! config.statusPageTheme) {
|
||||
config.statusPageTheme = "light";
|
||||
if (!statusPage) {
|
||||
response.statusCode = 404;
|
||||
response.json({
|
||||
msg: "Not Found"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (! config.statusPagePublished) {
|
||||
config.statusPagePublished = true;
|
||||
}
|
||||
|
||||
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 {
|
||||
await checkPublished();
|
||||
|
||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1");
|
||||
// Incident
|
||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
||||
statusPage.id,
|
||||
]);
|
||||
|
||||
if (incident) {
|
||||
incident = incident.toPublicJSON();
|
||||
}
|
||||
|
||||
// Public Group List
|
||||
const publicGroupList = [];
|
||||
const showTags = !!statusPage.show_tags;
|
||||
debug("Show Tags???" + showTags);
|
||||
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
|
||||
statusPage.id
|
||||
]);
|
||||
|
||||
for (let groupBean of list) {
|
||||
let monitorGroup = await groupBean.toPublicJSON(showTags);
|
||||
publicGroupList.push(monitorGroup);
|
||||
}
|
||||
|
||||
// Response
|
||||
response.json({
|
||||
ok: true,
|
||||
config: await statusPage.toPublicJSON(),
|
||||
incident,
|
||||
publicGroupList
|
||||
});
|
||||
|
||||
} 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 tagsVisible = (await getSettings("statusPage")).statusPageTags;
|
||||
const list = await R.find("group", " public = 1 ORDER BY weight ");
|
||||
for (let groupBean of list) {
|
||||
let monitorGroup = await groupBean.toPublicJSON();
|
||||
if (tagsVisible) {
|
||||
monitorGroup.monitorList = await Promise.all(monitorGroup.monitorList.map(async (monitor) => {
|
||||
// Includes tags as an array in response, allows for tags to be displayed on public status page
|
||||
const tags = await R.getAll(
|
||||
`SELECT monitor_tag.monitor_id, monitor_tag.value, tag.name, tag.color
|
||||
FROM monitor_tag
|
||||
JOIN tag
|
||||
ON monitor_tag.tag_id = tag.id
|
||||
WHERE monitor_tag.monitor_id = ?`, [monitor.id]
|
||||
);
|
||||
return {
|
||||
...monitor,
|
||||
tags: tags
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
publicGroupList.push(monitorGroup);
|
||||
}
|
||||
|
||||
response.json(publicGroupList);
|
||||
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Status Page Polling Data
|
||||
// Can fetch only if published
|
||||
router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => {
|
||||
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
||||
try {
|
||||
await checkPublished();
|
||||
|
||||
let heartbeatList = {};
|
||||
let uptimeList = {};
|
||||
|
||||
let slug = request.params.slug;
|
||||
let statusPageID = await StatusPage.slugToID(slug);
|
||||
|
||||
let monitorIDList = await R.getCol(`
|
||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND public = 1
|
||||
`);
|
||||
AND \`group\`.status_page_id = ?
|
||||
`, [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
for (let monitorID of monitorIDList) {
|
||||
let list = await R.getAll(`
|
||||
|
@ -214,22 +185,12 @@ router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, re
|
|||
}
|
||||
});
|
||||
|
||||
async function checkPublished() {
|
||||
if (! await isPublished()) {
|
||||
throw new Error("The status page is not published");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default is published
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function isPublished() {
|
||||
const value = await setting("statusPagePublished");
|
||||
if (value === null) {
|
||||
return true;
|
||||
}
|
||||
return value;
|
||||
return true;
|
||||
}
|
||||
|
||||
function send403(res, msg = "") {
|
||||
|
|
|
@ -132,6 +132,7 @@ const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sen
|
|||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||
const TwoFA = require("./2fa");
|
||||
const StatusPage = require("./model/status_page");
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
|
@ -200,8 +201,8 @@ exports.entryPage = "dashboard";
|
|||
|
||||
// Entry Page
|
||||
app.get("/", async (_request, response) => {
|
||||
if (exports.entryPage === "statusPage") {
|
||||
response.redirect("/status");
|
||||
if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
||||
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
||||
} else {
|
||||
response.redirect("/dashboard");
|
||||
}
|
||||
|
@ -578,6 +579,9 @@ exports.entryPage = "dashboard";
|
|||
throw new Error("Permission denied.");
|
||||
}
|
||||
|
||||
// Reset Prometheus labels
|
||||
monitorList[monitor.id]?.prometheus()?.remove();
|
||||
|
||||
bean.name = monitor.name;
|
||||
bean.type = monitor.type;
|
||||
bean.url = monitor.url;
|
||||
|
@ -1403,6 +1407,8 @@ async function afterLogin(socket, user) {
|
|||
|
||||
await sleep(500);
|
||||
|
||||
await StatusPage.sendStatusPageList(io, socket);
|
||||
|
||||
for (let monitorID in monitorList) {
|
||||
await sendHeartbeatList(socket, monitorID);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,36 @@
|
|||
const { R } = require("redbean-node");
|
||||
const { checkLogin, setSettings } = require("../util-server");
|
||||
const { checkLogin, setSettings, setSetting } = require("../util-server");
|
||||
const dayjs = require("dayjs");
|
||||
const { debug } = require("../../src/util");
|
||||
const ImageDataURI = require("../image-data-uri");
|
||||
const Database = require("../database");
|
||||
const apicache = require("../modules/apicache");
|
||||
const StatusPage = require("../model/status_page");
|
||||
const server = require("../server");
|
||||
|
||||
module.exports.statusPageSocketHandler = (socket) => {
|
||||
|
||||
// Post or edit incident
|
||||
socket.on("postIncident", async (incident, callback) => {
|
||||
socket.on("postIncident", async (slug, incident, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await R.exec("UPDATE incident SET pin = 0 ");
|
||||
let statusPageID = await StatusPage.slugToID(slug);
|
||||
|
||||
if (!statusPageID) {
|
||||
throw new Error("slug is not found");
|
||||
}
|
||||
|
||||
await R.exec("UPDATE incident SET pin = 0 WHERE status_page_id = ? ", [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
let incidentBean;
|
||||
|
||||
if (incident.id) {
|
||||
incidentBean = await R.findOne("incident", " id = ?", [
|
||||
incident.id
|
||||
incidentBean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [
|
||||
incident.id,
|
||||
statusPageID
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -31,6 +42,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||
incidentBean.content = incident.content;
|
||||
incidentBean.style = incident.style;
|
||||
incidentBean.pin = true;
|
||||
incidentBean.status_page_id = statusPageID;
|
||||
|
||||
if (incident.id) {
|
||||
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
||||
|
@ -52,11 +64,15 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||
}
|
||||
});
|
||||
|
||||
socket.on("unpinIncident", async (callback) => {
|
||||
socket.on("unpinIncident", async (slug, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1");
|
||||
let statusPageID = await StatusPage.slugToID(slug);
|
||||
|
||||
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1 AND status_page_id = ? ", [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
|
@ -71,13 +87,23 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||
|
||||
// Save Status Page
|
||||
// imgDataUrl Only Accept PNG!
|
||||
socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => {
|
||||
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
|
||||
|
||||
try {
|
||||
checkLogin(socket);
|
||||
checkSlug(config.slug);
|
||||
|
||||
checkLogin(socket);
|
||||
apicache.clear();
|
||||
|
||||
// Save Config
|
||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
|
||||
if (!statusPage) {
|
||||
throw new Error("No slug?");
|
||||
}
|
||||
|
||||
const header = "data:image/png;base64,";
|
||||
|
||||
// Check logo format
|
||||
|
@ -88,16 +114,28 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||
throw new Error("Only allowed PNG logo.");
|
||||
}
|
||||
|
||||
const filename = `logo${statusPage.id}.png`;
|
||||
|
||||
// Convert to file
|
||||
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png");
|
||||
config.logo = "/upload/logo.png?t=" + Date.now();
|
||||
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + filename);
|
||||
config.logo = `/upload/${filename}?t=` + Date.now();
|
||||
|
||||
} else {
|
||||
config.icon = imgDataUrl;
|
||||
}
|
||||
|
||||
// Save Config
|
||||
await setSettings("statusPage", config);
|
||||
statusPage.slug = config.slug;
|
||||
statusPage.title = config.title;
|
||||
statusPage.description = config.description;
|
||||
statusPage.icon = config.logo;
|
||||
statusPage.theme = config.theme;
|
||||
//statusPage.published = ;
|
||||
//statusPage.search_engine_index = ;
|
||||
statusPage.show_tags = config.showTags;
|
||||
//statusPage.password = null;
|
||||
statusPage.modified_date = R.isoDateTime();
|
||||
|
||||
await R.store(statusPage);
|
||||
|
||||
// Save Public Group List
|
||||
const groupIDList = [];
|
||||
|
@ -106,13 +144,15 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||
for (let group of publicGroupList) {
|
||||
let groupBean;
|
||||
if (group.id) {
|
||||
groupBean = await R.findOne("group", " id = ? AND public = 1 ", [
|
||||
group.id
|
||||
groupBean = await R.findOne("group", " id = ? AND public = 1 AND status_page_id = ? ", [
|
||||
group.id,
|
||||
statusPage.id
|
||||
]);
|
||||
} else {
|
||||
groupBean = R.dispense("group");
|
||||
}
|
||||
|
||||
groupBean.status_page_id = statusPage.id;
|
||||
groupBean.name = group.name;
|
||||
groupBean.public = true;
|
||||
groupBean.weight = groupOrder++;
|
||||
|
@ -124,7 +164,6 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||
]);
|
||||
|
||||
let monitorOrder = 1;
|
||||
console.log(group.monitorList);
|
||||
|
||||
for (let monitor of group.monitorList) {
|
||||
let relationBean = R.dispense("monitor_group");
|
||||
|
@ -141,7 +180,18 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||
// Delete groups that not in the list
|
||||
debug("Delete groups that not in the list");
|
||||
const slots = groupIDList.map(() => "?").join(",");
|
||||
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
|
||||
|
||||
const data = [
|
||||
...groupIDList,
|
||||
statusPage.id
|
||||
];
|
||||
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
|
||||
|
||||
// Also change entry page to new slug if it is the default one, and slug is changed.
|
||||
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
|
||||
server.entryPage = "statusPage-" + statusPage.slug;
|
||||
await setSetting("entryPage", server.entryPage, "general");
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
|
@ -149,7 +199,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
|
@ -158,4 +208,115 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Add a new status page
|
||||
socket.on("addStatusPage", async (title, slug, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
title = title?.trim();
|
||||
slug = slug?.trim();
|
||||
|
||||
// Check empty
|
||||
if (!title || !slug) {
|
||||
throw new Error("Please input all fields");
|
||||
}
|
||||
|
||||
// Make sure slug is string
|
||||
if (typeof slug !== "string") {
|
||||
throw new Error("Slug -Accept string only");
|
||||
}
|
||||
|
||||
// lower case only
|
||||
slug = slug.toLowerCase();
|
||||
|
||||
checkSlug(slug);
|
||||
|
||||
let statusPage = R.dispense("status_page");
|
||||
statusPage.slug = slug;
|
||||
statusPage.title = title;
|
||||
statusPage.theme = "light";
|
||||
statusPage.icon = "";
|
||||
await R.store(statusPage);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "OK!"
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a status page
|
||||
socket.on("deleteStatusPage", async (slug, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let statusPageID = await StatusPage.slugToID(slug);
|
||||
|
||||
if (statusPageID) {
|
||||
|
||||
// Reset entry page if it is the default one.
|
||||
if (server.entryPage === "statusPage-" + slug) {
|
||||
server.entryPage = "dashboard";
|
||||
await setSetting("entryPage", server.entryPage, "general");
|
||||
}
|
||||
|
||||
// No need to delete records from `status_page_cname`, because it has cascade foreign key.
|
||||
// But for incident & group, it is hard to add cascade foreign key during migration, so they have to be deleted manually.
|
||||
|
||||
// Delete incident
|
||||
await R.exec("DELETE FROM incident WHERE status_page_id = ? ", [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
// Delete group
|
||||
await R.exec("DELETE FROM `group` WHERE status_page_id = ? ", [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
// Delete status_page
|
||||
await R.exec("DELETE FROM status_page WHERE id = ? ", [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
} else {
|
||||
throw new Error("Status Page is not found");
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check slug a-z, 0-9, - only
|
||||
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
|
||||
*/
|
||||
function checkSlug(slug) {
|
||||
if (typeof slug !== "string") {
|
||||
throw new Error("Slug must be string");
|
||||
}
|
||||
|
||||
slug = slug.trim();
|
||||
|
||||
if (!slug) {
|
||||
throw new Error("Slug cannot be empty");
|
||||
}
|
||||
|
||||
if (!slug.match(/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/)) {
|
||||
throw new Error("Invalid Slug");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,6 +92,10 @@ textarea.form-control {
|
|||
}
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
background-color: #161B22;
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.table-shadow-box {
|
||||
padding: 10px !important;
|
||||
|
@ -144,6 +148,10 @@ textarea.form-control {
|
|||
background-color: #090c10;
|
||||
color: $dark-font-color;
|
||||
|
||||
mark, .mark {
|
||||
background-color: #b6ad86;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
|
||||
background: $dark-border-color;
|
||||
}
|
||||
|
@ -156,13 +164,24 @@ textarea.form-control {
|
|||
|
||||
.form-check-input {
|
||||
background-color: $dark-bg2;
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background-color: #282f39;
|
||||
border-color: $dark-border-color;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.form-check-input:checked {
|
||||
border-color: $primary; // Re-apply bootstrap border
|
||||
}
|
||||
|
||||
.form-switch .form-check-input {
|
||||
background-color: #232f3b;
|
||||
}
|
||||
|
||||
a,
|
||||
a:not(.btn),
|
||||
.table,
|
||||
.nav-link {
|
||||
color: $dark-font-color;
|
||||
|
@ -329,11 +348,8 @@ textarea.form-control {
|
|||
|
||||
.monitor-list {
|
||||
&.scrollbar {
|
||||
min-height: calc(100vh - 240px);
|
||||
max-height: calc(100vh - 30px);
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
height: calc(100% - 65px);
|
||||
}
|
||||
|
||||
.item {
|
||||
|
@ -433,6 +449,10 @@ textarea.form-control {
|
|||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
// Localization
|
||||
|
||||
@import "localization.scss";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="shadow-box mb-3">
|
||||
<div class="shadow-box mb-3" :style="boxStyle">
|
||||
<div class="list-header">
|
||||
<div class="placeholder"></div>
|
||||
<div class="search-wrapper">
|
||||
|
@ -63,9 +63,16 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
searchText: "",
|
||||
windowTop: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
boxStyle() {
|
||||
return {
|
||||
height: `calc(100vh - 160px + ${this.windowTop}px)`,
|
||||
};
|
||||
},
|
||||
|
||||
sortedMonitorList() {
|
||||
let result = Object.values(this.$root.monitorList);
|
||||
|
||||
|
@ -108,7 +115,20 @@ export default {
|
|||
return result;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("scroll", this.onScroll);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("scroll", this.onScroll);
|
||||
},
|
||||
methods: {
|
||||
onScroll() {
|
||||
if (window.top.scrollY <= 133) {
|
||||
this.windowTop = window.top.scrollY;
|
||||
} else {
|
||||
this.windowTop = 133;
|
||||
}
|
||||
},
|
||||
monitorURL(id) {
|
||||
return getMonitorRelativeURL(id);
|
||||
},
|
||||
|
@ -122,6 +142,12 @@ export default {
|
|||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.shadow-box {
|
||||
height: calc(100vh - 150px);
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.small-padding {
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
|
@ -142,6 +168,12 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.footer {
|
||||
// background-color: $dark-bg;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
.list-header {
|
||||
margin: -20px;
|
||||
|
|
|
@ -85,7 +85,9 @@ export default {
|
|||
model: null,
|
||||
processing: false,
|
||||
id: null,
|
||||
notificationTypes: Object.keys(NotificationFormList),
|
||||
notificationTypes: Object.keys(NotificationFormList).sort((a, b) => {
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
}),
|
||||
notification: {
|
||||
name: "",
|
||||
/** @type { null | keyof NotificationFormList } */
|
||||
|
@ -143,12 +145,9 @@ export default {
|
|||
this.id = null;
|
||||
this.notification = {
|
||||
name: "",
|
||||
type: null,
|
||||
type: "telegram",
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
// Set Default value here
|
||||
this.notification.type = this.notificationTypes[0];
|
||||
}
|
||||
|
||||
this.modal.show();
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
||||
{{ monitor.element.name }}
|
||||
</div>
|
||||
<div class="tags">
|
||||
<div v-if="showTags" class="tags">
|
||||
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -76,6 +76,9 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
showTags: {
|
||||
type: Boolean,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="alerta-api-endpoint" class="form-label">{{ $t("alertaApiEndpoint") }}</label>
|
||||
<input id="alerta-api-endpoint" v-model="$parent.notification.alertaApiEndpoint" type="text" class="form-control" required>
|
||||
<label for="alerta-environment" class="form-label">{{ $t("alertaEnvironment") }}</label>
|
||||
<input id="alerta-environment" v-model="$parent.notification.alertaEnvironment" type="text" class="form-control" required>
|
||||
<label for="alerta-api-key" class="form-label">{{ $t("alertaApiKey") }}</label>
|
||||
<input id="alerta-api-key" v-model="$parent.notification.alertaApiKey" type="text" class="form-control" required>
|
||||
<label for="alerta-alert-state" class="form-label">{{ $t("alertaAlertState") }}</label>
|
||||
<input id="alerta-alert-state" v-model="$parent.notification.alertaAlertState" type="text" class="form-control" placeholder="critical" required>
|
||||
<label for="alerta-recover-state" class="form-label">{{ $t("alertaRecoverState") }}</label>
|
||||
<input id="alerta-recover-state" v-model="$parent.notification.alertaRecoverState" type="text" class="form-control" placeholder="cleared" required>
|
||||
</div>
|
||||
</template>
|
|
@ -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 Pushover from "./Pushover.vue";
|
||||
import Pushy from "./Pushy.vue";
|
||||
import TechulusPush from "./TechulusPush.vue";
|
||||
import Octopush from "./Octopush.vue";
|
||||
import PromoSMS from "./PromoSMS.vue";
|
||||
import ClickSendSMS from "./ClickSendSMS.vue";
|
||||
|
@ -26,6 +27,8 @@ import SerwerSMS from "./SerwerSMS.vue";
|
|||
import Stackfield from './Stackfield.vue';
|
||||
import WeCom from "./WeCom.vue";
|
||||
import GoogleChat from "./GoogleChat.vue";
|
||||
import Gorush from "./Gorush.vue";
|
||||
import Alerta from "./Alerta.vue";
|
||||
|
||||
/**
|
||||
* Manage all notification form.
|
||||
|
@ -44,6 +47,7 @@ const NotificationFormList = {
|
|||
"rocket.chat": RocketChat,
|
||||
"pushover": Pushover,
|
||||
"pushy": Pushy,
|
||||
"PushByTechulus": TechulusPush,
|
||||
"octopush": Octopush,
|
||||
"promosms": PromoSMS,
|
||||
"clicksendsms": ClickSendSMS,
|
||||
|
@ -60,7 +64,9 @@ const NotificationFormList = {
|
|||
"serwersms": SerwerSMS,
|
||||
"stackfield": Stackfield,
|
||||
"WeCom": WeCom,
|
||||
"GoogleChat": GoogleChat
|
||||
"GoogleChat": GoogleChat,
|
||||
"gorush": Gorush,
|
||||
"alerta": Alerta,
|
||||
};
|
||||
|
||||
export default NotificationFormList;
|
||||
|
|
|
@ -4,14 +4,39 @@
|
|||
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
||||
<div class="fs-4 fw-bold">Uptime Kuma</div>
|
||||
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
|
||||
<div class="my-1 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
|
||||
|
||||
<div class="my-3 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
|
||||
|
||||
<div class="mt-1">
|
||||
<div class="form-check">
|
||||
<label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> Show update if available</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> Also check beta release</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
settings() {
|
||||
return this.$parent.$parent.$parent.settings;
|
||||
},
|
||||
saveSettings() {
|
||||
return this.$parent.$parent.$parent.saveSettings;
|
||||
},
|
||||
settingsLoaded() {
|
||||
return this.$parent.$parent.$parent.settingsLoaded;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -62,31 +62,31 @@
|
|||
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="entryPageYes"
|
||||
id="entryPageDashboard"
|
||||
v-model="settings.entryPage"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="statusPage"
|
||||
name="entryPage"
|
||||
value="dashboard"
|
||||
required
|
||||
/>
|
||||
<label class="form-check-label" for="entryPageYes">
|
||||
<label class="form-check-label" for="entryPageDashboard">
|
||||
{{ $t("Dashboard") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<div v-for="statusPage in $root.statusPageList" :key="statusPage.id" class="form-check">
|
||||
<input
|
||||
id="entryPageNo"
|
||||
:id="'status-page-' + statusPage.id"
|
||||
v-model="settings.entryPage"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="statusPage"
|
||||
value="statusPage"
|
||||
name="entryPage"
|
||||
:value="'statusPage-' + statusPage.slug"
|
||||
required
|
||||
/>
|
||||
<label class="form-check-label" for="entryPageNo">
|
||||
{{ $t("Status Page") }}
|
||||
<label class="form-check-label" :for="'status-page-' + statusPage.id">
|
||||
{{ $t("Status Page") }} - {{ statusPage.title }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -34,6 +34,9 @@ import {
|
|||
faAward,
|
||||
faLink,
|
||||
faChevronDown,
|
||||
faPen,
|
||||
faExternalLinkSquareAlt,
|
||||
faSpinner,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(
|
||||
|
@ -67,6 +70,9 @@ library.add(
|
|||
faAward,
|
||||
faLink,
|
||||
faChevronDown,
|
||||
faPen,
|
||||
faExternalLinkSquareAlt,
|
||||
faSpinner,
|
||||
);
|
||||
|
||||
export { FontAwesomeIcon };
|
||||
|
|
|
@ -197,6 +197,7 @@ export default {
|
|||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
"Status Page": "Статус страница",
|
||||
"Status Pages": "Статус страница",
|
||||
"Primary Base URL": "Основен базов URL адрес",
|
||||
"Push URL": "Генериран Push URL адрес",
|
||||
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди",
|
||||
|
@ -360,4 +361,14 @@ export default {
|
|||
smtpDkimHashAlgo: "Хеш алгоритъм (по желание)",
|
||||
smtpDkimheaderFieldNames: "Хедър ключове за подписване (по желание)",
|
||||
smtpDkimskipFields: "Хедър ключове, които да не се подписват (по желание)",
|
||||
PushByTechulus: "Push от Techulus",
|
||||
GoogleChat: "Google Chat (Само за работното пространство на Google)",
|
||||
gorush: "Gorush",
|
||||
alerta: "Alerta",
|
||||
alertaApiEndpoint: "Крайна точка на API",
|
||||
alertaEnvironment: "Среда",
|
||||
alertaApiKey: "API Ключ",
|
||||
alertaAlertState: "Състояние на тревога",
|
||||
alertaRecoverState: "Състояние на възстановяване",
|
||||
deleteStatusPageMsg: "Сигурни ли сте, че желаете да изтриете тази статус страница?",
|
||||
};
|
||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
|||
"Edit Status Page": "Upravit stavovou stránku",
|
||||
"Go to Dashboard": "Přejít na nástěnku",
|
||||
"Status Page": "Stavová stránka",
|
||||
"Status Pages": "Stavová stránka",
|
||||
defaultNotificationName: "Moje {notification} upozornění ({číslo})",
|
||||
here: "sem",
|
||||
Required: "Vyžadováno",
|
||||
|
|
|
@ -180,6 +180,7 @@ export default {
|
|||
"Edit Status Page": "Rediger Statusside",
|
||||
"Go to Dashboard": "Gå til Betjeningspanel",
|
||||
"Status Page": "Statusside",
|
||||
"Status Pages": "Statusside",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "Email (SMTP)",
|
||||
|
|
|
@ -4,14 +4,14 @@ export default {
|
|||
Dashboard: "Dashboard",
|
||||
"New Update": "Update verfügbar",
|
||||
Language: "Sprache",
|
||||
Appearance: "Erscheinung",
|
||||
Theme: "Thema",
|
||||
Appearance: "Erscheinungsbild",
|
||||
Theme: "Erscheinungsbild",
|
||||
General: "Allgemein",
|
||||
Version: "Version",
|
||||
"Check Update On GitHub": "Auf GitHub nach Updates suchen",
|
||||
List: "Liste",
|
||||
Add: "Hinzufügen",
|
||||
"Add New Monitor": "Neuer Monitor",
|
||||
"Add New Monitor": "Neuen Monitor hinzufügen",
|
||||
"Quick Stats": "Übersicht",
|
||||
Up: "Aktiv",
|
||||
Down: "Inaktiv",
|
||||
|
@ -49,20 +49,20 @@ export default {
|
|||
retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
|
||||
Advanced: "Erweitert",
|
||||
ignoreTLSError: "Ignoriere TLS-/SSL-Fehler von Webseiten",
|
||||
"Upside Down Mode": "Invertierter Modus",
|
||||
upsideDownModeDescription: "Im invertierten Modus wird der Dienst als inaktiv angezeigt, wenn er erreichbar ist.",
|
||||
"Upside Down Mode": "Umgekehrter Modus",
|
||||
upsideDownModeDescription: "Im umgekehrten Modus wird der Dienst als inaktiv angezeigt, wenn er erreichbar ist.",
|
||||
"Max. Redirects": "Max. Weiterleitungen",
|
||||
maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Auf 0 setzen, um Weiterleitungen zu deaktivieren.",
|
||||
"Accepted Status Codes": "Erlaubte HTTP-Statuscodes",
|
||||
acceptedStatusCodesDescription: "Wähle die Statuscodes aus, welche als erfolgreich gewertet werden sollen.",
|
||||
acceptedStatusCodesDescription: "Statuscodes auswählen, die als erfolgreiche Verbindung gelten sollen.",
|
||||
Save: "Speichern",
|
||||
Notifications: "Benachrichtigungen",
|
||||
"Not available, please setup.": "Keine verfügbar, bitte einrichten.",
|
||||
"Not available, please setup.": "Nicht verfügbar, bitte einrichten.",
|
||||
"Setup Notification": "Benachrichtigung einrichten",
|
||||
Light: "Hell",
|
||||
Dark: "Dunkel",
|
||||
Auto: "Auto",
|
||||
"Theme - Heartbeat Bar": "Thema - Zeitleiste",
|
||||
"Theme - Heartbeat Bar": "Erscheinungsbild - Zeitleiste",
|
||||
Normal: "Normal",
|
||||
Bottom: "Unten",
|
||||
None: "Keine",
|
||||
|
@ -71,7 +71,7 @@ export default {
|
|||
"Allow indexing": "Indizierung zulassen",
|
||||
"Discourage search engines from indexing site": "Halte Suchmaschinen von der Indexierung der Seite ab",
|
||||
"Change Password": "Passwort ändern",
|
||||
"Current Password": "Derzeitiges Passwort",
|
||||
"Current Password": "Aktuelles Passwort",
|
||||
"New Password": "Neues Passwort",
|
||||
"Repeat New Password": "Neues Passwort wiederholen",
|
||||
passwordNotMatchMsg: "Passwörter stimmen nicht überein.",
|
||||
|
@ -87,7 +87,7 @@ export default {
|
|||
No: "Nein",
|
||||
Username: "Benutzername",
|
||||
Password: "Passwort",
|
||||
"Remember me": "Passwort merken",
|
||||
"Remember me": "Angemeldet bleiben",
|
||||
Login: "Einloggen",
|
||||
"No Monitors, please": "Keine Monitore, bitte",
|
||||
"add one": "hinzufügen",
|
||||
|
@ -110,8 +110,8 @@ export default {
|
|||
Heartbeats: "Statistiken",
|
||||
confirmClearStatisticsMsg: "Bist du dir sicher, dass du ALLE Statistiken löschen möchtest?",
|
||||
"Create your admin account": "Erstelle dein Admin-Konto",
|
||||
"Repeat Password": "Wiederhole das Passwort",
|
||||
"Resource Record Type": "Resource Record Type",
|
||||
"Repeat Password": "Passwort erneut eingeben",
|
||||
"Resource Record Type": "Ressourcen Record Typ",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
respTime: "Antw.-Zeit (ms)",
|
||||
|
@ -162,8 +162,8 @@ export default {
|
|||
Purple: "Lila",
|
||||
Pink: "Pink",
|
||||
"Search...": "Suchen...",
|
||||
"Heartbeat Retry Interval": "Heartbeat-Wiederholungsintervall",
|
||||
retryCheckEverySecond: "Versuche alle {0} Sekunden",
|
||||
"Heartbeat Retry Interval": "Überprüfungsintervall",
|
||||
retryCheckEverySecond: "Alle {0} Sekunden neu versuchen",
|
||||
"Import Backup": "Backup importieren",
|
||||
"Export Backup": "Backup exportieren",
|
||||
"Avg. Ping": "Durchschn. Ping",
|
||||
|
@ -179,6 +179,7 @@ export default {
|
|||
"Edit Status Page": "Bearbeite Status-Seite",
|
||||
"Go to Dashboard": "Gehe zum Dashboard",
|
||||
"Status Page": "Status-Seite",
|
||||
"Status Pages": "Status-Seite",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "E-Mail (SMTP)",
|
||||
|
@ -214,8 +215,8 @@ export default {
|
|||
chatIDNotFound: "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
|
||||
"Post URL": "Post URL",
|
||||
"Content Type": "Content Type",
|
||||
webhookJsonDesc: "{0} ist gut für alle modernen HTTP-Server sowie Express.js",
|
||||
webhookFormDataDesc: "{multipart} ist gut für PHP. Die JSON muss mit {decodeFunction} geparst werden",
|
||||
webhookJsonDesc: "{0} ist gut für alle modernen HTTP-Server, wie z.B. Express.js, geeignet",
|
||||
webhookFormDataDesc: "{multipart} ist gut für PHP. Das JSON muss mit {decodeFunction} verarbeitet werden",
|
||||
secureOptionNone: "Keine / STARTTLS (25, 587)",
|
||||
secureOptionTLS: "TLS (465)",
|
||||
"Ignore TLS Error": "TLS-Fehler ignorieren",
|
||||
|
@ -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.",
|
||||
"Icon URL": "Icon URL",
|
||||
aboutIconURL: "Du kannst einen Link zu einem Bild in 'Icon URL' übergeben um das Standardprofilbild zu überschreiben. Wird nicht verwendet, wenn ein Icon Emoji gesetzt ist.",
|
||||
aboutMattermostChannelName: "Du kannst den Standardkanal, auf dem der Webhook postet überschreiben, indem der Kanalnamen in das Feld 'Channel Name' eingeben wird. Dies muss in den Mattermost Webhook-Einstellungen aktiviert werden. Ex: #other-channel",
|
||||
aboutMattermostChannelName: "Du kannst den Standardkanal, auf dem der Webhook gesendet wird überschreiben, indem der Kanalnamen in das Feld 'Channel Name' eingeben wird. Dies muss in den Mattermost Webhook-Einstellungen aktiviert werden. Ex: #other-channel",
|
||||
matrix: "Matrix",
|
||||
promosmsTypeEco: "SMS ECO - billig, aber langsam und oft überladen. 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",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
"Status Page": "Status Page",
|
||||
"Status Pages": "Status Pages",
|
||||
defaultNotificationName: "My {notification} Alert ({number})",
|
||||
here: "here",
|
||||
Required: "Required",
|
||||
|
@ -238,6 +239,7 @@ export default {
|
|||
"rocket.chat": "Rocket.Chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
PushByTechulus: "Push by Techulus",
|
||||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
clicksendsms: "ClickSend SMS",
|
||||
|
@ -329,21 +331,21 @@ export default {
|
|||
dark: "dark",
|
||||
Post: "Post",
|
||||
"Please input title and content": "Please input title and content",
|
||||
Created: "Created",
|
||||
"Created": "Created",
|
||||
"Last Updated": "Last Updated",
|
||||
Unpin: "Unpin",
|
||||
"Unpin": "Unpin",
|
||||
"Switch to Light Theme": "Switch to Light Theme",
|
||||
"Switch to Dark Theme": "Switch to Dark Theme",
|
||||
"Show Tags": "Show Tags",
|
||||
"Hide Tags": "Hide Tags",
|
||||
Description: "Description",
|
||||
"Description": "Description",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"Add one": "Add one",
|
||||
"No Monitors": "No Monitors",
|
||||
"Untitled Group": "Untitled Group",
|
||||
Services: "Services",
|
||||
Discard: "Discard",
|
||||
Cancel: "Cancel",
|
||||
"Services": "Services",
|
||||
"Discard": "Discard",
|
||||
"Cancel": "Cancel",
|
||||
"Powered by": "Powered by",
|
||||
shrinkDatabaseDescription: "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
|
||||
serwersms: "SerwerSMS.pl",
|
||||
|
@ -351,7 +353,7 @@ export default {
|
|||
serwersmsAPIPassword: "API Password",
|
||||
serwersmsPhoneNumber: "Phone number",
|
||||
serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
|
||||
"stackfield": "Stackfield",
|
||||
stackfield: "Stackfield",
|
||||
smtpDkimSettings: "DKIM Settings",
|
||||
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
|
||||
documentation: "documentation",
|
||||
|
@ -361,4 +363,12 @@ export default {
|
|||
smtpDkimHashAlgo: "Hash Algorithm (Optional)",
|
||||
smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
|
||||
smtpDkimskipFields: "Header Keys not to sign (Optional)",
|
||||
gorush: "Gorush",
|
||||
alerta: "Alerta",
|
||||
alertaApiEndpoint: "API Endpoint",
|
||||
alertaEnvironment: "Environment",
|
||||
alertaApiKey: "API Key",
|
||||
alertaAlertState: "Alert State",
|
||||
alertaRecoverState: "Recover State",
|
||||
deleteStatusPageMsg: "Are you sure want to delete this status page?",
|
||||
};
|
||||
|
|
|
@ -180,6 +180,7 @@ export default {
|
|||
"Edit Status Page": "Editar página de estado",
|
||||
"Go to Dashboard": "Ir al panel de control",
|
||||
"Status Page": "Página de estado",
|
||||
"Status Pages": "Página de estado",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "Email (SMTP)",
|
||||
|
|
|
@ -17,6 +17,7 @@ export default {
|
|||
pauseMonitorMsg: "Kas soovid peatada seire?",
|
||||
Settings: "Seaded",
|
||||
"Status Page": "Ülevaade",
|
||||
"Status Pages": "Ülevaated",
|
||||
Dashboard: "Töölaud",
|
||||
"New Update": "Uuem tarkvara versioon on saadaval.",
|
||||
Language: "Keel",
|
||||
|
@ -197,4 +198,10 @@ export default {
|
|||
pushbullet: "Pushbullet",
|
||||
line: "LINE",
|
||||
mattermost: "Mattermost",
|
||||
alerta: "Alerta",
|
||||
alertaApiEndpoint: "API otsik",
|
||||
alertaEnvironment: "Keskkond",
|
||||
alertaApiKey: "API võti",
|
||||
alertaAlertState: "Häireseisund",
|
||||
alertaRecoverState: "Taasta algolek",
|
||||
};
|
||||
|
|
|
@ -178,6 +178,7 @@ export default {
|
|||
"Add a monitor": "اضافه کردن مانیتور",
|
||||
"Edit Status Page": "ویرایش صفحه وضعیت",
|
||||
"Status Page": "صفحه وضعیت",
|
||||
"Status Pages": "صفحه وضعیت",
|
||||
"Go to Dashboard": "رفتن به پیشخوان",
|
||||
"Uptime Kuma": "آپتایم کوما",
|
||||
records: "مورد",
|
||||
|
|
|
@ -179,6 +179,7 @@ export default {
|
|||
"Edit Status Page": "Modifier la page de statut",
|
||||
"Go to Dashboard": "Accéder au tableau de bord",
|
||||
"Status Page": "Status Page",
|
||||
"Status Pages": "Status Pages",
|
||||
defaultNotificationName: "Ma notification {notification} numéro ({number})",
|
||||
here: "ici",
|
||||
Required: "Requis",
|
||||
|
@ -304,4 +305,9 @@ export default {
|
|||
steamApiKeyDescription: "Pour surveiller un serveur Steam, vous avez besoin d'une clé Steam Web-API. Vous pouvez enregistrer votre clé ici : ",
|
||||
"Current User": "Utilisateur actuel",
|
||||
recent: "Récent",
|
||||
alertaApiEndpoint: "API Endpoint",
|
||||
alertaEnvironment: "Environement",
|
||||
alertaApiKey: "Clé de l'API",
|
||||
alertaAlertState: "État de l'Alerte",
|
||||
alertaRecoverState: "État de récupération",
|
||||
};
|
||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
|||
"Edit Status Page": "Uredi Statusnu stranicu",
|
||||
"Go to Dashboard": "Na Kontrolnu ploču",
|
||||
"Status Page": "Statusna stranica",
|
||||
"Status Pages": "Statusne stranice",
|
||||
defaultNotificationName: "Moja {number}. {notification} obavijest",
|
||||
here: "ovdje",
|
||||
Required: "Potrebno",
|
||||
|
@ -346,4 +347,30 @@ export default {
|
|||
Cancel: "Otkaži",
|
||||
"Powered by": "Pokreće",
|
||||
Saved: "Spremljeno",
|
||||
PushByTechulus: "Push by Techulus",
|
||||
GoogleChat: "Google Chat (preko platforme Google Workspace)",
|
||||
shrinkDatabaseDescription: "Pokreni VACUUM operaciju za SQLite. Ako je baza podataka kreirana nakon inačice 1.10.0, AUTO_VACUUM opcija već je uključena te ova akcija nije nužna.",
|
||||
serwersms: "SerwerSMS.pl",
|
||||
serwersmsAPIUser: "API korisničko ime (uključujući webapi_ prefiks)",
|
||||
serwersmsAPIPassword: "API lozinka",
|
||||
serwersmsPhoneNumber: "Broj telefona",
|
||||
serwersmsSenderName: "Ime SMS pošiljatelja (registrirano preko korisničkog portala)",
|
||||
stackfield: "Stackfield",
|
||||
smtpDkimSettings: "DKIM postavke",
|
||||
smtpDkimDesc: "Za više informacija, postoji Nodemailer DKIM {0}.",
|
||||
documentation: "dokumentacija",
|
||||
smtpDkimDomain: "Domena",
|
||||
smtpDkimKeySelector: "Odabir ključa",
|
||||
smtpDkimPrivateKey: "Privatni ključ",
|
||||
smtpDkimHashAlgo: "Hash algoritam (neobavezno)",
|
||||
smtpDkimheaderFieldNames: "Ključevi zaglavlja za potpis (neobavezno)",
|
||||
smtpDkimskipFields: "Ključevi zaglavlja koji se neće potpisati (neobavezno)",
|
||||
gorush: "Gorush",
|
||||
alerta: "Alerta",
|
||||
alertaApiEndpoint: "Krajnja točka API-ja (Endpoint)",
|
||||
alertaEnvironment: "Okruženje (Environment)",
|
||||
alertaApiKey: "API ključ",
|
||||
alertaAlertState: "Stanje upozorenja",
|
||||
alertaRecoverState: "Stanje oporavka",
|
||||
deleteStatusPageMsg: "Sigurno želite obrisati ovu statusnu stranicu?",
|
||||
};
|
||||
|
|
|
@ -197,6 +197,7 @@ export default {
|
|||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
"Status Page": "Státusz oldal",
|
||||
"Status Pages": "Státusz oldal",
|
||||
"Primary Base URL": "Elsődleges URL",
|
||||
"Push URL": "Meghívandó URL",
|
||||
needPushEvery: "Ezt az URL-t kell meghívni minden {0} másodpercben.",
|
||||
|
@ -361,4 +362,12 @@ export default {
|
|||
smtpDkimHashAlgo: "Hash algoritmus (nem kötelező)",
|
||||
smtpDkimheaderFieldNames: "Fejléc kulcsok a bejelentkezéshez (nem kötelező)",
|
||||
smtpDkimskipFields: "Fejléc kulcsok egyéb esetben (nem kötelező)",
|
||||
PushByTechulus: "Techulus push",
|
||||
gorush: "Gorush",
|
||||
alerta: "Alerta",
|
||||
alertaApiEndpoint: "API végpont",
|
||||
alertaEnvironment: "Környezet",
|
||||
alertaApiKey: "API kulcs",
|
||||
alertaAlertState: "Figyelmeztetési állapot",
|
||||
alertaRecoverState: "Visszaállási állapot",
|
||||
};
|
||||
|
|
|
@ -179,6 +179,7 @@ export default {
|
|||
"Edit Status Page": "Edit Halaman Status",
|
||||
"Go to Dashboard": "Pergi ke Dasbor",
|
||||
"Status Page": "Halaman Status",
|
||||
"Status Pages": "Halaman Status",
|
||||
defaultNotificationName: "{notification} saya Peringatan ({number})",
|
||||
here: "di sini",
|
||||
Required: "Dibutuhkan",
|
||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
|||
"Edit Status Page": "Modifica pagina di stato",
|
||||
"Go to Dashboard": "Vai alla dashboard",
|
||||
"Status Page": "Pagina di stato",
|
||||
"Status Pages": "Pagina di stato",
|
||||
defaultNotificationName: "Notifica {notification} ({number})",
|
||||
here: "qui",
|
||||
Required: "Obbligatorio",
|
||||
|
|
|
@ -180,6 +180,7 @@ export default {
|
|||
"Edit Status Page": "ステータスページ編集",
|
||||
"Go to Dashboard": "ダッシュボード",
|
||||
"Status Page": "ステータスページ",
|
||||
"Status Pages": "ステータスページ",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "Email (SMTP)",
|
||||
|
|
|
@ -179,6 +179,7 @@ export default {
|
|||
"Edit Status Page": "상태 페이지 수정",
|
||||
"Go to Dashboard": "대시보드로 가기",
|
||||
"Status Page": "상태 페이지",
|
||||
"Status Pages": "상태 페이지",
|
||||
defaultNotificationName: "내 {notification} 알림 ({number})",
|
||||
here: "여기",
|
||||
Required: "필수",
|
||||
|
@ -188,7 +189,7 @@ export default {
|
|||
"Chat ID": "채팅 ID",
|
||||
supportTelegramChatID: "Direct Chat / Group / Channel's Chat ID를 지원해요.",
|
||||
wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.",
|
||||
"YOUR BOT TOKEN HERE": "YOUR BOT TOKEN HERE",
|
||||
"YOUR BOT TOKEN HERE": "여기에 BOT 토큰을 적어주세요.",
|
||||
chatIDNotFound: "채팅 ID를 찾을 수 없어요. 먼저 봇에게 메시지를 보내주세요.",
|
||||
webhook: "Webhook",
|
||||
"Post URL": "Post URL",
|
||||
|
@ -281,15 +282,15 @@ export default {
|
|||
promosmsSMSSender: "SMS 보내는 사람 이름 : 미리 등록된 이름 혹은 기본값 중 하나예요: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
|
||||
"Primary Base URL": "기본 URL",
|
||||
"Push URL": "Push URL",
|
||||
needPushEvery: "You should call this URL every {0} seconds.",
|
||||
pushOptionalParams: "Optional parameters: {0}",
|
||||
emailCustomSubject: "Custom Subject",
|
||||
needPushEvery: "이 URL을 {0} 초 마다 호출할 수 있어요.",
|
||||
pushOptionalParams: "선택적 파라미터: {0}",
|
||||
emailCustomSubject: "커스텀 주제",
|
||||
clicksendsms: "ClickSend SMS",
|
||||
checkPrice: "{0} 가격 확인:",
|
||||
apiCredentials: "API credentials",
|
||||
apiCredentials: "API 인증정보",
|
||||
octopushLegacyHint: "Octopush 레거시 버전 (2011-2020) 을 사용하시나요? 아니면 새 버전을 사용하시나요?",
|
||||
"Feishu WebHookUrl": "Feishu WebHookURL",
|
||||
matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)",
|
||||
matrixHomeserverURL: "Homeserver URL (http(s):// 와 함께 적어주세요. 그리고 포트 번호는 선택적 입니다.)",
|
||||
"Internal Room Id": "내부 방 ID",
|
||||
matrixDesc1: "Matrix 클라이언트 방 설정의 고급 섹션에서 내부 방 ID를 찾을 수 있어요. 내부 방 ID는 이렇게 생겼답니다: !QMdRCpUIfLwsfjxye6:home.server.",
|
||||
matrixDesc2: "사용자의 모든 방에 대한 엑세스가 허용될 수 있어서 새로운 사용자를 만들고 원하는 방에만 초대한 후 엑세스 토큰을 사용하는 것이 좋아요. {0} 이 명령어를 통해 엑세스 토큰을 얻을 수 있어요.",
|
||||
|
@ -349,6 +350,6 @@ export default {
|
|||
serwersmsAPIUser: "API Usename (webapi_ 접두사 포함)",
|
||||
serwersmsAPIPassword: "API 비밀번호",
|
||||
serwersmsPhoneNumber: "휴대전화 번호",
|
||||
serwersmsSenderName: "보내는 사람 이름 (registered via customer portal)",
|
||||
serwersmsSenderName: "보내는 사람 이름 (customer portal를 통해 가입된 정보)",
|
||||
stackfield: "Stackfield",
|
||||
};
|
||||
|
|
|
@ -179,6 +179,7 @@ export default {
|
|||
"Edit Status Page": "Rediger statusside",
|
||||
"Go to Dashboard": "Gå til Dashboard",
|
||||
"Status Page": "Statusside",
|
||||
"Status Pages": "Statusside",
|
||||
defaultNotificationName: "Min {notification} varsling ({number})",
|
||||
here: "her",
|
||||
Required: "Obligatorisk",
|
||||
|
|
|
@ -180,6 +180,7 @@ export default {
|
|||
"Edit Status Page": "Wijzig status pagina",
|
||||
"Go to Dashboard": "Ga naar Dashboard",
|
||||
"Status Page": "Status Pagina",
|
||||
"Status Pages": "Status Pagina",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "Email (SMTP)",
|
||||
|
|
|
@ -179,6 +179,7 @@ export default {
|
|||
"Edit Status Page": "Edytuj stronę statusu",
|
||||
"Go to Dashboard": "Idź do panelu",
|
||||
"Status Page": "Strona statusu",
|
||||
"Status Pages": "Strona statusu",
|
||||
defaultNotificationName: "Moje powiadomienie {notification} ({number})",
|
||||
here: "tutaj",
|
||||
Required: "Wymagane",
|
||||
|
|
|
@ -169,6 +169,7 @@ export default {
|
|||
"Avg. Ping": "Ping Médio.",
|
||||
"Avg. Response": "Resposta Média. ",
|
||||
"Status Page": "Página de Status",
|
||||
"Status Pages": "Página de Status",
|
||||
"Entry Page": "Página de entrada",
|
||||
statusPageNothing: "Nada aqui, por favor, adicione um grupo ou monitor.",
|
||||
"No Services": "Nenhum Serviço",
|
||||
|
|
|
@ -181,6 +181,7 @@ export default {
|
|||
"Edit Status Page": "Редактировать",
|
||||
"Go to Dashboard": "Панель управления",
|
||||
"Status Page": "Мониторинг",
|
||||
"Status Pages": "Панель мониторингов",
|
||||
Discard: "Отмена",
|
||||
"Create Incident": "Создать инцидент",
|
||||
"Switch to Dark Theme": "Тёмная тема",
|
||||
|
@ -312,9 +313,9 @@ export default {
|
|||
"Certificate Chain": "Цепочка сертификатов",
|
||||
"Valid": "Действительный",
|
||||
"Hide Tags": "Скрыть тэги",
|
||||
"Title": "Название инцидента:",
|
||||
"Content": "Содержание инцидента:",
|
||||
"Post": "Опубликовать",
|
||||
Title: "Название инцидента:",
|
||||
Content: "Содержание инцидента:",
|
||||
Post: "Опубликовать",
|
||||
"Cancel": "Отмена",
|
||||
"Created": "Создано",
|
||||
"Unpin": "Открепить",
|
||||
|
@ -334,4 +335,22 @@ export default {
|
|||
"Description": "Описание",
|
||||
"Powered by": "Работает на основе скрипта от",
|
||||
shrinkDatabaseDescription: "Включает VACUUM для базы данных SQLite. Если ваша база данных была создана на версии 1.10.0 и более, AUTO_VACUUM уже включен и это действие не требуется.",
|
||||
deleteStatusPageMsg: "Вы действительно хотите удалить эту страницу статуса сервисов?",
|
||||
Style: "Стиль",
|
||||
info: "ИНФО",
|
||||
warning: "ВНИМАНИЕ",
|
||||
danger: "ОШИБКА",
|
||||
primary: "ОСНОВНОЙ",
|
||||
light: "СВЕТЛЫЙ",
|
||||
dark: "ТЕМНЫЙ",
|
||||
"New Status Page": "Новый мониторинг",
|
||||
"Show update if available": "Показывать доступные обновления",
|
||||
"Also check beta release": "Проверять обновления для бета версий",
|
||||
"Add New Status Page": "Добавить страницу мониторинга",
|
||||
"Next": "Далее",
|
||||
"Accept characters: a-z 0-9 -": "Разрешены символы: a-z 0-9 -",
|
||||
"Start or end with a-z 0-9 only": "Начало и окончание имени только на символы: a-z 0-9",
|
||||
"No consecutive dashes --": "Запрещено использовать тире --",
|
||||
"HTTP Options": "HTTP Опции",
|
||||
"Basic Auth": "HTTP Авторизация",
|
||||
};
|
||||
|
|
|
@ -182,7 +182,8 @@ export default {
|
|||
"Add a monitor": "Dodaj monitor",
|
||||
"Edit Status Page": "Uredi statusno stran",
|
||||
"Go to Dashboard": "Pojdi na nadzorno ploščo",
|
||||
"Status Page": "Status",
|
||||
"Status Page": "Página de Status",
|
||||
"Status Pages": "Página de Status",
|
||||
defaultNotificationName: "Moje {notification} Obvestilo ({number})",
|
||||
here: "tukaj",
|
||||
Required: "Obvezno",
|
||||
|
|
|
@ -180,6 +180,7 @@ export default {
|
|||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
"Status Page": "Status Page",
|
||||
"Status Pages": "Status Pages",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "Email (SMTP)",
|
||||
|
|
|
@ -180,6 +180,7 @@ export default {
|
|||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
"Status Page": "Status Page",
|
||||
"Status Pages": "Status Pages",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "Email (SMTP)",
|
||||
|
|
|
@ -108,94 +108,4 @@ export default {
|
|||
"Repeat Password": "Upprepa Lösenord",
|
||||
respTime: "Svarstid (ms)",
|
||||
notAvailableShort: "Ej Tillg.",
|
||||
Create: "Create",
|
||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
||||
"Clear Data": "Clear Data",
|
||||
Events: "Events",
|
||||
Heartbeats: "Heartbeats",
|
||||
"Auto Get": "Auto Get",
|
||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
||||
"Default enabled": "Default enabled",
|
||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
||||
backupDescription2: "PS: History and event data is not included.",
|
||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
||||
alertNoFile: "Please select a file to import.",
|
||||
alertWrongFileType: "Please select a JSON file.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||
"Verify Token": "Verify Token",
|
||||
"Setup 2FA": "Setup 2FA",
|
||||
"Enable 2FA": "Enable 2FA",
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
Active: "Active",
|
||||
Inactive: "Inactive",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
"Clear all statistics": "Clear all Statistics",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Add New below or Select...",
|
||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||
color: "color",
|
||||
"value (optional)": "value (optional)",
|
||||
Gray: "Gray",
|
||||
Red: "Red",
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Blue: "Blue",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Pink: "Pink",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
"Degraded Service": "Degraded Service",
|
||||
"Add Group": "Add Group",
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
"Status Page": "Status Page",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "Email (SMTP)",
|
||||
discord: "Discord",
|
||||
teams: "Microsoft Teams",
|
||||
signal: "Signal",
|
||||
gotify: "Gotify",
|
||||
slack: "Slack",
|
||||
"rocket.chat": "Rocket.chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
lunasea: "LunaSea",
|
||||
apprise: "Apprise (Support 50+ Notification services)",
|
||||
pushbullet: "Pushbullet",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
};
|
||||
|
|
|
@ -124,7 +124,7 @@ export default {
|
|||
tokenValidSettingsMsg: "Token geçerli! Şimdi 2FA ayarlarını kaydedebilirsiniz. ",
|
||||
confirmEnableTwoFAMsg: "2FA'ı etkinleştirmek istediğinizden emin misiniz?",
|
||||
confirmDisableTwoFAMsg: "2FA'ı devre dışı bırakmak istediğinize emin misiniz?",
|
||||
"Heartbeat Retry Interval": "Sağlık Dırımları Tekrar Deneme Sıklığı",
|
||||
"Heartbeat Retry Interval": "Sağlık Durumları Tekrar Deneme Sıklığı",
|
||||
"Import Backup": "Yedeği içe aktar",
|
||||
"Export Backup": "Yedeği dışa aktar",
|
||||
Export: "Dışa aktar",
|
||||
|
@ -149,52 +149,4 @@ export default {
|
|||
"Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)",
|
||||
Active: "Aktif",
|
||||
Inactive: "İnaktif",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Add New below or Select...",
|
||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||
color: "color",
|
||||
"value (optional)": "value (optional)",
|
||||
Gray: "Gray",
|
||||
Red: "Red",
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Blue: "Blue",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Pink: "Pink",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
"Degraded Service": "Degraded Service",
|
||||
"Add Group": "Add Group",
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
"Status Page": "Status Page",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "Email (SMTP)",
|
||||
discord: "Discord",
|
||||
teams: "Microsoft Teams",
|
||||
signal: "Signal",
|
||||
gotify: "Gotify",
|
||||
slack: "Slack",
|
||||
"rocket.chat": "Rocket.chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
lunasea: "LunaSea",
|
||||
apprise: "Apprise (Support 50+ Notification services)",
|
||||
pushbullet: "Pushbullet",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
};
|
||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
|||
"Edit Status Page": "Sửa trang trạng thái",
|
||||
"Go to Dashboard": "Đi tới Dashboard",
|
||||
"Status Page": "Trang trạng thái",
|
||||
"Status Pages": "Trang trạng thái",
|
||||
defaultNotificationName: "My {notification} Alerts ({number})",
|
||||
here: "tại đây",
|
||||
Required: "Bắt buộc",
|
||||
|
|
|
@ -1,92 +1,104 @@
|
|||
export default {
|
||||
languageName: "简体中文",
|
||||
checkEverySecond: "检测频率 {0} 秒",
|
||||
retriesDescription: "最大重试失败次数",
|
||||
ignoreTLSError: "忽略 HTTPS 站点的证书错误",
|
||||
upsideDownModeDescription: "反向状态监控(状态码范围外为有效状态,反之为无效)",
|
||||
maxRedirectDescription: "最大重定向次数,设置为 0 禁止重定向",
|
||||
acceptedStatusCodesDescription: "选择被视为成功响应的状态码",
|
||||
passwordNotMatchMsg: "两次密码输入不一致",
|
||||
notificationDescription: "请为监控项配置消息通知",
|
||||
keywordDescription: "检测响应内容中的关键字,区分大小写",
|
||||
retryCheckEverySecond: "重试间隔 {0} 秒",
|
||||
retriesDescription: "服务被标记为故障并发送通知之前得最大重试次数",
|
||||
ignoreTLSError: "忽略 HTTPS 站点的 TLS/SSL 错误",
|
||||
upsideDownModeDescription: "反转状态监控,如果服务可访问,则认为是故障。",
|
||||
maxRedirectDescription: "允许的最大重定向次数。设置为 0 禁用重定向。",
|
||||
acceptedStatusCodesDescription: "选择被视为成功响应的状态码。",
|
||||
passwordNotMatchMsg: "两次输入的密码不一致。",
|
||||
notificationDescription: "通知必须被分配给监控项才能正常工作。",
|
||||
keywordDescription: "在纯 HTML 或 JSON 响应中搜索关键字,区分大小写。",
|
||||
pauseDashboardHome: "暂停",
|
||||
deleteMonitorMsg: "确定要删除此监控吗?",
|
||||
deleteNotificationMsg: "确定要删除此消息通知吗?这将对所有监控生效。",
|
||||
resolverserverDescription: "可自定义要使用的 DNS 服务器",
|
||||
deleteMonitorMsg: "确定要删除此监控项吗?",
|
||||
deleteNotificationMsg: "确定要为所有监控项删除此通知吗?",
|
||||
resolverserverDescription: "默认服务器是 Cloudflare。您随时可以修改解析服务器。",
|
||||
rrtypeDescription: "选择要监控的资源记录类型",
|
||||
pauseMonitorMsg: "确定要暂停吗?",
|
||||
enableDefaultNotificationDescription: "新的监控项将默认启用此通知,您仍然为每个监控项单独禁用。",
|
||||
clearEventsMsg: "确定要删除此监控项的所有事件吗?",
|
||||
clearHeartbeatsMsg: "确定要删除此监控项的所有心跳状态吗?",
|
||||
confirmClearStatisticsMsg: "确定要删除所有统计信息吗?",
|
||||
importHandleDescription: "如果想跳过同名的监控项或消息通知,请选择“跳过已存在”。“覆盖”将删除所有现有的监控项和通知。",
|
||||
confirmImportMsg: "确定要导入备份吗?请确保已经选择了正确的导入选项。",
|
||||
twoFAVerifyLabel: "请输入令牌码以确认二次验证:",
|
||||
tokenValidSettingsMsg: "令牌码有效!您现在可以保存二次验证设置了。",
|
||||
confirmEnableTwoFAMsg: "确定要启用二次验证吗?",
|
||||
confirmDisableTwoFAMsg: "确定要禁用二次验证吗?",
|
||||
Settings: "设置",
|
||||
Dashboard: "仪表盘",
|
||||
"New Update": "有新版本更新",
|
||||
"New Update": "有新版本",
|
||||
Language: "语言",
|
||||
Appearance: "外观设置",
|
||||
Appearance: "外观",
|
||||
Theme: "主题",
|
||||
General: "基本设置",
|
||||
"Primary Base URL": "站点地址(URL)",
|
||||
General: "常规",
|
||||
"Primary Base URL": "站点主 URL",
|
||||
About: "关于",
|
||||
Version: "版本",
|
||||
"Check Update On GitHub": "检查更新",
|
||||
"Check Update On GitHub": "检查 GitHub 上的更新",
|
||||
List: "列表",
|
||||
Add: "添加",
|
||||
"Add New Monitor": "创建监控项",
|
||||
"Add New Monitor": "添加监控项",
|
||||
"Quick Stats": "状态速览",
|
||||
Up: "正常",
|
||||
Down: "故障",
|
||||
Pending: "检测失败",
|
||||
Pending: "正在检测",
|
||||
Unknown: "未知",
|
||||
Pause: "暂停",
|
||||
Name: "名称",
|
||||
Status: "状态",
|
||||
DateTime: "时间",
|
||||
Message: "事件",
|
||||
DateTime: "日期时间",
|
||||
Message: "消息",
|
||||
"No important events": "暂无重要事件",
|
||||
Resume: "恢复",
|
||||
Edit: "修改",
|
||||
Edit: "编辑",
|
||||
Delete: "删除",
|
||||
Current: "当前",
|
||||
Uptime: "可用率",
|
||||
Uptime: "在线时间",
|
||||
"Cert Exp.": "证书有效期",
|
||||
days: "天",
|
||||
day: "天",
|
||||
"-day": " 天",
|
||||
hour: "小时",
|
||||
"-hour": " 小时",
|
||||
Response: "响应时长",
|
||||
Response: "响应",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "监控类型",
|
||||
Keyword: "关键字",
|
||||
"Friendly Name": "自定义名称",
|
||||
URL: "网址 URL",
|
||||
"Friendly Name": "显示名称",
|
||||
URL: "URL",
|
||||
Hostname: "主机名",
|
||||
Port: "端口号",
|
||||
"Heartbeat Interval": "心跳间隔",
|
||||
Retries: "重试次数",
|
||||
Advanced: "高级选项",
|
||||
"Upside Down Mode": "反向监控",
|
||||
"Max. Redirects": "重定向次数",
|
||||
"Heartbeat Retry Interval": "心跳重试间隔",
|
||||
Advanced: "高级",
|
||||
"Upside Down Mode": "反转监控",
|
||||
"Max. Redirects": "最大重定向次数",
|
||||
"Accepted Status Codes": "有效状态码",
|
||||
"Push URL": "推送链接",
|
||||
needPushEvery: "你需要每 {0} 秒调用一次",
|
||||
"Push URL": "推送 URL",
|
||||
needPushEvery: "您需要每 {0} 秒调用一次该 URL",
|
||||
pushOptionalParams: "可选参数:{0}",
|
||||
Save: "保存",
|
||||
Notifications: "消息通知",
|
||||
"Not available, please setup.": "无可用通道,请先设置",
|
||||
Notifications: "通知",
|
||||
"Not available, please setup.": "暂不可用,请先设置",
|
||||
"Setup Notification": "设置通知",
|
||||
Light: "明亮",
|
||||
Dark: "黑暗",
|
||||
Auto: "自动",
|
||||
"Theme - Heartbeat Bar": "状态显示",
|
||||
"Theme - Heartbeat Bar": "主题 - 心跳栏",
|
||||
Normal: "正常显示",
|
||||
Bottom: "靠下显示",
|
||||
None: "不显示",
|
||||
Timezone: "时区",
|
||||
"Search Engine Visibility": "搜索引擎设置",
|
||||
"Search Engine Visibility": "搜索引擎可见性",
|
||||
"Allow indexing": "允许索引",
|
||||
"Discourage search engines from indexing site": "阻止搜索引擎索引网站",
|
||||
"Change Password": "修改密码",
|
||||
"Current Password": "当前密码",
|
||||
"New Password": "新的密码",
|
||||
"Repeat New Password": "重复新的密码",
|
||||
"New Password": "新密码",
|
||||
"Repeat New Password": "重复新密码",
|
||||
"Update Password": "更新密码",
|
||||
"Disable Auth": "禁用身份验证",
|
||||
"Enable Auth": "启用身份验证",
|
||||
|
@ -94,74 +106,61 @@ export default {
|
|||
Leave: "离开",
|
||||
"I understand, please disable": "我已了解,继续禁用",
|
||||
Confirm: "确认",
|
||||
Yes: "确定",
|
||||
No: "取消",
|
||||
Yes: "是",
|
||||
No: "否",
|
||||
Username: "用户名",
|
||||
Password: "密码",
|
||||
"Remember me": "记住登录",
|
||||
"Remember me": "记住我",
|
||||
Login: "登录",
|
||||
"No Monitors, please": "还没有监控项,",
|
||||
"add one": "点击新增",
|
||||
"Notification Type": "消息类型",
|
||||
"add one": "点击添加",
|
||||
"Notification Type": "通知类型",
|
||||
Email: "邮件",
|
||||
Test: "测试一下",
|
||||
Test: "测试",
|
||||
"Certificate Info": "证书信息",
|
||||
"Resolver Server": "解析服务器",
|
||||
"Resource Record Type": "资源记录类型",
|
||||
"Last Result": "最后结果",
|
||||
"Create your admin account": "创建管理员账号",
|
||||
"Last Result": "上次结果",
|
||||
"Create your admin account": "创建管理员账户",
|
||||
"Repeat Password": "重复密码",
|
||||
Backup: "备份",
|
||||
"Import Backup": "导入备份",
|
||||
"Export Backup": "导出备份",
|
||||
Export: "导出",
|
||||
Import: "导入",
|
||||
respTime: "响应时间(毫秒)",
|
||||
notAvailableShort: "N/A",
|
||||
"Default enabled": "默认开启",
|
||||
"Apply on all existing monitors": "应用到所有现有监控项",
|
||||
Create: "创建",
|
||||
clearEventsMsg: "确定要删除此监控项的所有事件吗?",
|
||||
clearHeartbeatsMsg: "确定要删除此监控项的所有状态吗?",
|
||||
confirmClearStatisticsMsg: "确定要删除所有统计信息吗?",
|
||||
"Clear Data": "清除数据",
|
||||
Events: "事件",
|
||||
Heartbeats: "心跳",
|
||||
"Auto Get": "自动获取",
|
||||
enableDefaultNotificationDescription: "新的监控项将默认启用,你也可以在每个监控项中分别设置",
|
||||
"Default enabled": "默认开启",
|
||||
"Also apply to existing monitors": "应用到所有监控项",
|
||||
Export: "导出",
|
||||
Import: "导入",
|
||||
backupDescription: "你可以将所有的监控项和消息通知备份到一个 JSON 文件中",
|
||||
backupDescription2: "注意: 不包括历史状态和事件数据",
|
||||
backupDescription3: "导出的文件中可能包含敏感信息,如消息通知的 Token 信息,请小心存放!",
|
||||
alertNoFile: "请选择一个文件导入",
|
||||
alertWrongFileType: "请选择一个 JSON 格式的文件",
|
||||
twoFAVerifyLabel: "请输入 Token 以验证 2FA(二次验证)是否正常工作",
|
||||
tokenValidSettingsMsg: "Token 有效!您现在可以保存 2FA(二次验证)设置",
|
||||
confirmEnableTwoFAMsg: "确定要启用 2FA(二次验证)吗?",
|
||||
confirmDisableTwoFAMsg: "确定要禁用 2FA(二次验证)吗?",
|
||||
"Apply on all existing monitors": "应用到所有监控项",
|
||||
"Verify Token": "验证 Token",
|
||||
"Setup 2FA": "设置 2FA",
|
||||
"Enable 2FA": "启用 2FA",
|
||||
"Disable 2FA": "禁用 2FA",
|
||||
"2FA Settings": "2FA 设置",
|
||||
"Two Factor Authentication": "双因素认证",
|
||||
Active: "生效",
|
||||
Inactive: "未生效",
|
||||
Token: "Token",
|
||||
"Show URI": "显示链接",
|
||||
backupDescription: "您可以将所有监控项和通知备份到 JSON 文件。",
|
||||
backupDescription2: "注意: 不包括历史状态和事件数据。",
|
||||
backupDescription3: "导出的文件可能包含敏感信息,例如通知的令牌信息,请小心存放!",
|
||||
alertNoFile: "请选择要导入的文件",
|
||||
alertWrongFileType: "请选择一个 JSON 文件",
|
||||
"Clear all statistics": "清除所有统计数据",
|
||||
retryCheckEverySecond: "重试间隔 {0} 秒",
|
||||
importHandleDescription: "如果想跳过同名的监控项或通知,请选择“跳过”;“覆盖”将删除所有现有的监控项和通知。",
|
||||
confirmImportMsg: "确定要导入备份吗?请确保已经选择了正确的导入选项。",
|
||||
"Heartbeat Retry Interval": "心跳重试间隔",
|
||||
Backup: "备份",
|
||||
"Import Backup": "导入备份",
|
||||
"Export Backup": "导出备份",
|
||||
"Skip existing": "跳过",
|
||||
"Skip existing": "跳过已存在",
|
||||
Overwrite: "覆盖",
|
||||
Options: "选项",
|
||||
"Keep both": "全部保留",
|
||||
"Verify Token": "验证令牌",
|
||||
"Setup 2FA": "设置二次验证",
|
||||
"Enable 2FA": "启用二次验证",
|
||||
"Disable 2FA": "禁用二次验证",
|
||||
"2FA Settings": "二次验证设置",
|
||||
"Two Factor Authentication": "二次验证",
|
||||
Active: "激活",
|
||||
Inactive: "停用",
|
||||
Token: "令牌",
|
||||
"Show URI": "显示 URI",
|
||||
Tags: "标签",
|
||||
"Add New below or Select...": "在下面新增或选择...",
|
||||
"Tag with this name already exist.": "相同名称的标签已存在",
|
||||
"Tag with this value already exist.": "相同内容的标签已存在",
|
||||
"Add New below or Select...": "在下面添加或选择...",
|
||||
"Tag with this name already exist.": "相同名称的标签已存在。",
|
||||
"Tag with this value already exist.": "相同内容的标签已存在。",
|
||||
color: "颜色",
|
||||
"value (optional)": "值(可选)",
|
||||
Gray: "灰色",
|
||||
|
@ -181,141 +180,142 @@ export default {
|
|||
"All Systems Operational": "所有服务运行正常",
|
||||
"Partially Degraded Service": "部分服务出现故障",
|
||||
"Degraded Service": "全部服务出现故障",
|
||||
"Add Group": "新建分组",
|
||||
"Add Group": "添加分组",
|
||||
"Add a monitor": "添加监控项",
|
||||
"Edit Status Page": "编辑状态页",
|
||||
"Edit Status Page": "编辑状态页面",
|
||||
"Go to Dashboard": "前往仪表盘",
|
||||
"Status Page": "状态页",
|
||||
"Status Page": "状态页面",
|
||||
"Status Pages": "状态页面",
|
||||
defaultNotificationName: "{notification} 通知({number})",
|
||||
here: "这里",
|
||||
Required: "必填",
|
||||
telegram: "Telegram",
|
||||
"Bot Token": "Bot Token",
|
||||
wayToGetTelegramToken: "您可以从 {0} 获取 Token。",
|
||||
"Chat ID": "Chat ID",
|
||||
supportTelegramChatID: "支持对话/群组/频道的 Chat ID",
|
||||
wayToGetTelegramChatID: "您可以发送一条消息给您的机器人,然后访问此链接来查看 chat_id:",
|
||||
"YOUR BOT TOKEN HERE": "这里替换成您的 BOT TOKEN",
|
||||
chatIDNotFound: "未找到 Chat ID,请先给您的机器人发送一条消息。",
|
||||
webhook: "Webhook",
|
||||
"Post URL": "Post URL",
|
||||
"Content Type": "Content Type",
|
||||
webhookJsonDesc: "{0} 适合现代的 HTTP 服务器,例如 Express.js",
|
||||
webhookFormDataDesc: "{multipart} 适合 PHP,其中 JSON 需要使用 {decodeFunction} 解码",
|
||||
smtp: "电子邮件(SMTP)",
|
||||
secureOptionNone: "无 / STARTTLS(常用端口 25、587)",
|
||||
secureOptionTLS: "TLS(常用端口 465)",
|
||||
"Ignore TLS Error": "忽略 TLS 错误",
|
||||
"From Email": "发信人",
|
||||
emailCustomSubject: "邮件主题",
|
||||
"To Email": "收信人",
|
||||
smtpCC: "抄送",
|
||||
smtpBCC: "密送",
|
||||
discord: "Discord",
|
||||
"Discord Webhook URL": "Discord Webhook URL",
|
||||
wayToGetDiscordURL: "要获取,可以前往服务器设置 -> 整合 -> 创建 Webhook",
|
||||
"Bot Display Name": "机器人显示名称",
|
||||
"Prefix Custom Message": "自定义消息前缀",
|
||||
"Hello @everyone is...": "{'@'}everyone,……",
|
||||
teams: "Microsoft Teams",
|
||||
"Webhook URL": "Webhook URL",
|
||||
wayToGetTeamsURL: "您可以在 {0} 了解如何获取 Webhook URL。",
|
||||
signal: "Signal",
|
||||
Number: "号码",
|
||||
Recipients: "收件人",
|
||||
needSignalAPI: "您需要有一个支持 REST API 的 Signal 客户端。",
|
||||
wayToCheckSignalURL: "您可以通过下面的 URL 了解如何设置:",
|
||||
signalImportant: "重要:您不能混合设定收件人的分组和号码!",
|
||||
gotify: "Gotify",
|
||||
"Application Token": "Application Token",
|
||||
"Server URL": "服务器 URL",
|
||||
Priority: "优先级",
|
||||
slack: "Slack",
|
||||
"rocket.chat": "Rocket.chat",
|
||||
"Icon Emoji": "Emoji 图标",
|
||||
"Channel Name": "频道名称",
|
||||
"Uptime Kuma URL": "Uptime Kuma URL",
|
||||
aboutWebhooks: "关于 Webhook 的更多信息:{0}",
|
||||
aboutChannelName: "如果您想绕过 Webhook 频道,请在 {0} 字段输入所需的频道名称。例如:#other-channel",
|
||||
aboutKumaURL: "如果保留 Uptime Kuma URL 为空,将会默认指向项目的 GitHub 页面。",
|
||||
emojiCheatSheet: "Emoji 速查:{0}",
|
||||
"rocket.chat": "Rocket.Chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
clicksendsms: "ClickSend SMS",
|
||||
lunasea: "LunaSea",
|
||||
apprise: "Apprise (支持50+种通知服务)",
|
||||
apprise: "Apprise (支持 50+ 种通知服务)",
|
||||
GoogleChat: "Google Chat(仅 Google Workspace)",
|
||||
pushbullet: "Pushbullet",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
"Feishu WebHookUrl": "飞书 WebHook 地址",
|
||||
defaultNotificationName: "{notification} 通知({number})",
|
||||
here: "这里",
|
||||
Required: "必填",
|
||||
"Bot Token": "Bot Token",
|
||||
wayToGetTelegramToken: "你可以从 {0} 获取 Token。",
|
||||
"Chat ID": "Chat ID",
|
||||
supportTelegramChatID: "支持对话/群组/频道的 ID",
|
||||
wayToGetTelegramChatID: "你可以发送一条消息给你的机器人然后到下面的链接来查看你的 chat_id:",
|
||||
"YOUR BOT TOKEN HERE": "这里替换成你的 BOT TOKEN",
|
||||
chatIDNotFound: "没有找到 Chat ID,请先给你的机器人发送一条消息。",
|
||||
"Post URL": "目标链接",
|
||||
"Content Type": "Content Type",
|
||||
webhookJsonDesc: "{0} 适合现代的服务,比如 express.js",
|
||||
webhookFormDataDesc: "{multipart} 适合 PHP,解码使用 {decodeFunction}",
|
||||
secureOptionNone: "无 / STARTTLS(常见端口号为 25、587)",
|
||||
secureOptionTLS: "TLS(常见端口号为 465)",
|
||||
"Ignore TLS Error": "忽略 TLS 错误",
|
||||
"From Email": "发信人",
|
||||
"To Email": "收信人",
|
||||
smtpCC: "抄送",
|
||||
smtpBCC: "密送",
|
||||
"Discord Webhook URL": "Discord Webhook 链接",
|
||||
wayToGetDiscordURL: "获取方式:服务器设置 -> 整合 -> 创建 Webhook",
|
||||
"Bot Display Name": "机器人显示名称",
|
||||
"Prefix Custom Message": "自定义消息前缀",
|
||||
"Hello @everyone is...": "{'@'}所有人,……",
|
||||
"Webhook URL": "Webhook 链接",
|
||||
wayToGetTeamsURL: "你可以在 {0} 获取 Webhook 链接。",
|
||||
Number: "号码",
|
||||
Recipients: "收件人",
|
||||
needSignalAPI: "你需要有一个带 REST API 的 Signal 客户端。",
|
||||
wayToCheckSignalURL: "你可以通过下面的链接来了解如何设置:",
|
||||
signalImportant: "重要:你不能混合设定收件人的分组和号码!",
|
||||
"Application Token": "Application Token",
|
||||
"Server URL": "服务器链接",
|
||||
Priority: "优先级",
|
||||
"Icon Emoji": "Emoji 图标",
|
||||
"Channel Name": "频道名称",
|
||||
"Uptime Kuma URL": "Uptime Kuma 链接",
|
||||
aboutWebhooks: "关于 Webhook 的更多信息:{0}",
|
||||
aboutChannelName: "如果你想绕过 Webhook 设定的频道,请在设定 {0} 的频道名称字段为你想要的频道。例:#other-channel",
|
||||
aboutKumaURL: "如果保留 Uptime Kuma 链接为空,将会默认指向项目的 Github 页面。",
|
||||
emojiCheatSheet: "Emoji 参考表:{0}",
|
||||
"User Key": "User Key",
|
||||
Device: "设备",
|
||||
"Message Title": "消息标题",
|
||||
"Notification Sound": "通知铃声",
|
||||
"More info on:": "更多信息:{0}",
|
||||
pushoverDesc1: "紧急优先级(2)会在一小时内每30秒重试一次。",
|
||||
pushoverDesc2: "如果你想发送通知给不同的设备,请填写“设备”字段。",
|
||||
pushoverDesc1: "紧急优先级(2)会在一小时内每隔 30 秒重试一次。",
|
||||
pushoverDesc2: "如果您想发送通知给不同的设备,请填写“设备”字段。",
|
||||
"SMS Type": "短信类型",
|
||||
octopushTypePremium: "Premium(快 - 推荐用于警报)",
|
||||
octopushTypeLowCost: "Low Cost(慢 - 有时会被运营商屏蔽)",
|
||||
checkPrice: "查看 {0} 的价格:",
|
||||
apiCredentials: "API Credentials",
|
||||
octopushLegacyHint: "您是否在使用旧版本的 Octopush(2011-2020)?",
|
||||
"Check octopush prices": "查看 Octopush 的价格 {0}。",
|
||||
octopushPhoneNumber: "电话号码(国际格式,例:+33612345678)",
|
||||
octopushSMSSender: "短信发送名称:3-11位大小写字母、数字和空格(a-zA-Z0-9)",
|
||||
octopushPhoneNumber: "电话号码(国际格式,例如:+33612345678)",
|
||||
octopushSMSSender: "短信发送名称:3-11 位大小写字母、数字和空格(a-zA-Z0-9)",
|
||||
"LunaSea Device ID": "LunaSea 设备 ID",
|
||||
"Apprise URL": "Apprise 链接",
|
||||
"Example:": "例:{0}",
|
||||
"Apprise URL": "Apprise URL",
|
||||
"Example:": "例如:{0}",
|
||||
"Read more:": "了解更多:{0}",
|
||||
"Status:": "状态:{0}",
|
||||
"Read more": "了解更多",
|
||||
appriseInstalled: "Apprise 已安装",
|
||||
appriseNotInstalled: "Apprise 未安装。{0}",
|
||||
"Access Token": "Access Token",
|
||||
"Channel access token": "频道 access token",
|
||||
"Line Developers Console": "Line Developers Console",
|
||||
lineDevConsoleTo: "Line Developers Console - {0}",
|
||||
"Basic Settings": "Basic Settings",
|
||||
"User ID": "User ID",
|
||||
"Channel access token": "频道 Access Token",
|
||||
"Line Developers Console": "Line 开发者控制台",
|
||||
lineDevConsoleTo: "Line 开发者控制台 - {0}",
|
||||
"Basic Settings": "基本设置",
|
||||
"User ID": "用户 ID",
|
||||
"Messaging API": "Messaging API",
|
||||
wayToGetLineChannelToken: "首先访问 {0},创建一个提供者和频道(Messaging API),然后你就可以从上面提到的地方获取频道的 access token 和用户 ID。",
|
||||
"Icon URL": "图标链接",
|
||||
aboutIconURL: "你可以在“Icon URL”中提供一个图片地址来覆盖默认的资料图片。如果设置了 Emoji 图标此字段会被忽略。",
|
||||
aboutMattermostChannelName: "如果你想覆盖 Webhook 设定的频道,请在“频道名称”字段为你想要的频道。这需要在 Mattermost 的 Webhook 设定中启用。例:#other-channel",
|
||||
wayToGetLineChannelToken: "首先访问 {0},创建一个提供者和频道(Messaging API),然后您就可以从上面提到的菜单获取频道的 Access Token 和用户 ID。",
|
||||
"Icon URL": "图标 URL",
|
||||
aboutIconURL: "您可以在“图标 URL”中提供一个图片链接来覆盖默认的资料图片。如果设置了 Emoji 图标则此字段会被忽略。",
|
||||
aboutMattermostChannelName: "您可以覆盖 Webhook 发送消息的默认频道,只需在“频道名称”字段中输入您想要的频道名。这需要在 Mattermost 的 Webhook 设置中启用。例如:#other-channel",
|
||||
matrix: "Matrix",
|
||||
promosmsTypeEco: "SMS ECO - 便宜但是慢,并且容易超负荷。仅限波兰地区的收件人。",
|
||||
promosmsTypeFlash: "SMS FLASH - 消息会自动显示在收件人设备上。仅限波兰地区的收件人。",
|
||||
promosmsTypeFull: "SMS FULL - 高等级,你可以使用你自己的发件人名称(你需要先注册一个). 对于警报来说更可靠。",
|
||||
promosmsTypeSpeed: "SMS SPEED - 最高优先级。非常快速可靠,但更贵(越两倍 SMS FULL 等级的价格)。",
|
||||
promosmsPhoneNumber: "电话号码(波兰地区收件人可以不填区号)",
|
||||
promosmsSMSSender: "短信发件人名称:已注册的名称或以下默认值之一:InfoSMS,SMS Info,MaxSMS,INFO,SMS",
|
||||
checkPrice: "查看 {0} 的价格:",
|
||||
octopushLegacyHint: "你是否在使用旧版本的 Octopush(2011-2020)?",
|
||||
matrixHomeserverURL: "服务器链接(开头带 http(s):// 和可能的需要的端口号)",
|
||||
"Internal Room Id": "Internal Room Id",
|
||||
matrixDesc1: "你可以在 Matrix 客户端房间设置的高级选项找到 Internal Room Id。格式类似于 !QMdRCpUIfLwsfjxye6:home.server。",
|
||||
matrixDesc2: "请不要使用你自己的 Access Token,这将开放你所有的账户权限和你加入的房间权限。你可以创建一个新的用户并邀请它至你允许的的房间中。你可以运行以下命令来获取 Access Token:{0}",
|
||||
emailCustomSubject: "邮件主题",
|
||||
clicksendsms: "ClickSend SMS",
|
||||
GoogleChat: "Google Chat (Google Workspace only)",
|
||||
apiCredentials: "API credentials",
|
||||
promosmsTypeEco: "SMS ECO - 便宜但是慢,并且容易超负荷。仅限波兰地区的收信人。",
|
||||
promosmsTypeFlash: "SMS FLASH - 消息会自动显示在收信人设备上。仅限波兰地区的收信人。",
|
||||
promosmsTypeFull: "SMS FULL - 高级短信,您可以使用您自己的发信人名称(需要先注册)。对于警报来说更可靠。",
|
||||
promosmsTypeSpeed: "SMS SPEED - 最高优先级。非常快速可靠,但更贵(大约两倍 SMS FULL 的价格)。",
|
||||
promosmsPhoneNumber: "电话号码(波兰地区收信人可以不填区号)",
|
||||
promosmsSMSSender: "短信发信人名称:已注册的名称或以下默认值之一:InfoSMS、SMS Info、MaxSMS、INFO、SMS",
|
||||
"Feishu WebHookUrl": "飞书 WebHook URL",
|
||||
matrixHomeserverURL: "服务器 URL(包含 http(s):// 和可选的端口号)",
|
||||
"Internal Room Id": "内部房间 ID",
|
||||
matrixDesc1: "您可以在 Matrix 客户端房间设置的高级选项内找到内部房间 ID。格式类似于 !QMdRCpUIfLwsfjxye6:home.server。",
|
||||
matrixDesc2: "请不要使用您自己的 Access Token,这将开放您所有的账户权限和您已加入房间的权限。我们强烈建议您创建一个新用户并邀请它至您接收通知的房间中。您可以运行以下命令来获取 Access Token:{0}",
|
||||
Method: "方法",
|
||||
Body: "请求体",
|
||||
Headers: "请求头",
|
||||
PushUrl: "Push URL",
|
||||
PushUrl: "推送 URL",
|
||||
HeadersInvalidFormat: "请求头不是有效的 JSON: ",
|
||||
BodyInvalidFormat: "请求体不是有效的 JSON: ",
|
||||
"Monitor History": "监控历史数据",
|
||||
clearDataOlderThan: "保留监控历史数据 {0} 天",
|
||||
"Monitor History": "监控历史",
|
||||
clearDataOlderThan: "保留监控历史数据 {0} 天。",
|
||||
PasswordsDoNotMatch: "密码不匹配",
|
||||
records: "records",
|
||||
"One record": "One record",
|
||||
steamApiKeyDescription: "为了监控 Steam 游戏服务器,你需要一个 Steam Web-API key。你可以在这里注册你的 API 密钥: ",
|
||||
records: "记录",
|
||||
"One record": "一条记录",
|
||||
steamApiKeyDescription: "要监控 Steam 游戏服务器,您需要 Steam Web-API 密钥。您可以在这里注册您的 API 密钥: ",
|
||||
"Current User": "当前用户",
|
||||
recent: "最近",
|
||||
Done: "完成",
|
||||
Info: "信息",
|
||||
Security: "安全性",
|
||||
"Steam API Key": "Steam API Key",
|
||||
"Shrink Database": "缩小数据库",
|
||||
"Steam API Key": "Steam API 密钥",
|
||||
"Shrink Database": "压缩数据库",
|
||||
"Pick a RR-Type...": "选择资源记录类型...",
|
||||
"Pick Accepted Status Codes...": "选择有效的状态码...",
|
||||
Default: "默认",
|
||||
|
@ -324,45 +324,45 @@ export default {
|
|||
Title: "标题",
|
||||
Content: "内容",
|
||||
Style: "类型",
|
||||
info: "info",
|
||||
warning: "warning",
|
||||
danger: "danger",
|
||||
primary: "primary",
|
||||
light: "light",
|
||||
dark: "dark",
|
||||
info: "信息",
|
||||
warning: "警告",
|
||||
danger: "危险",
|
||||
primary: "主要",
|
||||
light: "明亮",
|
||||
dark: "黑暗",
|
||||
Post: "发布",
|
||||
"Please input title and content": "请输入标题和内容",
|
||||
Created: "创建于",
|
||||
"Last Updated": "最后更新",
|
||||
Unpin: "删除",
|
||||
Created: "创建时间",
|
||||
"Last Updated": "更新时间",
|
||||
Unpin: "取消钉选",
|
||||
"Switch to Light Theme": "切换到浅色主题",
|
||||
"Switch to Dark Theme": "切换到深色主题",
|
||||
"Show Tags": "显示标签",
|
||||
"Hide Tags": "隐藏标签",
|
||||
Description: "描述",
|
||||
"No monitors available.": "没有可用的监控项",
|
||||
"Add one": "创建新的监控项",
|
||||
"No monitors available.": "没有可用的监控项。",
|
||||
"Add one": "添加一个",
|
||||
"No Monitors": "没有监控项",
|
||||
"Untitled Group": "无标题的分组",
|
||||
"Untitled Group": "无标题分组",
|
||||
Services: "服务",
|
||||
Discard: "取消",
|
||||
Discard: "放弃",
|
||||
Cancel: "取消",
|
||||
"Powered by": "Powered by",
|
||||
shrinkDatabaseDescription: "这将触发 SQLite 数据库的 VACUUM 命令,如果您的数据库是在1.10.0版本之后创建的,AUTO_VACUUM 已经启用了,则不需要再使用此功能",
|
||||
shrinkDatabaseDescription: "触发 SQLite 数据库的 VACUUM 命令,如果您的数据库是在 1.10.0 版本之后创建的,则已启用 AUTO_VACUUM,不再需要此操作。",
|
||||
serwersms: "SerwerSMS.pl",
|
||||
serwersmsAPIUser: "API Username(请保留 webapi_ 前缀)",
|
||||
serwersmsAPIPassword: "API Password",
|
||||
serwersmsPhoneNumber: "Phone number",
|
||||
serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
|
||||
serwersmsAPIUser: "API 用户名(包括 webapi_ 前缀)",
|
||||
serwersmsAPIPassword: "API 密码",
|
||||
serwersmsPhoneNumber: "电话号码",
|
||||
serwersmsSenderName: "SMS 发信人名称(需要在客户中心注册)",
|
||||
stackfield: "Stackfield",
|
||||
smtpDkimSettings: "DKIM 设置",
|
||||
smtpDkimDesc: "请参考 Nodemailer 的 DKIM {0} 进行配置。",
|
||||
smtpDkimDesc: "请访问 Nodemailer DKIM {0} 了解配置方法。",
|
||||
documentation: "文档",
|
||||
smtpDkimDomain: "域名",
|
||||
smtpDkimKeySelector: "前缀选择器",
|
||||
smtpDkimPrivateKey: "密钥",
|
||||
smtpDkimHashAlgo: "哈希算法(可选)",
|
||||
smtpDkimheaderFieldNames: "明确包含在哈希计算对象内的 Header 列表(可选)",
|
||||
smtpDkimheaderFieldNames: "包含在哈希计算对象内的 Header 列表(可选)",
|
||||
smtpDkimskipFields: "不包含在哈希计算对象内的 Header 列表(可选)",
|
||||
Feishu: "飞书",
|
||||
AliyunSMS: "阿里云短信服务",
|
||||
|
@ -372,5 +372,5 @@ export default {
|
|||
SecretKey: "钉钉自定义机器人加签密钥",
|
||||
"For safety, must use secret key": "出于安全考虑,必须使用加签密钥",
|
||||
WeCom: "企业微信群机器人",
|
||||
"WeCom Bot Key": "企业微信群机器人 key",
|
||||
"WeCom Bot Key": "企业微信群机器人 Key",
|
||||
};
|
||||
|
|
|
@ -96,7 +96,7 @@ export default {
|
|||
Test: "測試",
|
||||
keywordDescription: "搜索 HTML 或 JSON 裡是否有出現關鍵字(注意英文大細階)",
|
||||
"Certificate Info": "憑證詳細資料",
|
||||
deleteMonitorMsg: "是否確定刪除這個監測器",
|
||||
deleteMonitorMsg: "是否確定刪除這個監測器?",
|
||||
deleteNotificationMsg: "是否確定刪除這個通知設定?如監測器啟用了這個通知,將會收不到通知。",
|
||||
"Resolver Server": "DNS 伺服器",
|
||||
"Resource Record Type": "DNS 記錄類型",
|
||||
|
@ -180,6 +180,7 @@ export default {
|
|||
"Edit Status Page": "編輯 Status Page",
|
||||
"Go to Dashboard": "前往主控台",
|
||||
"Status Page": "Status Page",
|
||||
"Status Pages": "Status Pages",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "電郵 (SMTP)",
|
||||
|
@ -198,4 +199,5 @@ export default {
|
|||
pushbullet: "Pushbullet",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
deleteStatusPageMsg: "是否確定刪除這個 Status Page?",
|
||||
};
|
||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
|||
"Edit Status Page": "編輯狀態頁",
|
||||
"Go to Dashboard": "前往儀表板",
|
||||
"Status Page": "狀態頁",
|
||||
"Status Pages": "狀態頁",
|
||||
defaultNotificationName: "我的 {notification} 通知 ({number})",
|
||||
here: "此處",
|
||||
Required: "必填",
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
|
||||
<div class="container-fluid">
|
||||
{{ $root.connectionErrorMsg }}
|
||||
<div v-if="$root.showReverseProxyGuide">
|
||||
Using a Reverse Proxy? <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">Check how to config it for WebSocket</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -18,10 +21,10 @@
|
|||
</a>
|
||||
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item me-2">
|
||||
<a href="/status" class="nav-link status-page">
|
||||
<font-awesome-icon icon="stream" /> {{ $t("Status Page") }}
|
||||
</a>
|
||||
<li v-if="$root.loggedIn" class="nav-item me-2">
|
||||
<router-link to="/manage-status-page" class="nav-link">
|
||||
<font-awesome-icon icon="stream" /> {{ $t("Status Pages") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="$root.loggedIn" class="nav-item me-2">
|
||||
<router-link to="/dashboard" class="nav-link">
|
||||
|
@ -45,7 +48,7 @@
|
|||
</header>
|
||||
|
||||
<main>
|
||||
<router-view v-if="$root.loggedIn" />
|
||||
<router-view v-if="$root.loggedIn || forceShowContent" />
|
||||
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
||||
</main>
|
||||
|
||||
|
@ -157,7 +160,7 @@ export default {
|
|||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
|
||||
&.router-link-exact-active {
|
||||
&.router-link-exact-active, &.active {
|
||||
color: $primary;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
@ -184,6 +187,8 @@ main {
|
|||
padding: 5px;
|
||||
background-color: crimson;
|
||||
color: white;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
import { io } from "socket.io-client";
|
||||
import { useToast } from "vue-toastification";
|
||||
import jwt_decode from "jwt-decode";
|
||||
import Favico from "favico.js";
|
||||
const toast = useToast();
|
||||
|
||||
let socket;
|
||||
|
||||
const noSocketIOPages = [
|
||||
"/status-page",
|
||||
"/status",
|
||||
"/"
|
||||
/^\/status-page$/, // /status-page
|
||||
/^\/status/, // /status**
|
||||
/^\/$/ // /
|
||||
];
|
||||
|
||||
const favicon = new Favico({
|
||||
animation: "none"
|
||||
});
|
||||
|
||||
export default {
|
||||
|
||||
data() {
|
||||
|
@ -33,7 +38,10 @@ export default {
|
|||
uptimeList: { },
|
||||
tlsInfoList: {},
|
||||
notificationList: [],
|
||||
statusPageListLoaded: false,
|
||||
statusPageList: [],
|
||||
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
||||
showReverseProxyGuide: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -51,8 +59,12 @@ export default {
|
|||
}
|
||||
|
||||
// No need to connect to the socket.io for status page
|
||||
if (! bypass && noSocketIOPages.includes(location.pathname)) {
|
||||
return;
|
||||
if (! bypass && location.pathname) {
|
||||
for (let page of noSocketIOPages) {
|
||||
if (location.pathname.match(page)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.socket.initedSocketIO = true;
|
||||
|
@ -103,6 +115,11 @@ export default {
|
|||
this.notificationList = data;
|
||||
});
|
||||
|
||||
socket.on("statusPageList", (data) => {
|
||||
this.statusPageListLoaded = true;
|
||||
this.statusPageList = data;
|
||||
});
|
||||
|
||||
socket.on("heartbeat", (data) => {
|
||||
if (! (data.monitorID in this.heartbeatList)) {
|
||||
this.heartbeatList[data.monitorID] = [];
|
||||
|
@ -169,6 +186,7 @@ export default {
|
|||
socket.on("connect_error", (err) => {
|
||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||
this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`;
|
||||
this.showReverseProxyGuide = true;
|
||||
this.socket.connected = false;
|
||||
this.socket.firstConnect = false;
|
||||
});
|
||||
|
@ -183,6 +201,7 @@ export default {
|
|||
console.log("Connected to the socket server");
|
||||
this.socket.connectCount++;
|
||||
this.socket.connected = true;
|
||||
this.showReverseProxyGuide = false;
|
||||
|
||||
// Reset Heartbeat list if it is re-connect
|
||||
if (this.socket.connectCount >= 2) {
|
||||
|
@ -239,6 +258,14 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
toastSuccess(msg) {
|
||||
toast.success(msg);
|
||||
},
|
||||
|
||||
toastError(msg) {
|
||||
toast.error(msg);
|
||||
},
|
||||
|
||||
login(username, password, token, callback) {
|
||||
socket.emit("login", {
|
||||
username,
|
||||
|
@ -392,10 +419,49 @@ export default {
|
|||
|
||||
return result;
|
||||
},
|
||||
|
||||
stats() {
|
||||
let result = {
|
||||
up: 0,
|
||||
down: 0,
|
||||
unknown: 0,
|
||||
pause: 0,
|
||||
};
|
||||
|
||||
for (let monitorID in this.$root.monitorList) {
|
||||
let beat = this.$root.lastHeartbeatList[monitorID];
|
||||
let monitor = this.$root.monitorList[monitorID];
|
||||
|
||||
if (monitor && ! monitor.active) {
|
||||
result.pause++;
|
||||
} else if (beat) {
|
||||
if (beat.status === 1) {
|
||||
result.up++;
|
||||
} else if (beat.status === 0) {
|
||||
result.down++;
|
||||
} else if (beat.status === 2) {
|
||||
result.up++;
|
||||
} else {
|
||||
result.unknown++;
|
||||
}
|
||||
} else {
|
||||
result.unknown++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
// Update Badge
|
||||
"stats.down"(to, from) {
|
||||
if (to !== from) {
|
||||
favicon.badge(to);
|
||||
}
|
||||
},
|
||||
|
||||
// Reload the SPA if the server version is changed.
|
||||
"info.version"(to, from) {
|
||||
if (from && from !== to) {
|
||||
|
@ -409,9 +475,15 @@ export default {
|
|||
|
||||
// Reconnect the socket io, if status-page to dashboard
|
||||
"$route.fullPath"(newValue, oldValue) {
|
||||
if (noSocketIOPages.includes(newValue)) {
|
||||
return;
|
||||
|
||||
if (newValue) {
|
||||
for (let page of noSocketIOPages) {
|
||||
if (newValue.match(page)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.initSocketIO();
|
||||
},
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ export default {
|
|||
return "light";
|
||||
}
|
||||
|
||||
if (this.path === "/status-page" || this.path === "/status") {
|
||||
if (this.path.startsWith("/status-page") || this.path.startsWith("/status")) {
|
||||
return this.statusPageTheme;
|
||||
} else {
|
||||
if (this.userTheme === "auto") {
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 class="mb-3">
|
||||
{{ $t("Add New Status Page") }}
|
||||
</h1>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div class="shadow-box">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">{{ $t("Name") }}</label>
|
||||
<input id="name" v-model="title" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="slug" class="form-label">{{ $t("Slug") }}</label>
|
||||
<div class="input-group">
|
||||
<span id="basic-addon3" class="input-group-text">/status/</span>
|
||||
<input id="slug" v-model="slug" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
<ul>
|
||||
<li>{{ $t("Accept characters:") }} <mark>a-z</mark> <mark>0-9</mark> <mark>-</mark></li>
|
||||
<li>{{ $t("Start or end with") }} <mark>a-z</mark> <mark>0-9</mark> only</li>
|
||||
<li>{{ $t("No consecutive dashes") }} <mark>--</mark></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 mb-1">
|
||||
<button id="monitor-submit-btn" class="btn btn-primary w-100" type="submit" :disabled="processing">{{ $t("Next") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: "",
|
||||
slug: "",
|
||||
processing: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("addStatusPage", this.title, this.slug, (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
location.href = "/status/" + this.slug + "?edit";
|
||||
} else {
|
||||
|
||||
if (res.msg.includes("UNIQUE constraint")) {
|
||||
this.$root.toastError(this.$t("The slug is already taken. Please choose another slug."));
|
||||
} else {
|
||||
this.$root.toastRes(res);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
|
@ -9,19 +9,19 @@
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>{{ $t("Up") }}</h3>
|
||||
<span class="num">{{ stats.up }}</span>
|
||||
<span class="num">{{ $root.stats.up }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("Down") }}</h3>
|
||||
<span class="num text-danger">{{ stats.down }}</span>
|
||||
<span class="num text-danger">{{ $root.stats.down }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("Unknown") }}</h3>
|
||||
<span class="num text-secondary">{{ stats.unknown }}</span>
|
||||
<span class="num text-secondary">{{ $root.stats.unknown }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("pauseDashboardHome") }}</h3>
|
||||
<span class="num text-secondary">{{ stats.pause }}</span>
|
||||
<span class="num text-secondary">{{ $root.stats.pause }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -89,37 +89,6 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
stats() {
|
||||
let result = {
|
||||
up: 0,
|
||||
down: 0,
|
||||
unknown: 0,
|
||||
pause: 0,
|
||||
};
|
||||
|
||||
for (let monitorID in this.$root.monitorList) {
|
||||
let beat = this.$root.lastHeartbeatList[monitorID];
|
||||
let monitor = this.$root.monitorList[monitorID];
|
||||
|
||||
if (monitor && ! monitor.active) {
|
||||
result.pause++;
|
||||
} else if (beat) {
|
||||
if (beat.status === 1) {
|
||||
result.up++;
|
||||
} else if (beat.status === 0) {
|
||||
result.down++;
|
||||
} else if (beat.status === 2) {
|
||||
result.up++;
|
||||
} else {
|
||||
result.unknown++;
|
||||
}
|
||||
} else {
|
||||
result.unknown++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
importantHeartBeatList() {
|
||||
let result = [];
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 class="mb-3">
|
||||
{{ $t("Status Pages") }}
|
||||
</h1>
|
||||
|
||||
<div>
|
||||
<router-link to="/add-status-page" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("New Status Page") }}</router-link>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box">
|
||||
<template v-if="$root.statusPageListLoaded">
|
||||
<span v-if="Object.keys($root.statusPageList).length === 0" class="d-flex align-items-center justify-content-center my-3">
|
||||
No status pages
|
||||
</span>
|
||||
|
||||
<!-- use <a> instead of <router-link>, because the heartbeat won't load. -->
|
||||
<a v-for="statusPage in $root.statusPageList" :key="statusPage.slug" :href="'/status/' + statusPage.slug" class="item">
|
||||
<img :src="icon(statusPage.icon)" alt class="logo me-2" />
|
||||
<div class="info">
|
||||
<div class="title">{{ statusPage.title }}</div>
|
||||
<div class="slug">/status/{{ statusPage.slug }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
<div v-else class="d-flex align-items-center justify-content-center my-3 spinner">
|
||||
<font-awesome-icon icon="spinner" size="2x" spin />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { getResBaseURL } from "../util-frontend";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
icon(icon) {
|
||||
if (icon === "/icon.svg") {
|
||||
return icon;
|
||||
} else {
|
||||
return getResBaseURL() + icon;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
transition: all ease-in-out 0.15s;
|
||||
padding: 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #cdf8f4;
|
||||
}
|
||||
|
||||
$logo-width: 70px;
|
||||
|
||||
.logo {
|
||||
width: $logo-width;
|
||||
|
||||
// Better when the image is loading
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
.info {
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.slug {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- Desktop header -->
|
||||
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
|
||||
<router-link to="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
|
||||
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" />
|
||||
<span class="fs-4 title">Uptime Kuma</span>
|
||||
</router-link>
|
||||
</header>
|
||||
|
||||
<!-- Mobile header -->
|
||||
<header v-else class="d-flex flex-wrap justify-content-center pt-2 pb-2 mb-3">
|
||||
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
|
||||
<object class="bi" width="40" height="40" data="/icon.svg" />
|
||||
<span class="fs-4 title ms-2">Uptime Kuma</span>
|
||||
</router-link>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div>
|
||||
<strong>🐻 {{ $t("Page Not Found") }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="guide">
|
||||
Most likely causes:
|
||||
<ul>
|
||||
<li>The resource is no longer available.</li>
|
||||
<li>There might be a typing error in the address.</li>
|
||||
</ul>
|
||||
|
||||
What you can try:<br />
|
||||
<ul>
|
||||
<li>Retype the address.</li>
|
||||
<li><a href="#" class="go-back" @click="goBack()">Go back to the previous page.</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.go-back {
|
||||
text-decoration: none;
|
||||
color: $primary !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 50px;
|
||||
padding-top: 30px;
|
||||
|
||||
strong {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.guide {
|
||||
max-width: 800px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dark {
|
||||
header {
|
||||
background-color: $dark-header-bg;
|
||||
border-bottom-color: $dark-header-bg !important;
|
||||
|
||||
span {
|
||||
color: #f0f6fc;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-nav {
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="settings-menu">
|
||||
<div v-if="showSubMenu" class="settings-menu col-lg-3 col-md-5">
|
||||
<router-link
|
||||
v-for="(item, key) in subMenus"
|
||||
:key="key"
|
||||
|
@ -17,8 +17,8 @@
|
|||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="settings-content">
|
||||
<div class="settings-content-header">
|
||||
<div class="settings-content col-lg-9 col-md-7">
|
||||
<div v-if="currentPage" class="settings-content-header">
|
||||
{{ subMenus[currentPage].title }}
|
||||
</div>
|
||||
<div class="mx-3">
|
||||
|
@ -41,7 +41,6 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
show: true,
|
||||
|
||||
settings: {},
|
||||
settingsLoaded: false,
|
||||
};
|
||||
|
@ -52,11 +51,19 @@ export default {
|
|||
let pathSplit = useRoute().path.split("/");
|
||||
let pathEnd = pathSplit[pathSplit.length - 1];
|
||||
if (!pathEnd || pathEnd === "settings") {
|
||||
return "general";
|
||||
return null;
|
||||
}
|
||||
return pathEnd;
|
||||
},
|
||||
|
||||
showSubMenu() {
|
||||
if (this.$root.isMobile) {
|
||||
return !this.currentPage;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
subMenus() {
|
||||
return {
|
||||
general: {
|
||||
|
@ -84,11 +91,26 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
"$root.isMobile"() {
|
||||
this.loadGeneralPage();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadSettings();
|
||||
this.loadGeneralPage();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
// For desktop only, mobile do nothing
|
||||
loadGeneralPage() {
|
||||
if (!this.currentPage && !this.$root.isMobile) {
|
||||
this.$router.push("/settings/general");
|
||||
}
|
||||
},
|
||||
|
||||
loadSettings() {
|
||||
this.$root.getSocket().emit("getSettings", (res) => {
|
||||
this.settings = res.data;
|
||||
|
@ -115,7 +137,7 @@ export default {
|
|||
this.loadSettings();
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -136,9 +158,6 @@ footer {
|
|||
}
|
||||
|
||||
.settings-menu {
|
||||
flex: 0 0 auto;
|
||||
width: 300px;
|
||||
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
@ -148,6 +167,8 @@ footer {
|
|||
margin: 0.5em;
|
||||
padding: 0.7em 1em;
|
||||
cursor: pointer;
|
||||
border-left-width: 0;
|
||||
transition: all ease-in-out 0.1s;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
|
@ -171,9 +192,6 @@ footer {
|
|||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% - 300px);
|
||||
|
||||
.settings-content-header {
|
||||
width: calc(100% + 20px);
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
|
@ -187,6 +205,14 @@ footer {
|
|||
background: $dark-header-bg;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.mobile & {
|
||||
padding: 15px 0 0 0;
|
||||
|
||||
.dark & {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,45 +1,54 @@
|
|||
<template>
|
||||
<div v-if="loadedTheme" class="container mt-3">
|
||||
<!-- Logo & Title -->
|
||||
<h1 class="mb-4">
|
||||
<!-- Logo -->
|
||||
<span class="logo-wrapper" @click="showImageCropUploadMethod">
|
||||
<img :src="logoURL" alt class="logo me-2" :class="logoClass" />
|
||||
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
|
||||
</span>
|
||||
|
||||
<!-- Uploader -->
|
||||
<!-- url="/api/status-page/upload-logo" -->
|
||||
<ImageCropUpload v-model="showImageCropUpload"
|
||||
field="img"
|
||||
:width="128"
|
||||
:height="128"
|
||||
:langType="$i18n.locale"
|
||||
img-format="png"
|
||||
:noCircle="true"
|
||||
:noSquare="false"
|
||||
@crop-success="cropSuccess"
|
||||
/>
|
||||
|
||||
<!-- Title -->
|
||||
<Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" />
|
||||
</h1>
|
||||
|
||||
<!-- Admin functions -->
|
||||
<div v-if="hasToken" class="mb-4">
|
||||
<div v-if="!enableEditMode">
|
||||
<button class="btn btn-info me-2" @click="edit">
|
||||
<font-awesome-icon icon="edit" />
|
||||
{{ $t("Edit Status Page") }}
|
||||
</button>
|
||||
|
||||
<a href="/dashboard" class="btn btn-info">
|
||||
<font-awesome-icon icon="tachometer-alt" />
|
||||
{{ $t("Go to Dashboard") }}
|
||||
</a>
|
||||
<!-- Sidebar for edit mode -->
|
||||
<div v-if="enableEditMode" class="sidebar">
|
||||
<div class="my-3">
|
||||
<label for="slug" class="form-label">{{ $t("Slug") }}</label>
|
||||
<div class="input-group">
|
||||
<span id="basic-addon3" class="input-group-text">/status/</span>
|
||||
<input id="slug" v-model="config.slug" type="text" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="my-3">
|
||||
<label for="title" class="form-label">{{ $t("Title") }}</label>
|
||||
<input id="title" v-model="config.title" type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="description" class="form-label">{{ $t("Description") }}</label>
|
||||
<textarea id="description" v-model="config.description" class="form-control"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="my-3 form-check form-switch">
|
||||
<input id="switch-theme" v-model="config.theme" class="form-check-input" type="checkbox" true-value="dark" false-value="light">
|
||||
<label class="form-check-label" for="switch-theme">{{ $t("Switch to Dark Theme") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="my-3 form-check form-switch">
|
||||
<input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label>
|
||||
</div>
|
||||
|
||||
<div v-if="false" class="my-3">
|
||||
<label for="password" class="form-label">{{ $t("Password") }} <sup>Coming Soon</sup></label>
|
||||
<input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if="false" class="my-3">
|
||||
<label for="cname" class="form-label">Domain Names <sup>Coming Soon</sup></label>
|
||||
<textarea id="cname" v-model="config.domanNames" rows="3" disabled class="form-control" :placeholder="domainNamesPlaceholder"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="danger-zone">
|
||||
<button class="btn btn-danger me-2" @click="deleteDialog">
|
||||
<font-awesome-icon icon="trash" />
|
||||
{{ $t("Delete") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Footer -->
|
||||
<div class="sidebar-footer">
|
||||
<button class="btn btn-success me-2" @click="save">
|
||||
<font-awesome-icon icon="save" />
|
||||
{{ $t("Save") }}
|
||||
|
@ -49,167 +58,182 @@
|
|||
<font-awesome-icon icon="save" />
|
||||
{{ $t("Discard") }}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary btn-add-group me-2" @click="createIncident">
|
||||
<font-awesome-icon icon="bullhorn" />
|
||||
{{ $t("Create Incident") }}
|
||||
</button>
|
||||
|
||||
<!--
|
||||
<button v-if="isPublished" class="btn btn-light me-2" @click="">
|
||||
<font-awesome-icon icon="save" />
|
||||
{{ $t("Unpublish") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!isPublished" class="btn btn-info me-2" @click="">
|
||||
<font-awesome-icon icon="save" />
|
||||
{{ $t("Publish") }}
|
||||
</button>-->
|
||||
|
||||
<!-- Set Default Language -->
|
||||
<!-- Set theme -->
|
||||
<button v-if="theme == 'dark'" class="btn btn-light me-2" @click="changeTheme('light')">
|
||||
<font-awesome-icon icon="save" />
|
||||
{{ $t("Switch to Light Theme") }}
|
||||
</button>
|
||||
|
||||
<button v-if="theme == 'light'" class="btn btn-dark me-2" @click="changeTheme('dark')">
|
||||
<font-awesome-icon icon="save" />
|
||||
{{ $t("Switch to Dark Theme") }}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary me-2" @click="changeTagsVisibilty(!tagsVisible)">
|
||||
<template v-if="tagsVisible">
|
||||
<font-awesome-icon icon="eye-slash" />
|
||||
{{ $t("Hide Tags") }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<font-awesome-icon icon="eye" />
|
||||
{{ $t("Show Tags") }}
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Incident -->
|
||||
<div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass">
|
||||
<strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
|
||||
<Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" />
|
||||
|
||||
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
|
||||
<Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
|
||||
|
||||
<!-- Incident Date -->
|
||||
<div class="date mt-3">
|
||||
{{ $t("Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br />
|
||||
<span v-if="incident.lastUpdatedDate">
|
||||
{{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }})
|
||||
<!-- Main Status Page -->
|
||||
<div :class="{ edit: enableEditMode}" class="main">
|
||||
<!-- Logo & Title -->
|
||||
<h1 class="mb-4 title-flex">
|
||||
<!-- Logo -->
|
||||
<span class="logo-wrapper" @click="showImageCropUploadMethod">
|
||||
<img :src="logoURL" alt class="logo me-2" :class="logoClass" />
|
||||
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="editMode" class="mt-3">
|
||||
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident">
|
||||
<font-awesome-icon icon="bullhorn" />
|
||||
{{ $t("Post") }}
|
||||
</button>
|
||||
<!-- Uploader -->
|
||||
<!-- url="/api/status-page/upload-logo" -->
|
||||
<ImageCropUpload v-model="showImageCropUpload"
|
||||
field="img"
|
||||
:width="128"
|
||||
:height="128"
|
||||
:langType="$i18n.locale"
|
||||
img-format="png"
|
||||
:noCircle="true"
|
||||
:noSquare="false"
|
||||
@crop-success="cropSuccess"
|
||||
/>
|
||||
|
||||
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident">
|
||||
<font-awesome-icon icon="edit" />
|
||||
{{ $t("Edit") }}
|
||||
</button>
|
||||
<!-- Title -->
|
||||
<Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" />
|
||||
</h1>
|
||||
|
||||
<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) }}
|
||||
<!-- 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>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
|
||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a></li>
|
||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning") }}</a></li>
|
||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger") }}</a></li>
|
||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary") }}</a></li>
|
||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light") }}</a></li>
|
||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="unpinIncident">
|
||||
<font-awesome-icon icon="unlink" />
|
||||
{{ $t("Unpin") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overall Status -->
|
||||
<div class="shadow-box list p-4 overall-status mb-4">
|
||||
<div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData">
|
||||
<font-awesome-icon icon="question-circle" class="ok" />
|
||||
{{ $t("No Services") }}
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="allUp">
|
||||
<font-awesome-icon icon="check-circle" class="ok" />
|
||||
{{ $t("All Systems Operational") }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="partialDown">
|
||||
<font-awesome-icon icon="exclamation-circle" class="warning" />
|
||||
{{ $t("Partially Degraded Service") }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="allDown">
|
||||
<font-awesome-icon icon="times-circle" class="danger" />
|
||||
{{ $t("Degraded Service") }}
|
||||
<a href="/manage-status-page" class="btn btn-info">
|
||||
<font-awesome-icon icon="tachometer-alt" />
|
||||
{{ $t("Go to Dashboard") }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<font-awesome-icon icon="question-circle" style="color: #efefef;" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<strong v-if="editMode">{{ $t("Description") }}:</strong>
|
||||
<Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" />
|
||||
|
||||
<div v-if="editMode" class="mb-4">
|
||||
<div>
|
||||
<button class="btn btn-primary btn-add-group me-2" @click="addGroup">
|
||||
<font-awesome-icon icon="plus" />
|
||||
{{ $t("Add Group") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div 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>
|
||||
<button class="btn btn-primary btn-add-group me-2" @click="createIncident">
|
||||
<font-awesome-icon icon="bullhorn" />
|
||||
{{ $t("Create Incident") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center">
|
||||
<!-- 👀 Nothing here, please add a group or a monitor. -->
|
||||
👀 {{ $t("statusPageNothing") }}
|
||||
<!-- Incident -->
|
||||
<div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass">
|
||||
<strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
|
||||
<Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" />
|
||||
|
||||
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
|
||||
<Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
|
||||
|
||||
<!-- Incident Date -->
|
||||
<div class="date mt-3">
|
||||
{{ $t("Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br />
|
||||
<span v-if="incident.lastUpdatedDate">
|
||||
{{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="editMode" class="mt-3">
|
||||
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident">
|
||||
<font-awesome-icon icon="bullhorn" />
|
||||
{{ $t("Post") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident">
|
||||
<font-awesome-icon icon="edit" />
|
||||
{{ $t("Edit") }}
|
||||
</button>
|
||||
|
||||
<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>
|
||||
|
||||
<PublicGroupList :edit-mode="enableEditMode" />
|
||||
<!-- Overall Status -->
|
||||
<div class="shadow-box list p-4 overall-status mb-4">
|
||||
<div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData">
|
||||
<font-awesome-icon icon="question-circle" class="ok" />
|
||||
{{ $t("No Services") }}
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="allUp">
|
||||
<font-awesome-icon icon="check-circle" class="ok" />
|
||||
{{ $t("All Systems Operational") }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="partialDown">
|
||||
<font-awesome-icon icon="exclamation-circle" class="warning" />
|
||||
{{ $t("Partially Degraded Service") }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="allDown">
|
||||
<font-awesome-icon icon="times-circle" class="danger" />
|
||||
{{ $t("Degraded Service") }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<font-awesome-icon icon="question-circle" style="color: #efefef;" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<strong v-if="editMode">{{ $t("Description") }}:</strong>
|
||||
<Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" />
|
||||
|
||||
<div v-if="editMode" class="mb-4">
|
||||
<div>
|
||||
<button class="btn btn-primary btn-add-group me-2" @click="addGroup">
|
||||
<font-awesome-icon icon="plus" />
|
||||
{{ $t("Add Group") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div 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 class="mb-4">
|
||||
<div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center">
|
||||
<!-- 👀 Nothing here, please add a group or a monitor. -->
|
||||
👀 {{ $t("statusPageNothing") }}
|
||||
</div>
|
||||
|
||||
<PublicGroupList :edit-mode="enableEditMode" :show-tags="config.showTags" />
|
||||
</div>
|
||||
|
||||
<footer class="mt-5 mb-4">
|
||||
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<footer class="mt-5 mb-4">
|
||||
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
|
||||
</footer>
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteStatusPage">
|
||||
{{ $t("deleteStatusPageMsg") }}
|
||||
</Confirm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -220,16 +244,25 @@ import ImageCropUpload from "vue-image-crop-upload";
|
|||
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts";
|
||||
import { useToast } from "vue-toastification";
|
||||
import dayjs from "dayjs";
|
||||
import Favico from "favico.js";
|
||||
import { getResBaseURL } from "../util-frontend";
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const leavePageMsg = "Do you really want to leave? you have unsaved changes!";
|
||||
|
||||
let feedInterval;
|
||||
|
||||
const favicon = new Favico({
|
||||
animation: "none"
|
||||
});
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PublicGroupList,
|
||||
ImageCropUpload
|
||||
ImageCropUpload,
|
||||
Confirm,
|
||||
},
|
||||
|
||||
// Leave Page for vue route change
|
||||
|
@ -247,6 +280,7 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
slug: null,
|
||||
enableEditMode: false,
|
||||
enableEditIncidentMode: false,
|
||||
hasToken: false,
|
||||
|
@ -259,6 +293,8 @@ export default {
|
|||
loadedTheme: false,
|
||||
loadedData: false,
|
||||
baseURL: "",
|
||||
clickedEditButton: false,
|
||||
domainNamesPlaceholder: "domain1.com\ndomain2.com\n..."
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -296,15 +332,7 @@ export default {
|
|||
},
|
||||
|
||||
isPublished() {
|
||||
return this.config.statusPagePublished;
|
||||
},
|
||||
|
||||
theme() {
|
||||
return this.config.statusPageTheme;
|
||||
},
|
||||
|
||||
tagsVisible() {
|
||||
return this.config.statusPageTags
|
||||
return this.config.published;
|
||||
},
|
||||
|
||||
logoClass() {
|
||||
|
@ -378,13 +406,28 @@ export default {
|
|||
},
|
||||
|
||||
// Set Theme
|
||||
"config.statusPageTheme"() {
|
||||
this.$root.statusPageTheme = this.config.statusPageTheme;
|
||||
"config.theme"() {
|
||||
this.$root.statusPageTheme = this.config.theme;
|
||||
this.loadedTheme = true;
|
||||
},
|
||||
|
||||
"config.title"(title) {
|
||||
document.title = title;
|
||||
},
|
||||
|
||||
"$root.monitorList"() {
|
||||
let count = Object.keys(this.$root.monitorList).length;
|
||||
|
||||
// Since publicGroupList is getting from public rest api, monitors' tags may not present if showTags = false
|
||||
if (count > 0) {
|
||||
for (let group of this.$root.publicGroupList) {
|
||||
for (let monitor of group.monitorList) {
|
||||
if (monitor.tags === undefined && this.$root.monitorList[monitor.id]) {
|
||||
monitor.tags = this.$root.monitorList[monitor.id].tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
@ -403,28 +446,24 @@ export default {
|
|||
});
|
||||
|
||||
// Special handle for dev
|
||||
const env = process.env.NODE_ENV;
|
||||
if (env === "development" || localStorage.dev === "dev") {
|
||||
this.baseURL = location.protocol + "//" + location.hostname + ":3001";
|
||||
}
|
||||
this.baseURL = getResBaseURL();
|
||||
},
|
||||
async mounted() {
|
||||
axios.get("/api/status-page/config").then((res) => {
|
||||
this.config = res.data;
|
||||
this.slug = this.$route.params.slug;
|
||||
|
||||
if (this.config.logo) {
|
||||
this.imgDataUrl = this.config.logo;
|
||||
if (!this.slug) {
|
||||
this.slug = "default";
|
||||
}
|
||||
|
||||
axios.get("/api/status-page/" + this.slug).then((res) => {
|
||||
this.config = res.data.config;
|
||||
|
||||
if (this.config.icon) {
|
||||
this.imgDataUrl = this.config.icon;
|
||||
}
|
||||
});
|
||||
|
||||
axios.get("/api/status-page/incident").then((res) => {
|
||||
if (res.data.ok) {
|
||||
this.incident = res.data.incident;
|
||||
}
|
||||
});
|
||||
|
||||
axios.get("/api/status-page/monitor-list").then((res) => {
|
||||
this.$root.publicGroupList = res.data;
|
||||
this.incident = res.data.incident;
|
||||
this.$root.publicGroupList = res.data.publicGroupList;
|
||||
});
|
||||
|
||||
// 5mins a loop
|
||||
|
@ -432,31 +471,87 @@ export default {
|
|||
feedInterval = setInterval(() => {
|
||||
this.updateHeartbeatList();
|
||||
}, (300 + 10) * 1000);
|
||||
|
||||
// Go to edit page if ?edit present
|
||||
// null means ?edit present, but no value
|
||||
if (this.$route.query.edit || this.$route.query.edit === null) {
|
||||
this.edit();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
updateHeartbeatList() {
|
||||
// If editMode, it will use the data from websocket.
|
||||
if (! this.editMode) {
|
||||
axios.get("/api/status-page/heartbeat").then((res) => {
|
||||
this.$root.heartbeatList = res.data.heartbeatList;
|
||||
this.$root.uptimeList = res.data.uptimeList;
|
||||
axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => {
|
||||
const { heartbeatList, uptimeList } = res.data;
|
||||
|
||||
this.$root.heartbeatList = heartbeatList;
|
||||
this.$root.uptimeList = uptimeList;
|
||||
|
||||
const heartbeatIds = Object.keys(heartbeatList);
|
||||
const downMonitors = heartbeatIds.reduce((downMonitorsAmount, currentId) => {
|
||||
const monitorHeartbeats = heartbeatList[currentId];
|
||||
const lastHeartbeat = monitorHeartbeats.at(-1);
|
||||
|
||||
if (lastHeartbeat) {
|
||||
return lastHeartbeat.status === 0 ? downMonitorsAmount + 1 : downMonitorsAmount;
|
||||
} else {
|
||||
return downMonitorsAmount;
|
||||
}
|
||||
}, 0);
|
||||
|
||||
favicon.badge(downMonitors);
|
||||
|
||||
this.loadedData = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
edit() {
|
||||
this.$root.initSocketIO(true);
|
||||
this.enableEditMode = true;
|
||||
if (this.hasToken) {
|
||||
this.$root.initSocketIO(true);
|
||||
this.enableEditMode = true;
|
||||
this.clickedEditButton = true;
|
||||
}
|
||||
},
|
||||
|
||||
save() {
|
||||
this.$root.getSocket().emit("saveStatusPage", this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
|
||||
let startTime = new Date();
|
||||
this.config.slug = this.config.slug.trim().toLowerCase();
|
||||
|
||||
this.$root.getSocket().emit("saveStatusPage", this.slug, this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
|
||||
if (res.ok) {
|
||||
this.enableEditMode = false;
|
||||
this.$root.publicGroupList = res.publicGroupList;
|
||||
location.reload();
|
||||
|
||||
// Add some delay, so that the side menu animation would be better
|
||||
let endTime = new Date();
|
||||
let time = 100 - (endTime - startTime) / 1000;
|
||||
|
||||
if (time < 0) {
|
||||
time = 0;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
location.href = "/status/" + this.config.slug;
|
||||
}, time);
|
||||
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
deleteDialog() {
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
deleteStatusPage() {
|
||||
this.$root.getSocket().emit("deleteStatusPage", this.slug, (res) => {
|
||||
if (res.ok) {
|
||||
this.enableEditMode = false;
|
||||
location.href = "/manage-status-page";
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
|
@ -481,30 +576,7 @@ export default {
|
|||
},
|
||||
|
||||
discard() {
|
||||
location.reload();
|
||||
},
|
||||
|
||||
changeTheme(name) {
|
||||
this.config.statusPageTheme = name;
|
||||
},
|
||||
changeTagsVisibilty(newState) {
|
||||
this.config.statusPageTags = newState;
|
||||
|
||||
// On load, the status page will not include tags if it's not enabled for security reasons
|
||||
// Which means if we enable tags, it won't show in the UI until saved
|
||||
// So we have this to enhance UX and load in the tags from the authenticated source instantly
|
||||
this.$root.publicGroupList = this.$root.publicGroupList.map((group) => {
|
||||
return {
|
||||
...group,
|
||||
monitorList: group.monitorList.map((monitor) => {
|
||||
// We only include the tags if visible so we can reuse the logic to hide the tags on disable
|
||||
return {
|
||||
...monitor,
|
||||
tags: newState ? this.$root.monitorList[monitor.id].tags : []
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
location.href = "/status/" + this.slug;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -540,7 +612,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
this.$root.getSocket().emit("postIncident", this.incident, (res) => {
|
||||
this.$root.getSocket().emit("postIncident", this.slug, this.incident, (res) => {
|
||||
|
||||
if (res.ok) {
|
||||
this.enableEditIncidentMode = false;
|
||||
|
@ -571,7 +643,7 @@ export default {
|
|||
},
|
||||
|
||||
unpinIncident() {
|
||||
this.$root.getSocket().emit("unpinIncident", () => {
|
||||
this.$root.getSocket().emit("unpinIncident", this.slug, () => {
|
||||
this.incident = null;
|
||||
});
|
||||
},
|
||||
|
@ -614,6 +686,40 @@ h1 {
|
|||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
transition: all ease-in-out 0.1s;
|
||||
|
||||
&.edit {
|
||||
margin-left: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 300px;
|
||||
height: 100vh;
|
||||
padding: 15px 15px 68px 15px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #ededed;
|
||||
|
||||
.danger-zone {
|
||||
border-top: 1px solid #ededed;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 15px;
|
||||
position: absolute;
|
||||
border-top: 1px solid #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
@ -623,6 +729,12 @@ footer {
|
|||
min-width: 50px;
|
||||
}
|
||||
|
||||
.title-flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
@ -681,4 +793,19 @@ footer {
|
|||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.sidebar {
|
||||
background-color: $dark-header-bg;
|
||||
border-right-color: $dark-border-color;
|
||||
|
||||
.danger-zone {
|
||||
border-top-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
border-top-color: $dark-border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -18,6 +18,9 @@ import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
|||
import Security from "./components/settings/Security.vue";
|
||||
import Backup from "./components/settings/Backup.vue";
|
||||
import About from "./components/settings/About.vue";
|
||||
import ManageStatusPage from "./pages/ManageStatusPage.vue";
|
||||
import AddStatusPage from "./pages/AddStatusPage.vue";
|
||||
import NotFound from "./pages/NotFound.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
|
@ -70,7 +73,6 @@ const routes = [
|
|||
children: [
|
||||
{
|
||||
path: "general",
|
||||
alias: "",
|
||||
component: General,
|
||||
},
|
||||
{
|
||||
|
@ -99,6 +101,14 @@ const routes = [
|
|||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/manage-status-page",
|
||||
component: ManageStatusPage,
|
||||
},
|
||||
{
|
||||
path: "/add-status-page",
|
||||
component: AddStatusPage,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -115,6 +125,14 @@ const routes = [
|
|||
path: "/status",
|
||||
component: StatusPage,
|
||||
},
|
||||
{
|
||||
path: "/status/:slug",
|
||||
component: StatusPage,
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
component: NotFound,
|
||||
},
|
||||
];
|
||||
|
||||
export const router = createRouter({
|
||||
|
|
|
@ -51,7 +51,19 @@ export function timezoneList() {
|
|||
}
|
||||
|
||||
export function setPageLocale() {
|
||||
const html = document.documentElement
|
||||
html.setAttribute('lang', currentLocale() )
|
||||
html.setAttribute('dir', localeDirection() )
|
||||
const html = document.documentElement;
|
||||
html.setAttribute("lang", currentLocale() );
|
||||
html.setAttribute("dir", localeDirection() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Mainly used for dev, because the backend and the frontend are in different ports.
|
||||
*/
|
||||
export function getResBaseURL() {
|
||||
const env = process.env.NODE_ENV;
|
||||
if (env === "development" || localStorage.dev === "dev") {
|
||||
return location.protocol + "//" + location.hostname + ":3001";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue