mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-12-17 03:34:51 -05:00
Merge branch 'master' into feature/request-with-http-proxy
# Conflicts: # package-lock.json # package.json # server/database.js # src/languages/en.js # src/mixins/socket.js
This commit is contained in:
commit
04e3394d02
@ -196,14 +196,13 @@ https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
|
|||||||
### Release Procedures
|
### Release Procedures
|
||||||
|
|
||||||
1. Draft a release note
|
1. Draft a release note
|
||||||
1. Make sure the repo is cleared
|
2. Make sure the repo is cleared
|
||||||
1. `npm run update-version 1.X.X`
|
3. `npm run release-final with env vars: `VERSION` and `GITHUB_TOKEN`
|
||||||
1. `npm run build`
|
4. Wait until the `Press any key to continue`
|
||||||
1. `npm run build-docker`
|
5. `git push`
|
||||||
1. `git push`
|
6. Publish the release note as 1.X.X
|
||||||
1. Publish the release note as 1.X.X
|
7. Press any key to continue
|
||||||
1. `npm run upload-artifacts` with env vars VERSION=1.X.X;GITHUB_TOKEN=XXXX
|
8. SSH to demo site server and update to 1.X.X
|
||||||
1. SSH to demo site server and update to 1.X.X
|
|
||||||
|
|
||||||
Checking:
|
Checking:
|
||||||
|
|
||||||
@ -211,6 +210,15 @@ Checking:
|
|||||||
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
|
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
|
||||||
- Try clean installation with Node.js
|
- Try clean installation with Node.js
|
||||||
|
|
||||||
|
### Release Beta Procedures
|
||||||
|
|
||||||
|
1. Draft a release note, check "This is a pre-release"
|
||||||
|
2. Make sure the repo is cleared
|
||||||
|
3. `npm run release-beta` with env vars: `VERSION` and `GITHUB_TOKEN`
|
||||||
|
4. Wait until the `Press any key to continue`
|
||||||
|
5. Publish the release note as 1.X.X-beta.X
|
||||||
|
6. Press any key to continue
|
||||||
|
|
||||||
### Release Wiki
|
### Release Wiki
|
||||||
|
|
||||||
#### Setup Repo
|
#### Setup Repo
|
||||||
|
@ -61,8 +61,14 @@ npm run setup
|
|||||||
node server/server.js
|
node server/server.js
|
||||||
|
|
||||||
# (Recommended) Option 2. Run in background using PM2
|
# (Recommended) Option 2. Run in background using PM2
|
||||||
# Install PM2 if you don't have it: npm install pm2 -g
|
# Install PM2 if you don't have it:
|
||||||
|
npm install pm2 -g && pm2 install pm2-logrotate
|
||||||
|
|
||||||
|
# Start Server
|
||||||
pm2 start server/server.js --name uptime-kuma
|
pm2 start server/server.js --name uptime-kuma
|
||||||
|
|
||||||
|
# If you want to see the current console output
|
||||||
|
pm2 monit
|
||||||
```
|
```
|
||||||
|
|
||||||
Browse to http://localhost:3001 after starting.
|
Browse to http://localhost:3001 after starting.
|
||||||
|
31
db/patch-status-page.sql
Normal file
31
db/patch-status-page.sql
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE [status_page](
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[slug] VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
[title] VARCHAR(255) NOT NULL,
|
||||||
|
[description] TEXT,
|
||||||
|
[icon] VARCHAR(255) NOT NULL,
|
||||||
|
[theme] VARCHAR(30) NOT NULL,
|
||||||
|
[published] BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
[search_engine_index] BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
[show_tags] BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
[password] VARCHAR,
|
||||||
|
[created_date] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
[modified_date] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX [slug] ON [status_page]([slug]);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE [status_page_cname](
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[status_page_id] INTEGER NOT NULL REFERENCES [status_page]([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
[domain] VARCHAR NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE incident ADD status_page_id INTEGER;
|
||||||
|
ALTER TABLE [group] ADD status_page_id INTEGER;
|
||||||
|
|
||||||
|
COMMIT;
|
@ -1,5 +1,5 @@
|
|||||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||||
FROM node:14-alpine3.12
|
FROM node:16-alpine3.12
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install apprise, iputils for non-root ping, setpriv
|
# Install apprise, iputils for non-root ping, setpriv
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||||
# If the image changed, the second stage image should be changed too
|
# If the image changed, the second stage image should be changed too
|
||||||
FROM node:14-buster-slim
|
FROM node:16-buster-slim
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Curl
|
||||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||||
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine!
|
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine!
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
@ -10,3 +13,14 @@ RUN apt update && \
|
|||||||
sqlite3 iputils-ping util-linux dumb-init && \
|
sqlite3 iputils-ping util-linux dumb-init && \
|
||||||
pip3 --no-cache-dir install apprise==0.9.7 && \
|
pip3 --no-cache-dir install apprise==0.9.7 && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install cloudflared
|
||||||
|
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
|
||||||
|
COPY extra/download-cloudflared.js ./extra/download-cloudflared.js
|
||||||
|
RUN node ./extra/download-cloudflared.js $TARGETPLATFORM && \
|
||||||
|
dpkg --add-architecture arm && \
|
||||||
|
apt update && \
|
||||||
|
apt --yes --no-install-recommends install ./cloudflared.deb && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
rm -f cloudflared.deb
|
||||||
|
|
||||||
|
76
extra/beta/update-version.js
Normal file
76
extra/beta/update-version.js
Normal file
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
44
extra/download-cloudflared.js
Normal file
44
extra/download-cloudflared.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
|
||||||
|
const http = require("https"); // or 'https' for https:// URLs
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const platform = process.argv[2];
|
||||||
|
|
||||||
|
if (!platform) {
|
||||||
|
console.error("No platform??");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let arch = null;
|
||||||
|
|
||||||
|
if (platform === "linux/amd64") {
|
||||||
|
arch = "amd64";
|
||||||
|
} else if (platform === "linux/arm64") {
|
||||||
|
arch = "arm64";
|
||||||
|
} else if (platform === "linux/arm/v7") {
|
||||||
|
arch = "arm";
|
||||||
|
} else {
|
||||||
|
console.error("Invalid platform?? " + platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fs.createWriteStream("cloudflared.deb");
|
||||||
|
get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb");
|
||||||
|
|
||||||
|
function get(url) {
|
||||||
|
http.get(url, function (res) {
|
||||||
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
|
console.log("Redirect to " + res.headers.location);
|
||||||
|
get(res.headers.location);
|
||||||
|
} else if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
res.pipe(file);
|
||||||
|
|
||||||
|
res.on("end", function () {
|
||||||
|
console.log("Downloaded");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(res.statusCode);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
19
extra/env2arg.js
Normal file
19
extra/env2arg.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const childProcess = require("child_process");
|
||||||
|
let env = process.env;
|
||||||
|
|
||||||
|
let cmd = process.argv[2];
|
||||||
|
let args = process.argv.slice(3);
|
||||||
|
let replacedArgs = [];
|
||||||
|
|
||||||
|
for (let arg of args) {
|
||||||
|
for (let key in env) {
|
||||||
|
arg = arg.replaceAll(`$${key}`, env[key]);
|
||||||
|
}
|
||||||
|
replacedArgs.push(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = childProcess.spawn(cmd, replacedArgs);
|
||||||
|
child.stdout.pipe(process.stdout);
|
||||||
|
child.stderr.pipe(process.stderr);
|
@ -189,7 +189,7 @@ if (type == "local") {
|
|||||||
bash("check=$(pm2 --version)");
|
bash("check=$(pm2 --version)");
|
||||||
if (check == "") {
|
if (check == "") {
|
||||||
println("Installing PM2");
|
println("Installing PM2");
|
||||||
bash("npm install pm2 -g");
|
bash("npm install pm2 -g && pm2 install pm2-logrotate");
|
||||||
bash("pm2 startup");
|
bash("pm2 startup");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
extra/press-any-key.js
Normal file
6
extra/press-any-key.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
console.log("Git Push and Publish the release note on github, then press any key to continue");
|
||||||
|
|
||||||
|
process.stdin.setRawMode(true);
|
||||||
|
process.stdin.resume();
|
||||||
|
process.stdin.on("data", process.exit.bind(process, 0));
|
||||||
|
|
@ -5,10 +5,8 @@ const util = require("../src/util");
|
|||||||
|
|
||||||
util.polyfill();
|
util.polyfill();
|
||||||
|
|
||||||
const oldVersion = pkg.version;
|
const newVersion = process.env.VERSION;
|
||||||
const newVersion = process.argv[2];
|
|
||||||
|
|
||||||
console.log("Old Version: " + oldVersion);
|
|
||||||
console.log("New Version: " + newVersion);
|
console.log("New Version: " + newVersion);
|
||||||
|
|
||||||
if (! newVersion) {
|
if (! newVersion) {
|
||||||
@ -22,23 +20,20 @@ if (! exists) {
|
|||||||
|
|
||||||
// Process package.json
|
// Process package.json
|
||||||
pkg.version = newVersion;
|
pkg.version = newVersion;
|
||||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
|
|
||||||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
|
// Replace the version: https://regex101.com/r/hmj2Bc/1
|
||||||
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion);
|
pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
||||||
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion);
|
|
||||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
|
||||||
commit(newVersion);
|
commit(newVersion);
|
||||||
tag(newVersion);
|
tag(newVersion);
|
||||||
|
|
||||||
updateWiki(oldVersion, newVersion);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log("version exists");
|
console.log("version exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
function commit(version) {
|
function commit(version) {
|
||||||
let msg = "update to " + version;
|
let msg = "Update to " + version;
|
||||||
|
|
||||||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||||
let stdout = res.stdout.toString().trim();
|
let stdout = res.stdout.toString().trim();
|
||||||
@ -64,37 +59,3 @@ function tagExists(version) {
|
|||||||
return res.stdout.toString().trim() === version;
|
return res.stdout.toString().trim() === version;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWiki(oldVersion, newVersion) {
|
|
||||||
const wikiDir = "./tmp/wiki";
|
|
||||||
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
|
|
||||||
|
|
||||||
safeDelete(wikiDir);
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
|
|
||||||
let content = fs.readFileSync(howToUpdateFilename).toString();
|
|
||||||
content = content.replaceAll(`git checkout ${oldVersion}`, `git checkout ${newVersion}`);
|
|
||||||
fs.writeFileSync(howToUpdateFilename, content);
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["add", "-A"], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion} from ${oldVersion}`], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Pushing to Github");
|
|
||||||
child_process.spawnSync("git", ["push"], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
safeDelete(wikiDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeDelete(dir) {
|
|
||||||
if (fs.existsSync(dir)) {
|
|
||||||
fs.rmdirSync(dir, {
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
48
extra/update-wiki-version.js
Normal file
48
extra/update-wiki-version.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
const child_process = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const newVersion = process.env.VERSION;
|
||||||
|
|
||||||
|
if (!newVersion) {
|
||||||
|
console.log("Missing version");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWiki(newVersion);
|
||||||
|
|
||||||
|
function updateWiki(newVersion) {
|
||||||
|
const wikiDir = "./tmp/wiki";
|
||||||
|
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
|
||||||
|
|
||||||
|
safeDelete(wikiDir);
|
||||||
|
|
||||||
|
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
|
||||||
|
let content = fs.readFileSync(howToUpdateFilename).toString();
|
||||||
|
|
||||||
|
// Replace the version: https://regex101.com/r/hmj2Bc/1
|
||||||
|
content = content.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
||||||
|
fs.writeFileSync(howToUpdateFilename, content);
|
||||||
|
|
||||||
|
child_process.spawnSync("git", ["add", "-A"], {
|
||||||
|
cwd: wikiDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion}`], {
|
||||||
|
cwd: wikiDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Pushing to Github");
|
||||||
|
child_process.spawnSync("git", ["push"], {
|
||||||
|
cwd: wikiDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
safeDelete(wikiDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeDelete(dir) {
|
||||||
|
if (fs.existsSync(dir)) {
|
||||||
|
fs.rmdirSync(dir, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -159,7 +159,7 @@ fi
|
|||||||
check=$(pm2 --version)
|
check=$(pm2 --version)
|
||||||
if [ "$check" == "" ]; then
|
if [ "$check" == "" ]; then
|
||||||
"echo" "-e" "Installing PM2"
|
"echo" "-e" "Installing PM2"
|
||||||
npm install pm2 -g
|
npm install pm2 -g && pm2 install pm2-logrotate
|
||||||
pm2 startup
|
pm2 startup
|
||||||
fi
|
fi
|
||||||
mkdir -p $installPath
|
mkdir -p $installPath
|
||||||
|
47
package.json
47
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.12.1",
|
"version": "1.14.0-beta.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -30,15 +30,14 @@
|
|||||||
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
|
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
|
||||||
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
||||||
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
||||||
"build-docker-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.12.1-alpine --target release . --push",
|
"build-docker-alpine": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:$VERSION-alpine --target release . --push",
|
||||||
"build-docker-debian": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.12.1 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.12.1-debian --target release . --push",
|
"build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
|
||||||
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||||
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||||
"setup": "git checkout 1.12.1 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.13.1 && npm ci --production && npm run download-dist",
|
||||||
"download-dist": "node extra/download-dist.js",
|
"download-dist": "node extra/download-dist.js",
|
||||||
"update-version": "node extra/update-version.js",
|
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
"remove-2fa": "node extra/remove-2fa.js",
|
"remove-2fa": "node extra/remove-2fa.js",
|
||||||
@ -51,7 +50,10 @@
|
|||||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||||
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
||||||
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
|
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
|
||||||
"ncu-patch": "ncu -u -t patch"
|
"ncu-patch": "npm-check-updates -u -t patch",
|
||||||
|
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||||
|
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
||||||
|
"git-remove-tag": "git tag -d"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||||
@ -61,34 +63,36 @@
|
|||||||
"@louislam/sqlite3": "~6.0.1",
|
"@louislam/sqlite3": "~6.0.1",
|
||||||
"@popperjs/core": "~2.10.2",
|
"@popperjs/core": "~2.10.2",
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.26.0",
|
"axios": "~0.26.1",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"bootstrap": "5.1.3",
|
"bootstrap": "5.1.3",
|
||||||
"bree": "~7.1.0",
|
"bree": "~7.1.5",
|
||||||
"chardet": "^1.3.0",
|
"chardet": "^1.3.0",
|
||||||
"chart.js": "~3.6.0",
|
"chart.js": "~3.6.2",
|
||||||
"chartjs-adapter-dayjs": "~1.0.0",
|
"chartjs-adapter-dayjs": "~1.0.0",
|
||||||
"check-password-strength": "^2.0.3",
|
"check-password-strength": "^2.0.5",
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
"compare-versions": "~3.6.0",
|
"compare-versions": "~3.6.0",
|
||||||
"dayjs": "~1.10.7",
|
"dayjs": "~1.10.8",
|
||||||
"express": "~4.17.1",
|
"express": "~4.17.3",
|
||||||
"express-basic-auth": "~1.2.0",
|
"express-basic-auth": "~1.2.1",
|
||||||
|
"favico.js": "^0.3.10",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"http-graceful-shutdown": "~3.1.5",
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
"http-proxy-agent": "^5.0.0",
|
"http-proxy-agent": "^5.0.0",
|
||||||
"https-proxy-agent": "^5.0.0",
|
"https-proxy-agent": "^5.0.0",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"limiter": "^2.1.0",
|
"limiter": "^2.1.0",
|
||||||
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
"nodemailer": "~6.6.5",
|
"nodemailer": "~6.6.5",
|
||||||
"notp": "~2.0.3",
|
"notp": "~2.0.3",
|
||||||
"password-hash": "~1.2.2",
|
"password-hash": "~1.2.2",
|
||||||
"postcss-rtlcss": "~3.4.1",
|
"postcss-rtlcss": "~3.4.1",
|
||||||
"postcss-scss": "~4.0.2",
|
"postcss-scss": "~4.0.3",
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.0",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
"qrcode": "~1.5.0",
|
"qrcode": "~1.5.0",
|
||||||
"redbean-node": "0.1.3",
|
"redbean-node": "0.1.3",
|
||||||
"socket.io": "~4.4.1",
|
"socket.io": "~4.4.1",
|
||||||
@ -107,7 +111,7 @@
|
|||||||
"vue-image-crop-upload": "~3.0.3",
|
"vue-image-crop-upload": "~3.0.3",
|
||||||
"vue-multiselect": "~3.0.0-alpha.2",
|
"vue-multiselect": "~3.0.0-alpha.2",
|
||||||
"vue-qrcode": "~1.0.0",
|
"vue-qrcode": "~1.0.0",
|
||||||
"vue-router": "~4.0.12",
|
"vue-router": "~4.0.14",
|
||||||
"vue-toastification": "~2.0.0-rc.5",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0"
|
"vuedraggable": "~4.1.0"
|
||||||
},
|
},
|
||||||
@ -115,10 +119,10 @@
|
|||||||
"@actions/github": "~5.0.0",
|
"@actions/github": "~5.0.0",
|
||||||
"@babel/eslint-parser": "~7.15.8",
|
"@babel/eslint-parser": "~7.15.8",
|
||||||
"@babel/preset-env": "^7.15.8",
|
"@babel/preset-env": "^7.15.8",
|
||||||
"@types/bootstrap": "~5.1.6",
|
"@types/bootstrap": "~5.1.9",
|
||||||
"@vitejs/plugin-legacy": "~1.6.3",
|
"@vitejs/plugin-legacy": "~1.6.4",
|
||||||
"@vitejs/plugin-vue": "~1.9.4",
|
"@vitejs/plugin-vue": "~1.9.4",
|
||||||
"@vue/compiler-sfc": "~3.2.22",
|
"@vue/compiler-sfc": "~3.2.31",
|
||||||
"babel-plugin-rewire": "~1.2.0",
|
"babel-plugin-rewire": "~1.2.0",
|
||||||
"core-js": "~3.18.3",
|
"core-js": "~3.18.3",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
@ -126,7 +130,8 @@
|
|||||||
"eslint": "~7.32.0",
|
"eslint": "~7.32.0",
|
||||||
"eslint-plugin-vue": "~7.18.0",
|
"eslint-plugin-vue": "~7.18.0",
|
||||||
"jest": "~27.2.5",
|
"jest": "~27.2.5",
|
||||||
"jest-puppeteer": "~6.0.0",
|
"jest-puppeteer": "~6.0.3",
|
||||||
|
"npm-check-updates": "^12.5.4",
|
||||||
"puppeteer": "~13.1.3",
|
"puppeteer": "~13.1.3",
|
||||||
"sass": "~1.42.1",
|
"sass": "~1.42.1",
|
||||||
"stylelint": "~14.2.0",
|
"stylelint": "~14.2.0",
|
||||||
|
@ -12,6 +12,10 @@ const { loginRateLimiter } = require("./rate-limiter");
|
|||||||
* @returns {Promise<Bean|null>}
|
* @returns {Promise<Bean|null>}
|
||||||
*/
|
*/
|
||||||
exports.login = async function (username, password) {
|
exports.login = async function (username, password) {
|
||||||
|
if (typeof username !== "string" || typeof password !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||||
username,
|
username,
|
||||||
]);
|
]);
|
||||||
@ -31,31 +35,34 @@ exports.login = async function (username, password) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function myAuthorizer(username, password, callback) {
|
function myAuthorizer(username, password, callback) {
|
||||||
setting("disableAuth").then((result) => {
|
// Login Rate Limit
|
||||||
if (result) {
|
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||||
callback(null, true);
|
if (pass) {
|
||||||
} else {
|
exports.login(username, password).then((user) => {
|
||||||
// Login Rate Limit
|
callback(null, user != null);
|
||||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
|
||||||
if (pass) {
|
|
||||||
exports.login(username, password).then((user) => {
|
|
||||||
callback(null, user != null);
|
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
loginRateLimiter.removeTokens(1);
|
loginRateLimiter.removeTokens(1);
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
callback(null, false);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
callback(null, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.basicAuth = basicAuth({
|
exports.basicAuth = async function (req, res, next) {
|
||||||
authorizer: myAuthorizer,
|
const middleware = basicAuth({
|
||||||
authorizeAsync: true,
|
authorizer: myAuthorizer,
|
||||||
challenge: true,
|
authorizeAsync: true,
|
||||||
});
|
challenge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabledAuth = await setting("disableAuth");
|
||||||
|
|
||||||
|
if (!disabledAuth) {
|
||||||
|
middleware(req, res, next);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const { setSetting } = require("./util-server");
|
const { setSetting, setting } = require("./util-server");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
|
const compareVersions = require("compare-versions");
|
||||||
|
|
||||||
exports.version = require("../package.json").version;
|
exports.version = require("../package.json").version;
|
||||||
exports.latestVersion = null;
|
exports.latestVersion = null;
|
||||||
@ -16,6 +17,19 @@ exports.startInterval = () => {
|
|||||||
res.data.slow = "1000.0.0";
|
res.data.slow = "1000.0.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!await setting("checkUpdate")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let checkBeta = await setting("checkBeta");
|
||||||
|
|
||||||
|
if (checkBeta && res.data.beta) {
|
||||||
|
if (compareVersions.compare(res.data.beta, res.data.beta, ">")) {
|
||||||
|
exports.latestVersion = res.data.beta;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (res.data.slow) {
|
if (res.data.slow) {
|
||||||
exports.latestVersion = res.data.slow;
|
exports.latestVersion = res.data.slow;
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,7 @@ class Database {
|
|||||||
"patch-2fa-invalidate-used-token.sql": true,
|
"patch-2fa-invalidate-used-token.sql": true,
|
||||||
"patch-notification_sent_history.sql": true,
|
"patch-notification_sent_history.sql": true,
|
||||||
"patch-monitor-basic-auth.sql": true,
|
"patch-monitor-basic-auth.sql": true,
|
||||||
|
"patch-status-page.sql": true,
|
||||||
"patch-proxy.sql": true,
|
"patch-proxy.sql": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,6 +172,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.patch2();
|
await this.patch2();
|
||||||
|
await this.migrateNewStatusPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -212,6 +214,74 @@ class Database {
|
|||||||
await setSetting("databasePatchedFiles", databasePatchedFiles);
|
await setSetting("databasePatchedFiles", databasePatchedFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate status page value in setting to "status_page" table
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async migrateNewStatusPage() {
|
||||||
|
|
||||||
|
// Fix 1.13.0 empty slug bug
|
||||||
|
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
|
||||||
|
|
||||||
|
let title = await setting("title");
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
console.log("Migrating Status Page");
|
||||||
|
|
||||||
|
let statusPageCheck = await R.findOne("status_page", " slug = 'default' ");
|
||||||
|
|
||||||
|
if (statusPageCheck !== null) {
|
||||||
|
console.log("Migrating Status Page - Skip, default slug record is already existing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusPage = R.dispense("status_page");
|
||||||
|
statusPage.slug = "default";
|
||||||
|
statusPage.title = title;
|
||||||
|
statusPage.description = await setting("description");
|
||||||
|
statusPage.icon = await setting("icon");
|
||||||
|
statusPage.theme = await setting("statusPageTheme");
|
||||||
|
statusPage.published = !!await setting("statusPagePublished");
|
||||||
|
statusPage.search_engine_index = !!await setting("searchEngineIndex");
|
||||||
|
statusPage.show_tags = !!await setting("statusPageTags");
|
||||||
|
statusPage.password = null;
|
||||||
|
|
||||||
|
if (!statusPage.title) {
|
||||||
|
statusPage.title = "My Status Page";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statusPage.icon) {
|
||||||
|
statusPage.icon = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statusPage.theme) {
|
||||||
|
statusPage.theme = "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = await R.store(statusPage);
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET status_page_id = ? WHERE status_page_id IS NULL", [
|
||||||
|
id
|
||||||
|
]);
|
||||||
|
|
||||||
|
await R.exec("UPDATE [group] SET status_page_id = ? WHERE status_page_id IS NULL", [
|
||||||
|
id
|
||||||
|
]);
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
|
||||||
|
|
||||||
|
// Migrate Entry Page if it is status page
|
||||||
|
let entryPage = await setting("entryPage");
|
||||||
|
|
||||||
|
if (entryPage === "statusPage") {
|
||||||
|
await setSetting("entryPage", "statusPage-default", "general");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Migrating Status Page - Done");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used it patch2() only
|
* Used it patch2() only
|
||||||
* @param sqlFilename
|
* @param sqlFilename
|
||||||
|
@ -3,12 +3,12 @@ const { R } = require("redbean-node");
|
|||||||
|
|
||||||
class Group extends BeanModel {
|
class Group extends BeanModel {
|
||||||
|
|
||||||
async toPublicJSON() {
|
async toPublicJSON(showTags = false) {
|
||||||
let monitorBeanList = await this.getMonitorList();
|
let monitorBeanList = await this.getMonitorList();
|
||||||
let monitorList = [];
|
let monitorList = [];
|
||||||
|
|
||||||
for (let bean of monitorBeanList) {
|
for (let bean of monitorBeanList) {
|
||||||
monitorList.push(await bean.toPublicJSON());
|
monitorList.push(await bean.toPublicJSON(showTags));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -25,18 +25,22 @@ const apicache = require("../modules/apicache");
|
|||||||
class Monitor extends BeanModel {
|
class Monitor extends BeanModel {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a object that ready to parse to JSON for public
|
* Return an object that ready to parse to JSON for public
|
||||||
* Only show necessary data to public
|
* Only show necessary data to public
|
||||||
*/
|
*/
|
||||||
async toPublicJSON() {
|
async toPublicJSON(showTags = false) {
|
||||||
return {
|
let obj = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
};
|
};
|
||||||
|
if (showTags) {
|
||||||
|
obj.tags = await this.getTags();
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a object that ready to parse to JSON
|
* Return an object that ready to parse to JSON
|
||||||
*/
|
*/
|
||||||
async toJSON() {
|
async toJSON() {
|
||||||
|
|
||||||
@ -50,7 +54,7 @@ class Monitor extends BeanModel {
|
|||||||
notificationIDList[bean.notification_id] = true;
|
notificationIDList[bean.notification_id] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
const tags = await this.getTags();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
@ -84,6 +88,10 @@ class Monitor extends BeanModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTags() {
|
||||||
|
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode user and password to Base64 encoding
|
* Encode user and password to Base64 encoding
|
||||||
* for HTTP "basic" auth, as per RFC-7617
|
* for HTTP "basic" auth, as per RFC-7617
|
||||||
@ -492,6 +500,12 @@ class Monitor extends BeanModel {
|
|||||||
stop() {
|
stop() {
|
||||||
clearTimeout(this.heartbeatInterval);
|
clearTimeout(this.heartbeatInterval);
|
||||||
this.isStop = true;
|
this.isStop = true;
|
||||||
|
|
||||||
|
this.prometheus().remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
prometheus() {
|
||||||
|
return new Prometheus(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
60
server/model/status_page.js
Normal file
60
server/model/status_page.js
Normal file
@ -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;
|
@ -9,36 +9,31 @@ class Pushover extends NotificationProvider {
|
|||||||
let okMsg = "Sent Successfully.";
|
let okMsg = "Sent Successfully.";
|
||||||
let pushoverlink = "https://api.pushover.net/1/messages.json";
|
let pushoverlink = "https://api.pushover.net/1/messages.json";
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg,
|
||||||
|
"user": notification.pushoveruserkey,
|
||||||
|
"token": notification.pushoverapptoken,
|
||||||
|
"sound": notification.pushoversounds,
|
||||||
|
"priority": notification.pushoverpriority,
|
||||||
|
"title": notification.pushovertitle,
|
||||||
|
"retry": "30",
|
||||||
|
"expire": "3600",
|
||||||
|
"html": 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notification.pushoverdevice) {
|
||||||
|
data.device = notification.pushoverdevice;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (heartbeatJSON == null) {
|
if (heartbeatJSON == null) {
|
||||||
let data = {
|
await axios.post(pushoverlink, data);
|
||||||
"message": msg,
|
return okMsg;
|
||||||
"user": notification.pushoveruserkey,
|
} else {
|
||||||
"token": notification.pushoverapptoken,
|
data.message += "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"];
|
||||||
"sound": notification.pushoversounds,
|
|
||||||
"priority": notification.pushoverpriority,
|
|
||||||
"title": notification.pushovertitle,
|
|
||||||
"retry": "30",
|
|
||||||
"expire": "3600",
|
|
||||||
"html": 1,
|
|
||||||
};
|
|
||||||
await axios.post(pushoverlink, data);
|
await axios.post(pushoverlink, data);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = {
|
|
||||||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
|
|
||||||
"user": notification.pushoveruserkey,
|
|
||||||
"token": notification.pushoverapptoken,
|
|
||||||
"sound": notification.pushoversounds,
|
|
||||||
"priority": notification.pushoverpriority,
|
|
||||||
"title": notification.pushovertitle,
|
|
||||||
"retry": "30",
|
|
||||||
"expire": "3600",
|
|
||||||
"html": 1,
|
|
||||||
};
|
|
||||||
await axios.post(pushoverlink, data);
|
|
||||||
return okMsg;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.throwGeneralAxiosError(error);
|
this.throwGeneralAxiosError(error);
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,16 @@ class Prometheus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
try {
|
||||||
|
monitor_cert_days_remaining.remove(this.monitorLabelValues);
|
||||||
|
monitor_cert_is_valid.remove(this.monitorLabelValues);
|
||||||
|
monitor_response_time.remove(this.monitorLabelValues);
|
||||||
|
monitor_status.remove(this.monitorLabelValues);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -34,6 +34,14 @@ const loginRateLimiter = new KumaRateLimiter({
|
|||||||
errorMessage: "Too frequently, try again later."
|
errorMessage: "Too frequently, try again later."
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const twoFaRateLimiter = new KumaRateLimiter({
|
||||||
|
tokensPerInterval: 30,
|
||||||
|
interval: "minute",
|
||||||
|
fireImmediately: true,
|
||||||
|
errorMessage: "Too frequently, try again later."
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
loginRateLimiter
|
loginRateLimiter,
|
||||||
|
twoFaRateLimiter,
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@ const apicache = require("../modules/apicache");
|
|||||||
const Monitor = require("../model/monitor");
|
const Monitor = require("../model/monitor");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { UP, flipStatus, debug } = require("../../src/util");
|
const { UP, flipStatus, debug } = require("../../src/util");
|
||||||
|
const StatusPage = require("../model/status_page");
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
let cache = apicache.middleware;
|
let cache = apicache.middleware;
|
||||||
@ -82,110 +83,80 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Status Page Config
|
// Status page config, incident, monitor list
|
||||||
router.get("/api/status-page/config", async (_request, response) => {
|
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
|
||||||
allowDevAllOrigin(response);
|
allowDevAllOrigin(response);
|
||||||
|
let slug = request.params.slug;
|
||||||
|
|
||||||
let config = await getSettings("statusPage");
|
// Get Status Page
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
if (! config.statusPageTheme) {
|
if (!statusPage) {
|
||||||
config.statusPageTheme = "light";
|
response.statusCode = 404;
|
||||||
|
response.json({
|
||||||
|
msg: "Not Found"
|
||||||
|
});
|
||||||
|
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 {
|
try {
|
||||||
await checkPublished();
|
// Incident
|
||||||
|
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
||||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1");
|
statusPage.id,
|
||||||
|
]);
|
||||||
|
|
||||||
if (incident) {
|
if (incident) {
|
||||||
incident = incident.toPublicJSON();
|
incident = incident.toPublicJSON();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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({
|
response.json({
|
||||||
ok: true,
|
config: await statusPage.toPublicJSON(),
|
||||||
incident,
|
incident,
|
||||||
|
publicGroupList
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
send403(response, error.message);
|
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
|
// Status Page Polling Data
|
||||||
// Can fetch only if published
|
// Can fetch only if published
|
||||||
router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => {
|
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
|
||||||
allowDevAllOrigin(response);
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await checkPublished();
|
|
||||||
|
|
||||||
let heartbeatList = {};
|
let heartbeatList = {};
|
||||||
let uptimeList = {};
|
let uptimeList = {};
|
||||||
|
|
||||||
|
let slug = request.params.slug;
|
||||||
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
let monitorIDList = await R.getCol(`
|
let monitorIDList = await R.getCol(`
|
||||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||||
WHERE monitor_group.group_id = \`group\`.id
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
AND public = 1
|
AND public = 1
|
||||||
`);
|
AND \`group\`.status_page_id = ?
|
||||||
|
`, [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
for (let monitorID of monitorIDList) {
|
for (let monitorID of monitorIDList) {
|
||||||
let list = await R.getAll(`
|
let list = await R.getAll(`
|
||||||
@ -214,22 +185,12 @@ router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, re
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function checkPublished() {
|
|
||||||
if (! await isPublished()) {
|
|
||||||
throw new Error("The status page is not published");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default is published
|
* Default is published
|
||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async function isPublished() {
|
async function isPublished() {
|
||||||
const value = await setting("statusPagePublished");
|
return true;
|
||||||
if (value === null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function send403(res, msg = "") {
|
function send403(res, msg = "") {
|
||||||
|
136
server/server.js
136
server/server.js
@ -52,7 +52,7 @@ console.log("Importing this project modules");
|
|||||||
debug("Importing Monitor");
|
debug("Importing Monitor");
|
||||||
const Monitor = require("./model/monitor");
|
const Monitor = require("./model/monitor");
|
||||||
debug("Importing Settings");
|
debug("Importing Settings");
|
||||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog } = require("./util-server");
|
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog, doubleCheckPassword } = require("./util-server");
|
||||||
|
|
||||||
debug("Importing Notification");
|
debug("Importing Notification");
|
||||||
const { Notification } = require("./notification");
|
const { Notification } = require("./notification");
|
||||||
@ -66,7 +66,7 @@ const Database = require("./database");
|
|||||||
|
|
||||||
debug("Importing Background Jobs");
|
debug("Importing Background Jobs");
|
||||||
const { initBackgroundJobs } = require("./jobs");
|
const { initBackgroundJobs } = require("./jobs");
|
||||||
const { loginRateLimiter } = require("./rate-limiter");
|
const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter");
|
||||||
|
|
||||||
const { basicAuth } = require("./auth");
|
const { basicAuth } = require("./auth");
|
||||||
const { login } = require("./auth");
|
const { login } = require("./auth");
|
||||||
@ -94,6 +94,7 @@ const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.p
|
|||||||
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||||
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||||
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
|
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
|
||||||
|
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
|
||||||
|
|
||||||
// 2FA / notp verification defaults
|
// 2FA / notp verification defaults
|
||||||
const twofa_verification_opts = {
|
const twofa_verification_opts = {
|
||||||
@ -135,6 +136,8 @@ const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sen
|
|||||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||||
const TwoFA = require("./2fa");
|
const TwoFA = require("./2fa");
|
||||||
|
const StatusPage = require("./model/status_page");
|
||||||
|
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart } = require("./socket-handlers/cloudflared-socket-handler");
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
@ -203,8 +206,8 @@ exports.entryPage = "dashboard";
|
|||||||
|
|
||||||
// Entry Page
|
// Entry Page
|
||||||
app.get("/", async (_request, response) => {
|
app.get("/", async (_request, response) => {
|
||||||
if (exports.entryPage === "statusPage") {
|
if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
||||||
response.redirect("/status");
|
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
||||||
} else {
|
} else {
|
||||||
response.redirect("/dashboard");
|
response.redirect("/dashboard");
|
||||||
}
|
}
|
||||||
@ -307,6 +310,15 @@ exports.entryPage = "dashboard";
|
|||||||
socket.on("login", async (data, callback) => {
|
socket.on("login", async (data, callback) => {
|
||||||
console.log("Login");
|
console.log("Login");
|
||||||
|
|
||||||
|
// Checking
|
||||||
|
if (typeof callback !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Login Rate Limit
|
// Login Rate Limit
|
||||||
if (! await loginRateLimiter.pass(callback)) {
|
if (! await loginRateLimiter.pass(callback)) {
|
||||||
return;
|
return;
|
||||||
@ -365,14 +377,27 @@ exports.entryPage = "dashboard";
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("logout", async (callback) => {
|
socket.on("logout", async (callback) => {
|
||||||
|
// Rate Limit
|
||||||
|
if (! await loginRateLimiter.pass(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
socket.leave(socket.userID);
|
socket.leave(socket.userID);
|
||||||
socket.userID = null;
|
socket.userID = null;
|
||||||
callback();
|
|
||||||
|
if (typeof callback === "function") {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("prepare2FA", async (callback) => {
|
socket.on("prepare2FA", async (currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
|
if (! await twoFaRateLimiter.pass(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
socket.userID,
|
socket.userID,
|
||||||
@ -407,14 +432,19 @@ exports.entryPage = "dashboard";
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Error while trying to prepare 2FA.",
|
msg: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("save2FA", async (callback) => {
|
socket.on("save2FA", async (currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
|
if (! await twoFaRateLimiter.pass(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
|
||||||
await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
|
await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
|
||||||
socket.userID,
|
socket.userID,
|
||||||
@ -427,14 +457,19 @@ exports.entryPage = "dashboard";
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Error while trying to change 2FA.",
|
msg: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("disable2FA", async (callback) => {
|
socket.on("disable2FA", async (currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
|
if (! await twoFaRateLimiter.pass(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
await TwoFA.disable2FA(socket.userID);
|
await TwoFA.disable2FA(socket.userID);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
@ -444,36 +479,47 @@ exports.entryPage = "dashboard";
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Error while trying to change 2FA.",
|
msg: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("verifyToken", async (token, callback) => {
|
socket.on("verifyToken", async (token, currentPassword, callback) => {
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
try {
|
||||||
socket.userID,
|
checkLogin(socket);
|
||||||
]);
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
|
||||||
let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
if (user.twofa_last_token !== token && verify) {
|
let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
|
||||||
callback({
|
|
||||||
ok: true,
|
if (user.twofa_last_token !== token && verify) {
|
||||||
valid: true,
|
callback({
|
||||||
});
|
ok: true,
|
||||||
} else {
|
valid: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: "Invalid Token.",
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Invalid Token.",
|
msg: error.message,
|
||||||
valid: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("twoFAStatus", async (callback) => {
|
socket.on("twoFAStatus", async (callback) => {
|
||||||
checkLogin(socket);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
socket.userID,
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
@ -490,9 +536,10 @@ exports.entryPage = "dashboard";
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Error while trying to get 2FA status.",
|
msg: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -581,6 +628,9 @@ exports.entryPage = "dashboard";
|
|||||||
throw new Error("Permission denied.");
|
throw new Error("Permission denied.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset Prometheus labels
|
||||||
|
monitorList[monitor.id]?.prometheus()?.remove();
|
||||||
|
|
||||||
bean.name = monitor.name;
|
bean.name = monitor.name;
|
||||||
bean.type = monitor.type;
|
bean.type = monitor.type;
|
||||||
bean.url = monitor.url;
|
bean.url = monitor.url;
|
||||||
@ -939,21 +989,13 @@ exports.entryPage = "dashboard";
|
|||||||
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
|
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
let user = await doubleCheckPassword(socket, password.currentPassword);
|
||||||
socket.userID,
|
await user.resetPassword(password.newPassword);
|
||||||
]);
|
|
||||||
|
|
||||||
if (user && passwordHash.verify(password.currentPassword, user.password)) {
|
callback({
|
||||||
|
ok: true,
|
||||||
user.resetPassword(password.newPassword);
|
msg: "Password has been updated successfully.",
|
||||||
|
});
|
||||||
callback({
|
|
||||||
ok: true,
|
|
||||||
msg: "Password has been updated successfully.",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error("Incorrect current password");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
@ -980,10 +1022,14 @@ exports.entryPage = "dashboard";
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("setSettings", async (data, callback) => {
|
socket.on("setSettings", async (data, currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
|
if (data.disableAuth) {
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
}
|
||||||
|
|
||||||
await setSettings("general", data);
|
await setSettings("general", data);
|
||||||
exports.entryPage = data.entryPage;
|
exports.entryPage = data.entryPage;
|
||||||
|
|
||||||
@ -1389,6 +1435,7 @@ exports.entryPage = "dashboard";
|
|||||||
|
|
||||||
// Status Page Socket Handler for admin only
|
// Status Page Socket Handler for admin only
|
||||||
statusPageSocketHandler(socket);
|
statusPageSocketHandler(socket);
|
||||||
|
cloudflaredSocketHandler(socket);
|
||||||
databaseSocketHandler(socket);
|
databaseSocketHandler(socket);
|
||||||
|
|
||||||
debug("added all socket handlers");
|
debug("added all socket handlers");
|
||||||
@ -1431,6 +1478,9 @@ exports.entryPage = "dashboard";
|
|||||||
|
|
||||||
initBackgroundJobs(args);
|
initBackgroundJobs(args);
|
||||||
|
|
||||||
|
// Start cloudflared at the end if configured
|
||||||
|
await cloudflaredAutoStart(cloudflaredToken);
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||||
@ -1475,6 +1525,8 @@ async function afterLogin(socket, user) {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
|
await StatusPage.sendStatusPageList(io, socket);
|
||||||
|
|
||||||
for (let monitorID in monitorList) {
|
for (let monitorID in monitorList) {
|
||||||
await sendHeartbeatList(socket, monitorID);
|
await sendHeartbeatList(socket, monitorID);
|
||||||
}
|
}
|
||||||
|
85
server/socket-handlers/cloudflared-socket-handler.js
Normal file
85
server/socket-handlers/cloudflared-socket-handler.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
|
||||||
|
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
|
||||||
|
const { io } = require("../server");
|
||||||
|
|
||||||
|
const prefix = "cloudflared_";
|
||||||
|
const cloudflared = new CloudflaredTunnel();
|
||||||
|
|
||||||
|
cloudflared.change = (running, message) => {
|
||||||
|
io.to("cloudflared").emit(prefix + "running", running);
|
||||||
|
io.to("cloudflared").emit(prefix + "message", message);
|
||||||
|
};
|
||||||
|
|
||||||
|
cloudflared.error = (errorMessage) => {
|
||||||
|
io.to("cloudflared").emit(prefix + "errorMessage", errorMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.cloudflaredSocketHandler = (socket) => {
|
||||||
|
|
||||||
|
socket.on(prefix + "join", async () => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
socket.join("cloudflared");
|
||||||
|
io.to(socket.userID).emit(prefix + "installed", cloudflared.checkInstalled());
|
||||||
|
io.to(socket.userID).emit(prefix + "running", cloudflared.running);
|
||||||
|
io.to(socket.userID).emit(prefix + "token", await setting("cloudflaredTunnelToken"));
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(prefix + "leave", async () => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
socket.leave("cloudflared");
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(prefix + "start", async (token) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
if (token && typeof token === "string") {
|
||||||
|
await setSetting("cloudflaredTunnelToken", token);
|
||||||
|
cloudflared.token = token;
|
||||||
|
} else {
|
||||||
|
cloudflared.token = null;
|
||||||
|
}
|
||||||
|
cloudflared.start();
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(prefix + "stop", async (currentPassword, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
cloudflared.stop();
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(prefix + "removeToken", async () => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
await setSetting("cloudflaredTunnelToken", "");
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.autoStart = async (token) => {
|
||||||
|
if (!token) {
|
||||||
|
token = await setting("cloudflaredTunnelToken");
|
||||||
|
} else {
|
||||||
|
// Override the current token via args or env var
|
||||||
|
await setSetting("cloudflaredTunnelToken", token);
|
||||||
|
console.log("Use cloudflared token from args or env var");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
console.log("Start cloudflared");
|
||||||
|
cloudflared.token = token;
|
||||||
|
cloudflared.start();
|
||||||
|
}
|
||||||
|
};
|
@ -1,25 +1,36 @@
|
|||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { checkLogin, setSettings } = require("../util-server");
|
const { checkLogin, setSettings, setSetting } = require("../util-server");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { debug } = require("../../src/util");
|
const { debug } = require("../../src/util");
|
||||||
const ImageDataURI = require("../image-data-uri");
|
const ImageDataURI = require("../image-data-uri");
|
||||||
const Database = require("../database");
|
const Database = require("../database");
|
||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
|
const StatusPage = require("../model/status_page");
|
||||||
|
const server = require("../server");
|
||||||
|
|
||||||
module.exports.statusPageSocketHandler = (socket) => {
|
module.exports.statusPageSocketHandler = (socket) => {
|
||||||
|
|
||||||
// Post or edit incident
|
// Post or edit incident
|
||||||
socket.on("postIncident", async (incident, callback) => {
|
socket.on("postIncident", async (slug, incident, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
await R.exec("UPDATE incident SET pin = 0 ");
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
|
if (!statusPageID) {
|
||||||
|
throw new Error("slug is not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET pin = 0 WHERE status_page_id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
let incidentBean;
|
let incidentBean;
|
||||||
|
|
||||||
if (incident.id) {
|
if (incident.id) {
|
||||||
incidentBean = await R.findOne("incident", " id = ?", [
|
incidentBean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [
|
||||||
incident.id
|
incident.id,
|
||||||
|
statusPageID
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,6 +42,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
incidentBean.content = incident.content;
|
incidentBean.content = incident.content;
|
||||||
incidentBean.style = incident.style;
|
incidentBean.style = incident.style;
|
||||||
incidentBean.pin = true;
|
incidentBean.pin = true;
|
||||||
|
incidentBean.status_page_id = statusPageID;
|
||||||
|
|
||||||
if (incident.id) {
|
if (incident.id) {
|
||||||
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
||||||
@ -52,11 +64,15 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("unpinIncident", async (callback) => {
|
socket.on("unpinIncident", async (slug, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1");
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1 AND status_page_id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@ -71,13 +87,23 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
|
|
||||||
// Save Status Page
|
// Save Status Page
|
||||||
// imgDataUrl Only Accept PNG!
|
// imgDataUrl Only Accept PNG!
|
||||||
socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => {
|
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkSlug(config.slug);
|
||||||
|
|
||||||
|
checkLogin(socket);
|
||||||
apicache.clear();
|
apicache.clear();
|
||||||
|
|
||||||
|
// Save Config
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!statusPage) {
|
||||||
|
throw new Error("No slug?");
|
||||||
|
}
|
||||||
|
|
||||||
const header = "data:image/png;base64,";
|
const header = "data:image/png;base64,";
|
||||||
|
|
||||||
// Check logo format
|
// Check logo format
|
||||||
@ -88,16 +114,28 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
throw new Error("Only allowed PNG logo.");
|
throw new Error("Only allowed PNG logo.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filename = `logo${statusPage.id}.png`;
|
||||||
|
|
||||||
// Convert to file
|
// Convert to file
|
||||||
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png");
|
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + filename);
|
||||||
config.logo = "/upload/logo.png?t=" + Date.now();
|
config.logo = `/upload/${filename}?t=` + Date.now();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
config.icon = imgDataUrl;
|
config.icon = imgDataUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save Config
|
statusPage.slug = config.slug;
|
||||||
await setSettings("statusPage", config);
|
statusPage.title = config.title;
|
||||||
|
statusPage.description = config.description;
|
||||||
|
statusPage.icon = config.logo;
|
||||||
|
statusPage.theme = config.theme;
|
||||||
|
//statusPage.published = ;
|
||||||
|
//statusPage.search_engine_index = ;
|
||||||
|
statusPage.show_tags = config.showTags;
|
||||||
|
//statusPage.password = null;
|
||||||
|
statusPage.modified_date = R.isoDateTime();
|
||||||
|
|
||||||
|
await R.store(statusPage);
|
||||||
|
|
||||||
// Save Public Group List
|
// Save Public Group List
|
||||||
const groupIDList = [];
|
const groupIDList = [];
|
||||||
@ -106,13 +144,15 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
for (let group of publicGroupList) {
|
for (let group of publicGroupList) {
|
||||||
let groupBean;
|
let groupBean;
|
||||||
if (group.id) {
|
if (group.id) {
|
||||||
groupBean = await R.findOne("group", " id = ? AND public = 1 ", [
|
groupBean = await R.findOne("group", " id = ? AND public = 1 AND status_page_id = ? ", [
|
||||||
group.id
|
group.id,
|
||||||
|
statusPage.id
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
groupBean = R.dispense("group");
|
groupBean = R.dispense("group");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupBean.status_page_id = statusPage.id;
|
||||||
groupBean.name = group.name;
|
groupBean.name = group.name;
|
||||||
groupBean.public = true;
|
groupBean.public = true;
|
||||||
groupBean.weight = groupOrder++;
|
groupBean.weight = groupOrder++;
|
||||||
@ -124,7 +164,6 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
let monitorOrder = 1;
|
let monitorOrder = 1;
|
||||||
console.log(group.monitorList);
|
|
||||||
|
|
||||||
for (let monitor of group.monitorList) {
|
for (let monitor of group.monitorList) {
|
||||||
let relationBean = R.dispense("monitor_group");
|
let relationBean = R.dispense("monitor_group");
|
||||||
@ -141,7 +180,18 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
// Delete groups that not in the list
|
// Delete groups that not in the list
|
||||||
debug("Delete groups that not in the list");
|
debug("Delete groups that not in the list");
|
||||||
const slots = groupIDList.map(() => "?").join(",");
|
const slots = groupIDList.map(() => "?").join(",");
|
||||||
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
|
|
||||||
|
const data = [
|
||||||
|
...groupIDList,
|
||||||
|
statusPage.id
|
||||||
|
];
|
||||||
|
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
|
||||||
|
|
||||||
|
// Also change entry page to new slug if it is the default one, and slug is changed.
|
||||||
|
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
|
||||||
|
server.entryPage = "statusPage-" + statusPage.slug;
|
||||||
|
await setSetting("entryPage", server.entryPage, "general");
|
||||||
|
}
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@ -149,7 +199,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.error(error);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
@ -158,4 +208,115 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add a new status page
|
||||||
|
socket.on("addStatusPage", async (title, slug, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
title = title?.trim();
|
||||||
|
slug = slug?.trim();
|
||||||
|
|
||||||
|
// Check empty
|
||||||
|
if (!title || !slug) {
|
||||||
|
throw new Error("Please input all fields");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure slug is string
|
||||||
|
if (typeof slug !== "string") {
|
||||||
|
throw new Error("Slug -Accept string only");
|
||||||
|
}
|
||||||
|
|
||||||
|
// lower case only
|
||||||
|
slug = slug.toLowerCase();
|
||||||
|
|
||||||
|
checkSlug(slug);
|
||||||
|
|
||||||
|
let statusPage = R.dispense("status_page");
|
||||||
|
statusPage.slug = slug;
|
||||||
|
statusPage.title = title;
|
||||||
|
statusPage.theme = "light";
|
||||||
|
statusPage.icon = "";
|
||||||
|
await R.store(statusPage);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "OK!"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete a status page
|
||||||
|
socket.on("deleteStatusPage", async (slug, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
|
if (statusPageID) {
|
||||||
|
|
||||||
|
// Reset entry page if it is the default one.
|
||||||
|
if (server.entryPage === "statusPage-" + slug) {
|
||||||
|
server.entryPage = "dashboard";
|
||||||
|
await setSetting("entryPage", server.entryPage, "general");
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to delete records from `status_page_cname`, because it has cascade foreign key.
|
||||||
|
// But for incident & group, it is hard to add cascade foreign key during migration, so they have to be deleted manually.
|
||||||
|
|
||||||
|
// Delete incident
|
||||||
|
await R.exec("DELETE FROM incident WHERE status_page_id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delete group
|
||||||
|
await R.exec("DELETE FROM `group` WHERE status_page_id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delete status_page
|
||||||
|
await R.exec("DELETE FROM status_page WHERE id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error("Status Page is not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check slug a-z, 0-9, - only
|
||||||
|
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
|
||||||
|
*/
|
||||||
|
function checkSlug(slug) {
|
||||||
|
if (typeof slug !== "string") {
|
||||||
|
throw new Error("Slug must be string");
|
||||||
|
}
|
||||||
|
|
||||||
|
slug = slug.trim();
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
throw new Error("Slug cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!slug.match(/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/)) {
|
||||||
|
throw new Error("Invalid Slug");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
const tcpp = require("tcp-ping");
|
const tcpp = require("tcp-ping");
|
||||||
const Ping = require("./ping-lite");
|
const Ping = require("./ping-lite");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { debug } = require("../src/util");
|
const { debug, genSecret } = require("../src/util");
|
||||||
const passwordHash = require("./password-hash");
|
const passwordHash = require("./password-hash");
|
||||||
const dayjs = require("dayjs");
|
|
||||||
const { Resolver } = require("dns");
|
const { Resolver } = require("dns");
|
||||||
const child_process = require("child_process");
|
const child_process = require("child_process");
|
||||||
const iconv = require("iconv-lite");
|
const iconv = require("iconv-lite");
|
||||||
@ -32,7 +31,7 @@ exports.initJWTSecret = async () => {
|
|||||||
jwtSecretBean.key = "jwtSecret";
|
jwtSecretBean.key = "jwtSecret";
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
|
jwtSecretBean.value = passwordHash.generate(genSecret());
|
||||||
await R.store(jwtSecretBean);
|
await R.store(jwtSecretBean);
|
||||||
return jwtSecretBean;
|
return jwtSecretBean;
|
||||||
};
|
};
|
||||||
@ -321,6 +320,28 @@ exports.checkLogin = (socket) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For logged-in users, double-check the password
|
||||||
|
* @param socket
|
||||||
|
* @param currentPassword
|
||||||
|
* @returns {Promise<Bean>}
|
||||||
|
*/
|
||||||
|
exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||||
|
if (typeof currentPassword !== "string") {
|
||||||
|
throw new Error("Wrong data type?");
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!user || !passwordHash.verify(currentPassword, user.password)) {
|
||||||
|
throw new Error("Incorrect current password");
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
exports.startUnitTest = async () => {
|
exports.startUnitTest = async () => {
|
||||||
console.log("Starting unit test...");
|
console.log("Starting unit test...");
|
||||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||||
|
@ -92,6 +92,10 @@ textarea.form-control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-dark {
|
||||||
|
background-color: #161B22;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 550px) {
|
@media (max-width: 550px) {
|
||||||
.table-shadow-box {
|
.table-shadow-box {
|
||||||
padding: 10px !important;
|
padding: 10px !important;
|
||||||
@ -144,6 +148,10 @@ textarea.form-control {
|
|||||||
background-color: #090c10;
|
background-color: #090c10;
|
||||||
color: $dark-font-color;
|
color: $dark-font-color;
|
||||||
|
|
||||||
|
mark, .mark {
|
||||||
|
background-color: #b6ad86;
|
||||||
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
|
||||||
background: $dark-border-color;
|
background: $dark-border-color;
|
||||||
}
|
}
|
||||||
@ -159,6 +167,12 @@ textarea.form-control {
|
|||||||
border-color: $dark-border-color;
|
border-color: $dark-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
background-color: #282f39;
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
|
||||||
.form-check-input:checked {
|
.form-check-input:checked {
|
||||||
border-color: $primary; // Re-apply bootstrap border
|
border-color: $primary; // Re-apply bootstrap border
|
||||||
}
|
}
|
||||||
@ -167,7 +181,7 @@ textarea.form-control {
|
|||||||
background-color: #232f3b;
|
background-color: #232f3b;
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a:not(.btn),
|
||||||
.table,
|
.table,
|
||||||
.nav-link {
|
.nav-link {
|
||||||
color: $dark-font-color;
|
color: $dark-font-color;
|
||||||
@ -334,11 +348,8 @@ textarea.form-control {
|
|||||||
|
|
||||||
.monitor-list {
|
.monitor-list {
|
||||||
&.scrollbar {
|
&.scrollbar {
|
||||||
min-height: calc(100vh - 240px);
|
|
||||||
max-height: calc(100vh - 30px);
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
position: sticky;
|
height: calc(100% - 65px);
|
||||||
top: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
@ -438,6 +449,10 @@ textarea.form-control {
|
|||||||
border-radius: 10px !important;
|
border-radius: 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
// Localization
|
// Localization
|
||||||
|
|
||||||
@import "localization.scss";
|
@import "localization.scss";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="shadow-box mb-3">
|
<div class="shadow-box mb-3" :style="boxStyle">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div class="placeholder"></div>
|
<div class="placeholder"></div>
|
||||||
<div class="search-wrapper">
|
<div class="search-wrapper">
|
||||||
@ -9,7 +9,9 @@
|
|||||||
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
||||||
<font-awesome-icon icon="times" />
|
<font-awesome-icon icon="times" />
|
||||||
</a>
|
</a>
|
||||||
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" />
|
<form>
|
||||||
|
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" autocomplete="off" />
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
||||||
@ -63,9 +65,16 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchText: "",
|
searchText: "",
|
||||||
|
windowTop: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
boxStyle() {
|
||||||
|
return {
|
||||||
|
height: `calc(100vh - 160px + ${this.windowTop}px)`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
sortedMonitorList() {
|
sortedMonitorList() {
|
||||||
let result = Object.values(this.$root.monitorList);
|
let result = Object.values(this.$root.monitorList);
|
||||||
|
|
||||||
@ -108,7 +117,20 @@ export default {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener("scroll", this.onScroll);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener("scroll", this.onScroll);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onScroll() {
|
||||||
|
if (window.top.scrollY <= 133) {
|
||||||
|
this.windowTop = window.top.scrollY;
|
||||||
|
} else {
|
||||||
|
this.windowTop = 133;
|
||||||
|
}
|
||||||
|
},
|
||||||
monitorURL(id) {
|
monitorURL(id) {
|
||||||
return getMonitorRelativeURL(id);
|
return getMonitorRelativeURL(id);
|
||||||
},
|
},
|
||||||
@ -122,6 +144,12 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../assets/vars.scss";
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.shadow-box {
|
||||||
|
height: calc(100vh - 150px);
|
||||||
|
position: sticky;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.small-padding {
|
.small-padding {
|
||||||
padding-left: 5px !important;
|
padding-left: 5px !important;
|
||||||
padding-right: 5px !important;
|
padding-right: 5px !important;
|
||||||
@ -142,6 +170,12 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.footer {
|
||||||
|
// background-color: $dark-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 770px) {
|
@media (max-width: 770px) {
|
||||||
.list-header {
|
.list-header {
|
||||||
margin: -20px;
|
margin: -20px;
|
||||||
|
@ -145,12 +145,9 @@ export default {
|
|||||||
this.id = null;
|
this.id = null;
|
||||||
this.notification = {
|
this.notification = {
|
||||||
name: "",
|
name: "",
|
||||||
type: null,
|
type: "telegram",
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set Default value here
|
|
||||||
this.notification.type = this.notificationTypes[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.modal.show();
|
this.modal.show();
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
||||||
{{ monitor.element.name }}
|
{{ monitor.element.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tags">
|
<div v-if="showTags" class="tags">
|
||||||
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -76,6 +76,9 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
showTags: {
|
||||||
|
type: Boolean,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -19,6 +19,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
|
<p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
|
||||||
|
|
||||||
|
<div v-if="!(uri && twoFAStatus == false)" class="mb-3">
|
||||||
|
<label for="current-password" class="form-label">
|
||||||
|
{{ $t("Current Password") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="current-password"
|
||||||
|
v-model="currentPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
|
<button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
|
||||||
{{ $t("Enable 2FA") }}
|
{{ $t("Enable 2FA") }}
|
||||||
</button>
|
</button>
|
||||||
@ -59,11 +72,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Modal } from "bootstrap"
|
import { Modal } from "bootstrap";
|
||||||
import Confirm from "./Confirm.vue";
|
import Confirm from "./Confirm.vue";
|
||||||
import VueQrcode from "vue-qrcode"
|
import VueQrcode from "vue-qrcode";
|
||||||
import { useToast } from "vue-toastification"
|
import { useToast } from "vue-toastification";
|
||||||
const toast = useToast()
|
const toast = useToast();
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -73,35 +86,36 @@ export default {
|
|||||||
props: {},
|
props: {},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
currentPassword: "",
|
||||||
processing: false,
|
processing: false,
|
||||||
uri: null,
|
uri: null,
|
||||||
tokenValid: false,
|
tokenValid: false,
|
||||||
twoFAStatus: null,
|
twoFAStatus: null,
|
||||||
token: null,
|
token: null,
|
||||||
showURI: false,
|
showURI: false,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.modal = new Modal(this.$refs.modal)
|
this.modal = new Modal(this.$refs.modal);
|
||||||
this.getStatus();
|
this.getStatus();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
show() {
|
show() {
|
||||||
this.modal.show()
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
confirmEnableTwoFA() {
|
confirmEnableTwoFA() {
|
||||||
this.$refs.confirmEnableTwoFA.show()
|
this.$refs.confirmEnableTwoFA.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
confirmDisableTwoFA() {
|
confirmDisableTwoFA() {
|
||||||
this.$refs.confirmDisableTwoFA.show()
|
this.$refs.confirmDisableTwoFA.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
prepare2FA() {
|
prepare2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
this.$root.getSocket().emit("prepare2FA", (res) => {
|
this.$root.getSocket().emit("prepare2FA", this.currentPassword, (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@ -109,49 +123,51 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
save2FA() {
|
save2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
this.$root.getSocket().emit("save2FA", (res) => {
|
this.$root.getSocket().emit("save2FA", this.currentPassword, (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.$root.toastRes(res)
|
this.$root.toastRes(res);
|
||||||
this.getStatus();
|
this.getStatus();
|
||||||
|
this.currentPassword = "";
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
disable2FA() {
|
disable2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
this.$root.getSocket().emit("disable2FA", (res) => {
|
this.$root.getSocket().emit("disable2FA", this.currentPassword, (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.$root.toastRes(res)
|
this.$root.toastRes(res);
|
||||||
this.getStatus();
|
this.getStatus();
|
||||||
|
this.currentPassword = "";
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
verifyToken() {
|
verifyToken() {
|
||||||
this.$root.getSocket().emit("verifyToken", this.token, (res) => {
|
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.tokenValid = res.valid;
|
this.tokenValid = res.valid;
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getStatus() {
|
getStatus() {
|
||||||
@ -161,10 +177,10 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -4,14 +4,39 @@
|
|||||||
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
||||||
<div class="fs-4 fw-bold">Uptime Kuma</div>
|
<div class="fs-4 fw-bold">Uptime Kuma</div>
|
||||||
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
|
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
|
||||||
<div class="my-1 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
|
|
||||||
|
<div class="my-3 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
|
||||||
|
|
||||||
|
<div class="mt-1">
|
||||||
|
<div class="form-check">
|
||||||
|
<label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> Show update if available</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> Also check beta release</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
return this.$parent.$parent.$parent.saveSettings;
|
||||||
|
},
|
||||||
|
settingsLoaded() {
|
||||||
|
return this.$parent.$parent.$parent.settingsLoaded;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -62,31 +62,31 @@
|
|||||||
|
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
id="entryPageYes"
|
id="entryPageDashboard"
|
||||||
v-model="settings.entryPage"
|
v-model="settings.entryPage"
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="statusPage"
|
name="entryPage"
|
||||||
value="dashboard"
|
value="dashboard"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label class="form-check-label" for="entryPageYes">
|
<label class="form-check-label" for="entryPageDashboard">
|
||||||
{{ $t("Dashboard") }}
|
{{ $t("Dashboard") }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check">
|
<div v-for="statusPage in $root.statusPageList" :key="statusPage.id" class="form-check">
|
||||||
<input
|
<input
|
||||||
id="entryPageNo"
|
:id="'status-page-' + statusPage.id"
|
||||||
v-model="settings.entryPage"
|
v-model="settings.entryPage"
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="statusPage"
|
name="entryPage"
|
||||||
value="statusPage"
|
:value="'statusPage-' + statusPage.slug"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label class="form-check-label" for="entryPageNo">
|
<label class="form-check-label" :for="'status-page-' + statusPage.id">
|
||||||
{{ $t("Status Page") }}
|
{{ $t("Status Page") }} - {{ statusPage.title }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
139
src/components/settings/ReverseProxy.vue
Normal file
139
src/components/settings/ReverseProxy.vue
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h4 class="mt-4">Cloudflare Tunnel</h4>
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<div>
|
||||||
|
cloudflared:
|
||||||
|
<span v-if="installed === true" class="text-primary">{{ $t("Installed") }}</span>
|
||||||
|
<span v-else-if="installed === false" class="text-danger">{{ $t("Not installed") }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ $t("Status") }}:
|
||||||
|
<span v-if="running" class="text-primary">{{ $t("Running") }}</span>
|
||||||
|
<span v-else-if="!running" class="text-danger">{{ $t("Not running") }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="false">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="errorMessage" class="mt-3">
|
||||||
|
Message:
|
||||||
|
<textarea v-model="errorMessage" class="form-control" readonly></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="installed === false">(Download cloudflared from <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/">Cloudflare Website</a>)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- If installed show token input -->
|
||||||
|
<div v-if="installed" class="mb-2">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label" for="cloudflareTunnelToken">
|
||||||
|
Cloudflare Tunnel {{ $t("Token") }}
|
||||||
|
</label>
|
||||||
|
<HiddenInput
|
||||||
|
id="cloudflareTunnelToken"
|
||||||
|
v-model="cloudflareTunnelToken"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
:readonly="running"
|
||||||
|
/>
|
||||||
|
<div class="form-text">
|
||||||
|
<div v-if="cloudflareTunnelToken" class="mb-3">
|
||||||
|
<span v-if="!running" class="remove-token" @click="removeToken">{{ $t("Remove Token") }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Don't know how to get the token? Please read the guide:<br />
|
||||||
|
<a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy-with-Cloudflare-Tunnel" target="_blank">
|
||||||
|
https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy-with-Cloudflare-Tunnel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button v-if="!running" class="btn btn-primary" type="submit" @click="start">
|
||||||
|
{{ $t("Start") }} cloudflared
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="running" class="btn btn-danger" type="submit" @click="$refs.confirmStop.show();">
|
||||||
|
{{ $t("Stop") }} cloudflared
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Confirm ref="confirmStop" btn-style="btn-danger" :yes-text="$t('Stop') + ' cloudflared'" :no-text="$t('Cancel')" @yes="stop">
|
||||||
|
The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<label for="current-password2" class="form-label">
|
||||||
|
{{ $t("Current Password") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="current-password2"
|
||||||
|
v-model="currentPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="mt-4">Other Software</h4>
|
||||||
|
<div>
|
||||||
|
For example: nginx, Apache and Traefik. <br />
|
||||||
|
Please read <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy</a>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../../components/HiddenInput.vue";
|
||||||
|
import Confirm from "../Confirm.vue";
|
||||||
|
|
||||||
|
const prefix = "cloudflared_";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
Confirm
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
// See /src/mixins/socket.js
|
||||||
|
return this.$root.cloudflared;
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$root.getSocket().emit(prefix + "join");
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.$root.getSocket().emit(prefix + "leave");
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
start() {
|
||||||
|
this.$root.getSocket().emit(prefix + "start", this.cloudflareTunnelToken);
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
this.$root.getSocket().emit(prefix + "stop", this.currentPassword, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
removeToken() {
|
||||||
|
this.$root.getSocket().emit(prefix + "removeToken");
|
||||||
|
this.cloudflareTunnelToken = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.remove-token {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -192,6 +192,12 @@
|
|||||||
<p>Пожалуйста, используйте с осторожностью.</p>
|
<p>Пожалуйста, используйте с осторожностью.</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="$i18n.locale === 'uk-UA' ">
|
||||||
|
<p>Ви впевнені, що бажаєте <strong>вимкнути авторизацію</strong>?</p>
|
||||||
|
<p>Це підходить для <strong>тих, у кого встановлена інша авторизація</strong> пееред відкриттям Uptime Kuma, наприклад Cloudflare Access.</p>
|
||||||
|
<p>Будь ласка, використовуйте з обережністю.</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'fa' ">
|
<template v-else-if="$i18n.locale === 'fa' ">
|
||||||
<p>آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?</p>
|
<p>آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?</p>
|
||||||
<p>این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کردهاند</strong>، مانند Cloudflare Access.</p>
|
<p>این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کردهاند</strong>، مانند Cloudflare Access.</p>
|
||||||
@ -222,7 +228,7 @@
|
|||||||
<p>Používejte ji prosím s rozmyslem.</p>
|
<p>Používejte ji prosím s rozmyslem.</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'vi-VN' ">
|
<template v-else-if="$i18n.locale === 'vi-VN' ">
|
||||||
<p>Bạn có muốn <strong>TẮT XÁC THỰC</strong> không?</p>
|
<p>Bạn có muốn <strong>TẮT XÁC THỰC</strong> không?</p>
|
||||||
<p>Điều này rất nguy hiểm<strong>BẤT KỲ AI</strong> cũng có thể truy cập và cướp quyền điều khiển.</p>
|
<p>Điều này rất nguy hiểm<strong>BẤT KỲ AI</strong> cũng có thể truy cập và cướp quyền điều khiển.</p>
|
||||||
<p>Vui lòng <strong>cẩn thận</strong>.</p>
|
<p>Vui lòng <strong>cẩn thận</strong>.</p>
|
||||||
@ -234,6 +240,19 @@
|
|||||||
<p>It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.</p>
|
<p>It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.</p>
|
||||||
<p>Please use this option carefully!</p>
|
<p>Please use this option carefully!</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="current-password2" class="form-label">
|
||||||
|
{{ $t("Current Password") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="current-password2"
|
||||||
|
v-model="password.currentPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Confirm>
|
</Confirm>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -310,7 +329,12 @@ export default {
|
|||||||
|
|
||||||
disableAuth() {
|
disableAuth() {
|
||||||
this.settings.disableAuth = true;
|
this.settings.disableAuth = true;
|
||||||
this.saveSettings();
|
|
||||||
|
// Need current password to disable auth
|
||||||
|
// Set it to empty if done
|
||||||
|
this.saveSettings(() => {
|
||||||
|
this.password.currentPassword = "";
|
||||||
|
}, this.password.currentPassword);
|
||||||
},
|
},
|
||||||
|
|
||||||
enableAuth() {
|
enableAuth() {
|
||||||
|
@ -29,7 +29,8 @@ const languageList = {
|
|||||||
"pl": "Polski",
|
"pl": "Polski",
|
||||||
"et-EE": "eesti",
|
"et-EE": "eesti",
|
||||||
"vi-VN": "Tiếng Việt",
|
"vi-VN": "Tiếng Việt",
|
||||||
"zh-TW": "繁體中文 (台灣)"
|
"zh-TW": "繁體中文 (台灣)",
|
||||||
|
"uk-UA": "Український",
|
||||||
};
|
};
|
||||||
|
|
||||||
let messages = {
|
let messages = {
|
||||||
|
@ -34,6 +34,9 @@ import {
|
|||||||
faAward,
|
faAward,
|
||||||
faLink,
|
faLink,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
|
faPen,
|
||||||
|
faExternalLinkSquareAlt,
|
||||||
|
faSpinner,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
@ -67,6 +70,9 @@ library.add(
|
|||||||
faAward,
|
faAward,
|
||||||
faLink,
|
faLink,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
|
faPen,
|
||||||
|
faExternalLinkSquareAlt,
|
||||||
|
faSpinner,
|
||||||
);
|
);
|
||||||
|
|
||||||
export { FontAwesomeIcon };
|
export { FontAwesomeIcon };
|
||||||
|
@ -197,6 +197,7 @@ export default {
|
|||||||
line: "Line Messenger",
|
line: "Line Messenger",
|
||||||
mattermost: "Mattermost",
|
mattermost: "Mattermost",
|
||||||
"Status Page": "Статус страница",
|
"Status Page": "Статус страница",
|
||||||
|
"Status Pages": "Статус страница",
|
||||||
"Primary Base URL": "Основен базов URL адрес",
|
"Primary Base URL": "Основен базов URL адрес",
|
||||||
"Push URL": "Генериран Push URL адрес",
|
"Push URL": "Генериран Push URL адрес",
|
||||||
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди",
|
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди",
|
||||||
@ -360,4 +361,14 @@ export default {
|
|||||||
smtpDkimHashAlgo: "Хеш алгоритъм (по желание)",
|
smtpDkimHashAlgo: "Хеш алгоритъм (по желание)",
|
||||||
smtpDkimheaderFieldNames: "Хедър ключове за подписване (по желание)",
|
smtpDkimheaderFieldNames: "Хедър ключове за подписване (по желание)",
|
||||||
smtpDkimskipFields: "Хедър ключове, които да не се подписват (по желание)",
|
smtpDkimskipFields: "Хедър ключове, които да не се подписват (по желание)",
|
||||||
|
PushByTechulus: "Push от Techulus",
|
||||||
|
GoogleChat: "Google Chat (Само за работното пространство на Google)",
|
||||||
|
gorush: "Gorush",
|
||||||
|
alerta: "Alerta",
|
||||||
|
alertaApiEndpoint: "Крайна точка на API",
|
||||||
|
alertaEnvironment: "Среда",
|
||||||
|
alertaApiKey: "API Ключ",
|
||||||
|
alertaAlertState: "Състояние на тревога",
|
||||||
|
alertaRecoverState: "Състояние на възстановяване",
|
||||||
|
deleteStatusPageMsg: "Сигурни ли сте, че желаете да изтриете тази статус страница?",
|
||||||
};
|
};
|
||||||
|
@ -183,6 +183,7 @@ export default {
|
|||||||
"Edit Status Page": "Upravit stavovou stránku",
|
"Edit Status Page": "Upravit stavovou stránku",
|
||||||
"Go to Dashboard": "Přejít na nástěnku",
|
"Go to Dashboard": "Přejít na nástěnku",
|
||||||
"Status Page": "Stavová stránka",
|
"Status Page": "Stavová stránka",
|
||||||
|
"Status Pages": "Stavová stránka",
|
||||||
defaultNotificationName: "Moje {notification} upozornění ({číslo})",
|
defaultNotificationName: "Moje {notification} upozornění ({číslo})",
|
||||||
here: "sem",
|
here: "sem",
|
||||||
Required: "Vyžadováno",
|
Required: "Vyžadováno",
|
||||||
|
@ -180,6 +180,7 @@ export default {
|
|||||||
"Edit Status Page": "Rediger Statusside",
|
"Edit Status Page": "Rediger Statusside",
|
||||||
"Go to Dashboard": "Gå til Betjeningspanel",
|
"Go to Dashboard": "Gå til Betjeningspanel",
|
||||||
"Status Page": "Statusside",
|
"Status Page": "Statusside",
|
||||||
|
"Status Pages": "Statusside",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "Email (SMTP)",
|
smtp: "Email (SMTP)",
|
||||||
|
@ -179,6 +179,7 @@ export default {
|
|||||||
"Edit Status Page": "Bearbeite Status-Seite",
|
"Edit Status Page": "Bearbeite Status-Seite",
|
||||||
"Go to Dashboard": "Gehe zum Dashboard",
|
"Go to Dashboard": "Gehe zum Dashboard",
|
||||||
"Status Page": "Status-Seite",
|
"Status Page": "Status-Seite",
|
||||||
|
"Status Pages": "Status-Seite",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "E-Mail (SMTP)",
|
smtp: "E-Mail (SMTP)",
|
||||||
|
@ -183,6 +183,7 @@ export default {
|
|||||||
"Edit Status Page": "Edit Status Page",
|
"Edit Status Page": "Edit Status Page",
|
||||||
"Go to Dashboard": "Go to Dashboard",
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
"Status Page": "Status Page",
|
"Status Page": "Status Page",
|
||||||
|
"Status Pages": "Status Pages",
|
||||||
defaultNotificationName: "My {notification} Alert ({number})",
|
defaultNotificationName: "My {notification} Alert ({number})",
|
||||||
here: "here",
|
here: "here",
|
||||||
Required: "Required",
|
Required: "Required",
|
||||||
@ -330,21 +331,21 @@ export default {
|
|||||||
dark: "dark",
|
dark: "dark",
|
||||||
Post: "Post",
|
Post: "Post",
|
||||||
"Please input title and content": "Please input title and content",
|
"Please input title and content": "Please input title and content",
|
||||||
Created: "Created",
|
"Created": "Created",
|
||||||
"Last Updated": "Last Updated",
|
"Last Updated": "Last Updated",
|
||||||
Unpin: "Unpin",
|
"Unpin": "Unpin",
|
||||||
"Switch to Light Theme": "Switch to Light Theme",
|
"Switch to Light Theme": "Switch to Light Theme",
|
||||||
"Switch to Dark Theme": "Switch to Dark Theme",
|
"Switch to Dark Theme": "Switch to Dark Theme",
|
||||||
"Show Tags": "Show Tags",
|
"Show Tags": "Show Tags",
|
||||||
"Hide Tags": "Hide Tags",
|
"Hide Tags": "Hide Tags",
|
||||||
Description: "Description",
|
"Description": "Description",
|
||||||
"No monitors available.": "No monitors available.",
|
"No monitors available.": "No monitors available.",
|
||||||
"Add one": "Add one",
|
"Add one": "Add one",
|
||||||
"No Monitors": "No Monitors",
|
"No Monitors": "No Monitors",
|
||||||
"Untitled Group": "Untitled Group",
|
"Untitled Group": "Untitled Group",
|
||||||
Services: "Services",
|
"Services": "Services",
|
||||||
Discard: "Discard",
|
"Discard": "Discard",
|
||||||
Cancel: "Cancel",
|
"Cancel": "Cancel",
|
||||||
"Powered by": "Powered by",
|
"Powered by": "Powered by",
|
||||||
shrinkDatabaseDescription: "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
|
shrinkDatabaseDescription: "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
|
||||||
serwersms: "SerwerSMS.pl",
|
serwersms: "SerwerSMS.pl",
|
||||||
@ -352,7 +353,7 @@ export default {
|
|||||||
serwersmsAPIPassword: "API Password",
|
serwersmsAPIPassword: "API Password",
|
||||||
serwersmsPhoneNumber: "Phone number",
|
serwersmsPhoneNumber: "Phone number",
|
||||||
serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
|
serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
|
||||||
"stackfield": "Stackfield",
|
stackfield: "Stackfield",
|
||||||
smtpDkimSettings: "DKIM Settings",
|
smtpDkimSettings: "DKIM Settings",
|
||||||
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
|
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
|
||||||
documentation: "documentation",
|
documentation: "documentation",
|
||||||
@ -363,12 +364,13 @@ export default {
|
|||||||
smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
|
smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
|
||||||
smtpDkimskipFields: "Header Keys not to sign (Optional)",
|
smtpDkimskipFields: "Header Keys not to sign (Optional)",
|
||||||
gorush: "Gorush",
|
gorush: "Gorush",
|
||||||
alerta: 'Alerta',
|
alerta: "Alerta",
|
||||||
alertaApiEndpoint: 'API Endpoint',
|
alertaApiEndpoint: "API Endpoint",
|
||||||
alertaEnvironment: 'Environment',
|
alertaEnvironment: "Environment",
|
||||||
alertaApiKey: 'API Key',
|
alertaApiKey: "API Key",
|
||||||
alertaAlertState: 'Alert State',
|
alertaAlertState: "Alert State",
|
||||||
alertaRecoverState: 'Recover State',
|
alertaRecoverState: "Recover State",
|
||||||
|
deleteStatusPageMsg: "Are you sure want to delete this status page?",
|
||||||
Proxies: "Proxies",
|
Proxies: "Proxies",
|
||||||
default: "Default",
|
default: "Default",
|
||||||
enabled: "Enabled",
|
enabled: "Enabled",
|
||||||
|
@ -180,6 +180,7 @@ export default {
|
|||||||
"Edit Status Page": "Editar página de estado",
|
"Edit Status Page": "Editar página de estado",
|
||||||
"Go to Dashboard": "Ir al panel de control",
|
"Go to Dashboard": "Ir al panel de control",
|
||||||
"Status Page": "Página de estado",
|
"Status Page": "Página de estado",
|
||||||
|
"Status Pages": "Página de estado",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "Email (SMTP)",
|
smtp: "Email (SMTP)",
|
||||||
|
@ -17,6 +17,7 @@ export default {
|
|||||||
pauseMonitorMsg: "Kas soovid peatada seire?",
|
pauseMonitorMsg: "Kas soovid peatada seire?",
|
||||||
Settings: "Seaded",
|
Settings: "Seaded",
|
||||||
"Status Page": "Ülevaade",
|
"Status Page": "Ülevaade",
|
||||||
|
"Status Pages": "Ülevaated",
|
||||||
Dashboard: "Töölaud",
|
Dashboard: "Töölaud",
|
||||||
"New Update": "Uuem tarkvara versioon on saadaval.",
|
"New Update": "Uuem tarkvara versioon on saadaval.",
|
||||||
Language: "Keel",
|
Language: "Keel",
|
||||||
@ -197,4 +198,10 @@ export default {
|
|||||||
pushbullet: "Pushbullet",
|
pushbullet: "Pushbullet",
|
||||||
line: "LINE",
|
line: "LINE",
|
||||||
mattermost: "Mattermost",
|
mattermost: "Mattermost",
|
||||||
|
alerta: "Alerta",
|
||||||
|
alertaApiEndpoint: "API otsik",
|
||||||
|
alertaEnvironment: "Keskkond",
|
||||||
|
alertaApiKey: "API võti",
|
||||||
|
alertaAlertState: "Häireseisund",
|
||||||
|
alertaRecoverState: "Taasta algolek",
|
||||||
};
|
};
|
||||||
|
@ -178,6 +178,7 @@ export default {
|
|||||||
"Add a monitor": "اضافه کردن مانیتور",
|
"Add a monitor": "اضافه کردن مانیتور",
|
||||||
"Edit Status Page": "ویرایش صفحه وضعیت",
|
"Edit Status Page": "ویرایش صفحه وضعیت",
|
||||||
"Status Page": "صفحه وضعیت",
|
"Status Page": "صفحه وضعیت",
|
||||||
|
"Status Pages": "صفحه وضعیت",
|
||||||
"Go to Dashboard": "رفتن به پیشخوان",
|
"Go to Dashboard": "رفتن به پیشخوان",
|
||||||
"Uptime Kuma": "آپتایم کوما",
|
"Uptime Kuma": "آپتایم کوما",
|
||||||
records: "مورد",
|
records: "مورد",
|
||||||
|
@ -179,6 +179,7 @@ export default {
|
|||||||
"Edit Status Page": "Modifier la page de statut",
|
"Edit Status Page": "Modifier la page de statut",
|
||||||
"Go to Dashboard": "Accéder au tableau de bord",
|
"Go to Dashboard": "Accéder au tableau de bord",
|
||||||
"Status Page": "Status Page",
|
"Status Page": "Status Page",
|
||||||
|
"Status Pages": "Status Pages",
|
||||||
defaultNotificationName: "Ma notification {notification} numéro ({number})",
|
defaultNotificationName: "Ma notification {notification} numéro ({number})",
|
||||||
here: "ici",
|
here: "ici",
|
||||||
Required: "Requis",
|
Required: "Requis",
|
||||||
@ -304,9 +305,9 @@ export default {
|
|||||||
steamApiKeyDescription: "Pour surveiller un serveur Steam, vous avez besoin d'une clé Steam Web-API. Vous pouvez enregistrer votre clé ici : ",
|
steamApiKeyDescription: "Pour surveiller un serveur Steam, vous avez besoin d'une clé Steam Web-API. Vous pouvez enregistrer votre clé ici : ",
|
||||||
"Current User": "Utilisateur actuel",
|
"Current User": "Utilisateur actuel",
|
||||||
recent: "Récent",
|
recent: "Récent",
|
||||||
alertaApiEndpoint: 'API Endpoint',
|
alertaApiEndpoint: "API Endpoint",
|
||||||
alertaEnvironment: 'Environement',
|
alertaEnvironment: "Environement",
|
||||||
alertaApiKey: "Clé de l'API",
|
alertaApiKey: "Clé de l'API",
|
||||||
alertaAlertState: "État de l'Alerte",
|
alertaAlertState: "État de l'Alerte",
|
||||||
alertaRecoverState: 'État de récupération',
|
alertaRecoverState: "État de récupération",
|
||||||
};
|
};
|
||||||
|
@ -183,6 +183,7 @@ export default {
|
|||||||
"Edit Status Page": "Uredi Statusnu stranicu",
|
"Edit Status Page": "Uredi Statusnu stranicu",
|
||||||
"Go to Dashboard": "Na Kontrolnu ploču",
|
"Go to Dashboard": "Na Kontrolnu ploču",
|
||||||
"Status Page": "Statusna stranica",
|
"Status Page": "Statusna stranica",
|
||||||
|
"Status Pages": "Statusne stranice",
|
||||||
defaultNotificationName: "Moja {number}. {notification} obavijest",
|
defaultNotificationName: "Moja {number}. {notification} obavijest",
|
||||||
here: "ovdje",
|
here: "ovdje",
|
||||||
Required: "Potrebno",
|
Required: "Potrebno",
|
||||||
@ -346,4 +347,30 @@ export default {
|
|||||||
Cancel: "Otkaži",
|
Cancel: "Otkaži",
|
||||||
"Powered by": "Pokreće",
|
"Powered by": "Pokreće",
|
||||||
Saved: "Spremljeno",
|
Saved: "Spremljeno",
|
||||||
|
PushByTechulus: "Push by Techulus",
|
||||||
|
GoogleChat: "Google Chat (preko platforme Google Workspace)",
|
||||||
|
shrinkDatabaseDescription: "Pokreni VACUUM operaciju za SQLite. Ako je baza podataka kreirana nakon inačice 1.10.0, AUTO_VACUUM opcija već je uključena te ova akcija nije nužna.",
|
||||||
|
serwersms: "SerwerSMS.pl",
|
||||||
|
serwersmsAPIUser: "API korisničko ime (uključujući webapi_ prefiks)",
|
||||||
|
serwersmsAPIPassword: "API lozinka",
|
||||||
|
serwersmsPhoneNumber: "Broj telefona",
|
||||||
|
serwersmsSenderName: "Ime SMS pošiljatelja (registrirano preko korisničkog portala)",
|
||||||
|
stackfield: "Stackfield",
|
||||||
|
smtpDkimSettings: "DKIM postavke",
|
||||||
|
smtpDkimDesc: "Za više informacija, postoji Nodemailer DKIM {0}.",
|
||||||
|
documentation: "dokumentacija",
|
||||||
|
smtpDkimDomain: "Domena",
|
||||||
|
smtpDkimKeySelector: "Odabir ključa",
|
||||||
|
smtpDkimPrivateKey: "Privatni ključ",
|
||||||
|
smtpDkimHashAlgo: "Hash algoritam (neobavezno)",
|
||||||
|
smtpDkimheaderFieldNames: "Ključevi zaglavlja za potpis (neobavezno)",
|
||||||
|
smtpDkimskipFields: "Ključevi zaglavlja koji se neće potpisati (neobavezno)",
|
||||||
|
gorush: "Gorush",
|
||||||
|
alerta: "Alerta",
|
||||||
|
alertaApiEndpoint: "Krajnja točka API-ja (Endpoint)",
|
||||||
|
alertaEnvironment: "Okruženje (Environment)",
|
||||||
|
alertaApiKey: "API ključ",
|
||||||
|
alertaAlertState: "Stanje upozorenja",
|
||||||
|
alertaRecoverState: "Stanje oporavka",
|
||||||
|
deleteStatusPageMsg: "Sigurno želite obrisati ovu statusnu stranicu?",
|
||||||
};
|
};
|
||||||
|
@ -197,6 +197,7 @@ export default {
|
|||||||
line: "Line Messenger",
|
line: "Line Messenger",
|
||||||
mattermost: "Mattermost",
|
mattermost: "Mattermost",
|
||||||
"Status Page": "Státusz oldal",
|
"Status Page": "Státusz oldal",
|
||||||
|
"Status Pages": "Státusz oldal",
|
||||||
"Primary Base URL": "Elsődleges URL",
|
"Primary Base URL": "Elsődleges URL",
|
||||||
"Push URL": "Meghívandó URL",
|
"Push URL": "Meghívandó URL",
|
||||||
needPushEvery: "Ezt az URL-t kell meghívni minden {0} másodpercben.",
|
needPushEvery: "Ezt az URL-t kell meghívni minden {0} másodpercben.",
|
||||||
@ -361,4 +362,12 @@ export default {
|
|||||||
smtpDkimHashAlgo: "Hash algoritmus (nem kötelező)",
|
smtpDkimHashAlgo: "Hash algoritmus (nem kötelező)",
|
||||||
smtpDkimheaderFieldNames: "Fejléc kulcsok a bejelentkezéshez (nem kötelező)",
|
smtpDkimheaderFieldNames: "Fejléc kulcsok a bejelentkezéshez (nem kötelező)",
|
||||||
smtpDkimskipFields: "Fejléc kulcsok egyéb esetben (nem kötelező)",
|
smtpDkimskipFields: "Fejléc kulcsok egyéb esetben (nem kötelező)",
|
||||||
|
PushByTechulus: "Techulus push",
|
||||||
|
gorush: "Gorush",
|
||||||
|
alerta: "Alerta",
|
||||||
|
alertaApiEndpoint: "API végpont",
|
||||||
|
alertaEnvironment: "Környezet",
|
||||||
|
alertaApiKey: "API kulcs",
|
||||||
|
alertaAlertState: "Figyelmeztetési állapot",
|
||||||
|
alertaRecoverState: "Visszaállási állapot",
|
||||||
};
|
};
|
||||||
|
@ -179,6 +179,7 @@ export default {
|
|||||||
"Edit Status Page": "Edit Halaman Status",
|
"Edit Status Page": "Edit Halaman Status",
|
||||||
"Go to Dashboard": "Pergi ke Dasbor",
|
"Go to Dashboard": "Pergi ke Dasbor",
|
||||||
"Status Page": "Halaman Status",
|
"Status Page": "Halaman Status",
|
||||||
|
"Status Pages": "Halaman Status",
|
||||||
defaultNotificationName: "{notification} saya Peringatan ({number})",
|
defaultNotificationName: "{notification} saya Peringatan ({number})",
|
||||||
here: "di sini",
|
here: "di sini",
|
||||||
Required: "Dibutuhkan",
|
Required: "Dibutuhkan",
|
||||||
|
@ -183,6 +183,7 @@ export default {
|
|||||||
"Edit Status Page": "Modifica pagina di stato",
|
"Edit Status Page": "Modifica pagina di stato",
|
||||||
"Go to Dashboard": "Vai alla dashboard",
|
"Go to Dashboard": "Vai alla dashboard",
|
||||||
"Status Page": "Pagina di stato",
|
"Status Page": "Pagina di stato",
|
||||||
|
"Status Pages": "Pagina di stato",
|
||||||
defaultNotificationName: "Notifica {notification} ({number})",
|
defaultNotificationName: "Notifica {notification} ({number})",
|
||||||
here: "qui",
|
here: "qui",
|
||||||
Required: "Obbligatorio",
|
Required: "Obbligatorio",
|
||||||
|
@ -180,6 +180,7 @@ export default {
|
|||||||
"Edit Status Page": "ステータスページ編集",
|
"Edit Status Page": "ステータスページ編集",
|
||||||
"Go to Dashboard": "ダッシュボード",
|
"Go to Dashboard": "ダッシュボード",
|
||||||
"Status Page": "ステータスページ",
|
"Status Page": "ステータスページ",
|
||||||
|
"Status Pages": "ステータスページ",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "Email (SMTP)",
|
smtp: "Email (SMTP)",
|
||||||
|
@ -179,6 +179,7 @@ export default {
|
|||||||
"Edit Status Page": "상태 페이지 수정",
|
"Edit Status Page": "상태 페이지 수정",
|
||||||
"Go to Dashboard": "대시보드로 가기",
|
"Go to Dashboard": "대시보드로 가기",
|
||||||
"Status Page": "상태 페이지",
|
"Status Page": "상태 페이지",
|
||||||
|
"Status Pages": "상태 페이지",
|
||||||
defaultNotificationName: "내 {notification} 알림 ({number})",
|
defaultNotificationName: "내 {notification} 알림 ({number})",
|
||||||
here: "여기",
|
here: "여기",
|
||||||
Required: "필수",
|
Required: "필수",
|
||||||
@ -188,7 +189,7 @@ export default {
|
|||||||
"Chat ID": "채팅 ID",
|
"Chat ID": "채팅 ID",
|
||||||
supportTelegramChatID: "Direct Chat / Group / Channel's Chat ID를 지원해요.",
|
supportTelegramChatID: "Direct Chat / Group / Channel's Chat ID를 지원해요.",
|
||||||
wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.",
|
wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.",
|
||||||
"YOUR BOT TOKEN HERE": "YOUR BOT TOKEN HERE",
|
"YOUR BOT TOKEN HERE": "여기에 BOT 토큰을 적어주세요.",
|
||||||
chatIDNotFound: "채팅 ID를 찾을 수 없어요. 먼저 봇에게 메시지를 보내주세요.",
|
chatIDNotFound: "채팅 ID를 찾을 수 없어요. 먼저 봇에게 메시지를 보내주세요.",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
"Post URL": "Post URL",
|
"Post URL": "Post URL",
|
||||||
@ -281,15 +282,15 @@ export default {
|
|||||||
promosmsSMSSender: "SMS 보내는 사람 이름 : 미리 등록된 이름 혹은 기본값 중 하나예요: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
|
promosmsSMSSender: "SMS 보내는 사람 이름 : 미리 등록된 이름 혹은 기본값 중 하나예요: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
|
||||||
"Primary Base URL": "기본 URL",
|
"Primary Base URL": "기본 URL",
|
||||||
"Push URL": "Push URL",
|
"Push URL": "Push URL",
|
||||||
needPushEvery: "You should call this URL every {0} seconds.",
|
needPushEvery: "이 URL을 {0} 초 마다 호출할 수 있어요.",
|
||||||
pushOptionalParams: "Optional parameters: {0}",
|
pushOptionalParams: "선택적 파라미터: {0}",
|
||||||
emailCustomSubject: "Custom Subject",
|
emailCustomSubject: "커스텀 주제",
|
||||||
clicksendsms: "ClickSend SMS",
|
clicksendsms: "ClickSend SMS",
|
||||||
checkPrice: "{0} 가격 확인:",
|
checkPrice: "{0} 가격 확인:",
|
||||||
apiCredentials: "API credentials",
|
apiCredentials: "API 인증정보",
|
||||||
octopushLegacyHint: "Octopush 레거시 버전 (2011-2020) 을 사용하시나요? 아니면 새 버전을 사용하시나요?",
|
octopushLegacyHint: "Octopush 레거시 버전 (2011-2020) 을 사용하시나요? 아니면 새 버전을 사용하시나요?",
|
||||||
"Feishu WebHookUrl": "Feishu WebHookURL",
|
"Feishu WebHookUrl": "Feishu WebHookURL",
|
||||||
matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)",
|
matrixHomeserverURL: "Homeserver URL (http(s):// 와 함께 적어주세요. 그리고 포트 번호는 선택적 입니다.)",
|
||||||
"Internal Room Id": "내부 방 ID",
|
"Internal Room Id": "내부 방 ID",
|
||||||
matrixDesc1: "Matrix 클라이언트 방 설정의 고급 섹션에서 내부 방 ID를 찾을 수 있어요. 내부 방 ID는 이렇게 생겼답니다: !QMdRCpUIfLwsfjxye6:home.server.",
|
matrixDesc1: "Matrix 클라이언트 방 설정의 고급 섹션에서 내부 방 ID를 찾을 수 있어요. 내부 방 ID는 이렇게 생겼답니다: !QMdRCpUIfLwsfjxye6:home.server.",
|
||||||
matrixDesc2: "사용자의 모든 방에 대한 엑세스가 허용될 수 있어서 새로운 사용자를 만들고 원하는 방에만 초대한 후 엑세스 토큰을 사용하는 것이 좋아요. {0} 이 명령어를 통해 엑세스 토큰을 얻을 수 있어요.",
|
matrixDesc2: "사용자의 모든 방에 대한 엑세스가 허용될 수 있어서 새로운 사용자를 만들고 원하는 방에만 초대한 후 엑세스 토큰을 사용하는 것이 좋아요. {0} 이 명령어를 통해 엑세스 토큰을 얻을 수 있어요.",
|
||||||
@ -349,6 +350,6 @@ export default {
|
|||||||
serwersmsAPIUser: "API Usename (webapi_ 접두사 포함)",
|
serwersmsAPIUser: "API Usename (webapi_ 접두사 포함)",
|
||||||
serwersmsAPIPassword: "API 비밀번호",
|
serwersmsAPIPassword: "API 비밀번호",
|
||||||
serwersmsPhoneNumber: "휴대전화 번호",
|
serwersmsPhoneNumber: "휴대전화 번호",
|
||||||
serwersmsSenderName: "보내는 사람 이름 (registered via customer portal)",
|
serwersmsSenderName: "보내는 사람 이름 (customer portal를 통해 가입된 정보)",
|
||||||
stackfield: "Stackfield",
|
stackfield: "Stackfield",
|
||||||
};
|
};
|
||||||
|
@ -179,6 +179,7 @@ export default {
|
|||||||
"Edit Status Page": "Rediger statusside",
|
"Edit Status Page": "Rediger statusside",
|
||||||
"Go to Dashboard": "Gå til Dashboard",
|
"Go to Dashboard": "Gå til Dashboard",
|
||||||
"Status Page": "Statusside",
|
"Status Page": "Statusside",
|
||||||
|
"Status Pages": "Statusside",
|
||||||
defaultNotificationName: "Min {notification} varsling ({number})",
|
defaultNotificationName: "Min {notification} varsling ({number})",
|
||||||
here: "her",
|
here: "her",
|
||||||
Required: "Obligatorisk",
|
Required: "Obligatorisk",
|
||||||
|
@ -180,6 +180,7 @@ export default {
|
|||||||
"Edit Status Page": "Wijzig status pagina",
|
"Edit Status Page": "Wijzig status pagina",
|
||||||
"Go to Dashboard": "Ga naar Dashboard",
|
"Go to Dashboard": "Ga naar Dashboard",
|
||||||
"Status Page": "Status Pagina",
|
"Status Page": "Status Pagina",
|
||||||
|
"Status Pages": "Status Pagina",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "Email (SMTP)",
|
smtp: "Email (SMTP)",
|
||||||
|
@ -179,6 +179,7 @@ export default {
|
|||||||
"Edit Status Page": "Edytuj stronę statusu",
|
"Edit Status Page": "Edytuj stronę statusu",
|
||||||
"Go to Dashboard": "Idź do panelu",
|
"Go to Dashboard": "Idź do panelu",
|
||||||
"Status Page": "Strona statusu",
|
"Status Page": "Strona statusu",
|
||||||
|
"Status Pages": "Strona statusu",
|
||||||
defaultNotificationName: "Moje powiadomienie {notification} ({number})",
|
defaultNotificationName: "Moje powiadomienie {notification} ({number})",
|
||||||
here: "tutaj",
|
here: "tutaj",
|
||||||
Required: "Wymagane",
|
Required: "Wymagane",
|
||||||
|
@ -169,6 +169,7 @@ export default {
|
|||||||
"Avg. Ping": "Ping Médio.",
|
"Avg. Ping": "Ping Médio.",
|
||||||
"Avg. Response": "Resposta Média. ",
|
"Avg. Response": "Resposta Média. ",
|
||||||
"Status Page": "Página de Status",
|
"Status Page": "Página de Status",
|
||||||
|
"Status Pages": "Página de Status",
|
||||||
"Entry Page": "Página de entrada",
|
"Entry Page": "Página de entrada",
|
||||||
statusPageNothing: "Nada aqui, por favor, adicione um grupo ou monitor.",
|
statusPageNothing: "Nada aqui, por favor, adicione um grupo ou monitor.",
|
||||||
"No Services": "Nenhum Serviço",
|
"No Services": "Nenhum Serviço",
|
||||||
|
@ -180,7 +180,8 @@ export default {
|
|||||||
"Add a monitor": "Добавить монитор",
|
"Add a monitor": "Добавить монитор",
|
||||||
"Edit Status Page": "Редактировать",
|
"Edit Status Page": "Редактировать",
|
||||||
"Go to Dashboard": "Панель управления",
|
"Go to Dashboard": "Панель управления",
|
||||||
"Status Page": "Мониторинг",
|
"Status Page": "Страница статуса",
|
||||||
|
"Status Pages": "Страницы статуса",
|
||||||
Discard: "Отмена",
|
Discard: "Отмена",
|
||||||
"Create Incident": "Создать инцидент",
|
"Create Incident": "Создать инцидент",
|
||||||
"Switch to Dark Theme": "Тёмная тема",
|
"Switch to Dark Theme": "Тёмная тема",
|
||||||
@ -310,28 +311,82 @@ export default {
|
|||||||
"One record": "Одна запись",
|
"One record": "Одна запись",
|
||||||
steamApiKeyDescription: "Для мониторинга игрового сервера Steam вам необходим Web-API ключ Steam. Зарегистрировать его можно здесь: ",
|
steamApiKeyDescription: "Для мониторинга игрового сервера Steam вам необходим Web-API ключ Steam. Зарегистрировать его можно здесь: ",
|
||||||
"Certificate Chain": "Цепочка сертификатов",
|
"Certificate Chain": "Цепочка сертификатов",
|
||||||
"Valid": "Действительный",
|
Valid: "Действительный",
|
||||||
"Hide Tags": "Скрыть тэги",
|
"Hide Tags": "Скрыть тэги",
|
||||||
"Title": "Название инцидента:",
|
Title: "Название инцидента:",
|
||||||
"Content": "Содержание инцидента:",
|
Content: "Содержание инцидента:",
|
||||||
"Post": "Опубликовать",
|
Post: "Опубликовать",
|
||||||
"Cancel": "Отмена",
|
Cancel: "Отмена",
|
||||||
"Created": "Создано",
|
Created: "Создано",
|
||||||
"Unpin": "Открепить",
|
Unpin: "Открепить",
|
||||||
"Show Tags": "Показать тэги",
|
"Show Tags": "Показать тэги",
|
||||||
"recent": "Сейчас",
|
recent: "Сейчас",
|
||||||
"3h": "3 часа",
|
"3h": "3 часа",
|
||||||
"6h": "6 часов",
|
"6h": "6 часов",
|
||||||
"24h": "24 часа",
|
"24h": "24 часа",
|
||||||
"1w": "1 неделя",
|
"1w": "1 неделя",
|
||||||
"No monitors available.": "Нет доступных мониторов",
|
"No monitors available.": "Нет доступных мониторов",
|
||||||
"Add one": "Добавить новый",
|
"Add one": "Добавить новый",
|
||||||
"Backup": "Резервная копия",
|
Backup: "Резервная копия",
|
||||||
"Security": "Безопасность",
|
Security: "Безопасность",
|
||||||
"Shrink Database": "Сжать Базу Данных",
|
"Shrink Database": "Сжать Базу Данных",
|
||||||
"Current User": "Текущий пользователь",
|
"Current User": "Текущий пользователь",
|
||||||
"About": "О программе",
|
About: "О программе",
|
||||||
"Description": "Описание",
|
Description: "Описание",
|
||||||
"Powered by": "Работает на основе скрипта от",
|
"Powered by": "Работает на основе скрипта от",
|
||||||
shrinkDatabaseDescription: "Включает VACUUM для базы данных SQLite. Если ваша база данных была создана на версии 1.10.0 и более, AUTO_VACUUM уже включен и это действие не требуется.",
|
shrinkDatabaseDescription: "Включает VACUUM для базы данных SQLite. Если ваша база данных была создана на версии 1.10.0 и более, AUTO_VACUUM уже включен и это действие не требуется.",
|
||||||
|
deleteStatusPageMsg: "Вы действительно хотите удалить эту страницу статуса сервисов?",
|
||||||
|
Style: "Стиль",
|
||||||
|
info: "ИНФО",
|
||||||
|
warning: "ВНИМАНИЕ",
|
||||||
|
danger: "ОШИБКА",
|
||||||
|
primary: "ОСНОВНОЙ",
|
||||||
|
light: "СВЕТЛЫЙ",
|
||||||
|
dark: "ТЕМНЫЙ",
|
||||||
|
"New Status Page": "Новая страница статуса",
|
||||||
|
"Show update if available": "Показывать доступные обновления",
|
||||||
|
"Also check beta release": "Проверять обновления для бета версий",
|
||||||
|
"Add New Status Page": "Добавить страницу статуса",
|
||||||
|
Next: "Далее",
|
||||||
|
"Accept characters: a-z 0-9 -": "Разрешены символы: a-z 0-9 -",
|
||||||
|
"Start or end with a-z 0-9 only": "Начало и окончание имени только на символы: a-z 0-9",
|
||||||
|
"No consecutive dashes --": "Запрещено использовать тире --",
|
||||||
|
"HTTP Options": "HTTP Опции",
|
||||||
|
"Basic Auth": "HTTP Авторизация",
|
||||||
|
PushByTechulus: "Push by Techulus",
|
||||||
|
clicksendsms: "ClickSend SMS",
|
||||||
|
GoogleChat: "Google Chat (только Google Workspace)",
|
||||||
|
apiCredentials: "API реквизиты",
|
||||||
|
Done: "Готово",
|
||||||
|
Info: "Инфо",
|
||||||
|
"Steam API Key": "Steam API-Ключ",
|
||||||
|
"Pick a RR-Type...": "Выберите RR-Тип...",
|
||||||
|
"Pick Accepted Status Codes...": "Выберите принятые коды состояния...",
|
||||||
|
Default: "По умолчанию",
|
||||||
|
"Please input title and content": "Пожалуйста, введите название и содержание",
|
||||||
|
"Last Updated": "Последнее Обновление",
|
||||||
|
"Untitled Group": "Группа без названия",
|
||||||
|
Services: "Сервисы",
|
||||||
|
serwersms: "SerwerSMS.pl",
|
||||||
|
serwersmsAPIUser: "API Пользователь (включая префикс webapi_)",
|
||||||
|
serwersmsAPIPassword: "API Пароль",
|
||||||
|
serwersmsPhoneNumber: "Номер телефона",
|
||||||
|
serwersmsSenderName: "SMS Имя Отправителя (регистрированный через пользовательский портал)",
|
||||||
|
stackfield: "Stackfield",
|
||||||
|
smtpDkimSettings: "DKIM Настройки",
|
||||||
|
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
|
||||||
|
documentation: "документация",
|
||||||
|
smtpDkimDomain: "Имя Домена",
|
||||||
|
smtpDkimKeySelector: "Ключ",
|
||||||
|
smtpDkimPrivateKey: "Приватный ключ",
|
||||||
|
smtpDkimHashAlgo: "Алгоритм хэша (опционально)",
|
||||||
|
smtpDkimheaderFieldNames: "Заголовок ключей для подписи (опционально)",
|
||||||
|
smtpDkimskipFields: "Заколовок ключей не для подписи (опционально)",
|
||||||
|
gorush: "Gorush",
|
||||||
|
alerta: "Alerta",
|
||||||
|
alertaApiEndpoint: "Конечная точка API",
|
||||||
|
alertaEnvironment: "Среда",
|
||||||
|
alertaApiKey: "Ключ API",
|
||||||
|
alertaAlertState: "Состояние алерта",
|
||||||
|
alertaRecoverState: "Состояние восстановления",
|
||||||
};
|
};
|
||||||
|
@ -182,7 +182,8 @@ export default {
|
|||||||
"Add a monitor": "Dodaj monitor",
|
"Add a monitor": "Dodaj monitor",
|
||||||
"Edit Status Page": "Uredi statusno stran",
|
"Edit Status Page": "Uredi statusno stran",
|
||||||
"Go to Dashboard": "Pojdi na nadzorno ploščo",
|
"Go to Dashboard": "Pojdi na nadzorno ploščo",
|
||||||
"Status Page": "Status",
|
"Status Page": "Página de Status",
|
||||||
|
"Status Pages": "Página de Status",
|
||||||
defaultNotificationName: "Moje {notification} Obvestilo ({number})",
|
defaultNotificationName: "Moje {notification} Obvestilo ({number})",
|
||||||
here: "tukaj",
|
here: "tukaj",
|
||||||
Required: "Obvezno",
|
Required: "Obvezno",
|
||||||
|
@ -180,6 +180,7 @@ export default {
|
|||||||
"Edit Status Page": "Edit Status Page",
|
"Edit Status Page": "Edit Status Page",
|
||||||
"Go to Dashboard": "Go to Dashboard",
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
"Status Page": "Status Page",
|
"Status Page": "Status Page",
|
||||||
|
"Status Pages": "Status Pages",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "Email (SMTP)",
|
smtp: "Email (SMTP)",
|
||||||
|
@ -180,6 +180,7 @@ export default {
|
|||||||
"Edit Status Page": "Edit Status Page",
|
"Edit Status Page": "Edit Status Page",
|
||||||
"Go to Dashboard": "Go to Dashboard",
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
"Status Page": "Status Page",
|
"Status Page": "Status Page",
|
||||||
|
"Status Pages": "Status Pages",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "Email (SMTP)",
|
smtp: "Email (SMTP)",
|
||||||
|
@ -108,94 +108,4 @@ export default {
|
|||||||
"Repeat Password": "Upprepa Lösenord",
|
"Repeat Password": "Upprepa Lösenord",
|
||||||
respTime: "Svarstid (ms)",
|
respTime: "Svarstid (ms)",
|
||||||
notAvailableShort: "Ej Tillg.",
|
notAvailableShort: "Ej Tillg.",
|
||||||
Create: "Create",
|
|
||||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
|
||||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
|
||||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
|
||||||
"Clear Data": "Clear Data",
|
|
||||||
Events: "Events",
|
|
||||||
Heartbeats: "Heartbeats",
|
|
||||||
"Auto Get": "Auto Get",
|
|
||||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
|
||||||
"Default enabled": "Default enabled",
|
|
||||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
|
||||||
Export: "Export",
|
|
||||||
Import: "Import",
|
|
||||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
|
||||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
|
||||||
alertNoFile: "Please select a file to import.",
|
|
||||||
alertWrongFileType: "Please select a JSON file.",
|
|
||||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
|
||||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
|
||||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
|
||||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
|
||||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
|
||||||
"Verify Token": "Verify Token",
|
|
||||||
"Setup 2FA": "Setup 2FA",
|
|
||||||
"Enable 2FA": "Enable 2FA",
|
|
||||||
"Disable 2FA": "Disable 2FA",
|
|
||||||
"2FA Settings": "2FA Settings",
|
|
||||||
"Two Factor Authentication": "Two Factor Authentication",
|
|
||||||
Active: "Active",
|
|
||||||
Inactive: "Inactive",
|
|
||||||
Token: "Token",
|
|
||||||
"Show URI": "Show URI",
|
|
||||||
"Clear all statistics": "Clear all Statistics",
|
|
||||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
|
||||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
|
||||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
|
||||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
|
||||||
"Import Backup": "Import Backup",
|
|
||||||
"Export Backup": "Export Backup",
|
|
||||||
"Skip existing": "Skip existing",
|
|
||||||
Overwrite: "Overwrite",
|
|
||||||
Options: "Options",
|
|
||||||
"Keep both": "Keep both",
|
|
||||||
Tags: "Tags",
|
|
||||||
"Add New below or Select...": "Add New below or Select...",
|
|
||||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
|
||||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
|
||||||
color: "color",
|
|
||||||
"value (optional)": "value (optional)",
|
|
||||||
Gray: "Gray",
|
|
||||||
Red: "Red",
|
|
||||||
Orange: "Orange",
|
|
||||||
Green: "Green",
|
|
||||||
Blue: "Blue",
|
|
||||||
Indigo: "Indigo",
|
|
||||||
Purple: "Purple",
|
|
||||||
Pink: "Pink",
|
|
||||||
"Search...": "Search...",
|
|
||||||
"Avg. Ping": "Avg. Ping",
|
|
||||||
"Avg. Response": "Avg. Response",
|
|
||||||
"Entry Page": "Entry Page",
|
|
||||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
|
||||||
"No Services": "No Services",
|
|
||||||
"All Systems Operational": "All Systems Operational",
|
|
||||||
"Partially Degraded Service": "Partially Degraded Service",
|
|
||||||
"Degraded Service": "Degraded Service",
|
|
||||||
"Add Group": "Add Group",
|
|
||||||
"Add a monitor": "Add a monitor",
|
|
||||||
"Edit Status Page": "Edit Status Page",
|
|
||||||
"Go to Dashboard": "Go to Dashboard",
|
|
||||||
"Status Page": "Status Page",
|
|
||||||
telegram: "Telegram",
|
|
||||||
webhook: "Webhook",
|
|
||||||
smtp: "Email (SMTP)",
|
|
||||||
discord: "Discord",
|
|
||||||
teams: "Microsoft Teams",
|
|
||||||
signal: "Signal",
|
|
||||||
gotify: "Gotify",
|
|
||||||
slack: "Slack",
|
|
||||||
"rocket.chat": "Rocket.chat",
|
|
||||||
pushover: "Pushover",
|
|
||||||
pushy: "Pushy",
|
|
||||||
octopush: "Octopush",
|
|
||||||
promosms: "PromoSMS",
|
|
||||||
lunasea: "LunaSea",
|
|
||||||
apprise: "Apprise (Support 50+ Notification services)",
|
|
||||||
pushbullet: "Pushbullet",
|
|
||||||
line: "Line Messenger",
|
|
||||||
mattermost: "Mattermost",
|
|
||||||
};
|
};
|
||||||
|
@ -124,7 +124,7 @@ export default {
|
|||||||
tokenValidSettingsMsg: "Token geçerli! Şimdi 2FA ayarlarını kaydedebilirsiniz. ",
|
tokenValidSettingsMsg: "Token geçerli! Şimdi 2FA ayarlarını kaydedebilirsiniz. ",
|
||||||
confirmEnableTwoFAMsg: "2FA'ı etkinleştirmek istediğinizden emin misiniz?",
|
confirmEnableTwoFAMsg: "2FA'ı etkinleştirmek istediğinizden emin misiniz?",
|
||||||
confirmDisableTwoFAMsg: "2FA'ı devre dışı bırakmak istediğinize emin misiniz?",
|
confirmDisableTwoFAMsg: "2FA'ı devre dışı bırakmak istediğinize emin misiniz?",
|
||||||
"Heartbeat Retry Interval": "Sağlık Dırımları Tekrar Deneme Sıklığı",
|
"Heartbeat Retry Interval": "Sağlık Durumları Tekrar Deneme Sıklığı",
|
||||||
"Import Backup": "Yedeği içe aktar",
|
"Import Backup": "Yedeği içe aktar",
|
||||||
"Export Backup": "Yedeği dışa aktar",
|
"Export Backup": "Yedeği dışa aktar",
|
||||||
Export: "Dışa aktar",
|
Export: "Dışa aktar",
|
||||||
@ -149,52 +149,4 @@ export default {
|
|||||||
"Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)",
|
"Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)",
|
||||||
Active: "Aktif",
|
Active: "Aktif",
|
||||||
Inactive: "İnaktif",
|
Inactive: "İnaktif",
|
||||||
Token: "Token",
|
|
||||||
"Show URI": "Show URI",
|
|
||||||
Tags: "Tags",
|
|
||||||
"Add New below or Select...": "Add New below or Select...",
|
|
||||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
|
||||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
|
||||||
color: "color",
|
|
||||||
"value (optional)": "value (optional)",
|
|
||||||
Gray: "Gray",
|
|
||||||
Red: "Red",
|
|
||||||
Orange: "Orange",
|
|
||||||
Green: "Green",
|
|
||||||
Blue: "Blue",
|
|
||||||
Indigo: "Indigo",
|
|
||||||
Purple: "Purple",
|
|
||||||
Pink: "Pink",
|
|
||||||
"Search...": "Search...",
|
|
||||||
"Avg. Ping": "Avg. Ping",
|
|
||||||
"Avg. Response": "Avg. Response",
|
|
||||||
"Entry Page": "Entry Page",
|
|
||||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
|
||||||
"No Services": "No Services",
|
|
||||||
"All Systems Operational": "All Systems Operational",
|
|
||||||
"Partially Degraded Service": "Partially Degraded Service",
|
|
||||||
"Degraded Service": "Degraded Service",
|
|
||||||
"Add Group": "Add Group",
|
|
||||||
"Add a monitor": "Add a monitor",
|
|
||||||
"Edit Status Page": "Edit Status Page",
|
|
||||||
"Go to Dashboard": "Go to Dashboard",
|
|
||||||
"Status Page": "Status Page",
|
|
||||||
telegram: "Telegram",
|
|
||||||
webhook: "Webhook",
|
|
||||||
smtp: "Email (SMTP)",
|
|
||||||
discord: "Discord",
|
|
||||||
teams: "Microsoft Teams",
|
|
||||||
signal: "Signal",
|
|
||||||
gotify: "Gotify",
|
|
||||||
slack: "Slack",
|
|
||||||
"rocket.chat": "Rocket.chat",
|
|
||||||
pushover: "Pushover",
|
|
||||||
pushy: "Pushy",
|
|
||||||
octopush: "Octopush",
|
|
||||||
promosms: "PromoSMS",
|
|
||||||
lunasea: "LunaSea",
|
|
||||||
apprise: "Apprise (Support 50+ Notification services)",
|
|
||||||
pushbullet: "Pushbullet",
|
|
||||||
line: "Line Messenger",
|
|
||||||
mattermost: "Mattermost",
|
|
||||||
};
|
};
|
||||||
|
392
src/languages/uk-UA.js
Normal file
392
src/languages/uk-UA.js
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
export default {
|
||||||
|
languageName: "Український",
|
||||||
|
checkEverySecond: "Перевірка кожні {0} секунд",
|
||||||
|
retriesDescription: "Максимальна кількість спроб перед позначенням сервісу як недоступного та надсиланням повідомлення",
|
||||||
|
ignoreTLSError: "Ігнорувати помилку TLS/SSL для сайтів HTTPS",
|
||||||
|
upsideDownModeDescription: "Реверс статусу сервісу. Якщо сервіс доступний, він позначається як НЕДОСТУПНИЙ.",
|
||||||
|
maxRedirectDescription: "Максимальна кількість перенаправлень. Поставте 0, щоб вимкнути перенаправлення.",
|
||||||
|
acceptedStatusCodesDescription: "Виберіть коди статусів для визначення доступності сервісу.",
|
||||||
|
passwordNotMatchMsg: "Повторення паролю не збігається.",
|
||||||
|
notificationDescription: "Прив'яжіть повідомлення до моніторів.",
|
||||||
|
keywordDescription: "Пошук слова в чистому HTML або JSON-відповіді (чутливо до регістру)",
|
||||||
|
pauseDashboardHome: "Пауза",
|
||||||
|
deleteMonitorMsg: "Ви дійсно хочете видалити цей монітор?",
|
||||||
|
deleteNotificationMsg: "Ви дійсно хочете видалити це повідомлення для всіх моніторів?",
|
||||||
|
resolverserverDescription: "Cloudflare є сервером за замовчуванням. Ви завжди можете змінити цей сервер.",
|
||||||
|
rrtypeDescription: "Виберіть тип ресурсного запису, який ви хочете відстежувати",
|
||||||
|
pauseMonitorMsg: "Ви дійсно хочете поставити на паузу?",
|
||||||
|
Settings: "Налаштування",
|
||||||
|
Dashboard: "Панель управління",
|
||||||
|
"New Update": "Оновлення",
|
||||||
|
Language: "Мова",
|
||||||
|
Appearance: "Зовнішній вигляд",
|
||||||
|
Theme: "Тема",
|
||||||
|
General: "Загальне",
|
||||||
|
Version: "Версія",
|
||||||
|
"Check Update On GitHub": "Перевірити оновлення на GitHub",
|
||||||
|
List: "Список",
|
||||||
|
Add: "Додати",
|
||||||
|
"Add New Monitor": "Новий монітор",
|
||||||
|
"Quick Stats": "Статистика",
|
||||||
|
Up: "Доступний",
|
||||||
|
Down: "Недоступний",
|
||||||
|
Pending: "Очікування",
|
||||||
|
Unknown: "Невідомо",
|
||||||
|
Pause: "Пауза",
|
||||||
|
Name: "Ім'я",
|
||||||
|
Status: "Статус",
|
||||||
|
DateTime: "Дата і час",
|
||||||
|
Message: "Повідомлення",
|
||||||
|
"No important events": "Важливих подій немає",
|
||||||
|
Resume: "Відновити",
|
||||||
|
Edit: "Змінити",
|
||||||
|
Delete: "Видалити",
|
||||||
|
Current: "Поточний",
|
||||||
|
Uptime: "Аптайм",
|
||||||
|
"Cert Exp.": "Сертифікат спливає",
|
||||||
|
days: "днів",
|
||||||
|
day: "день",
|
||||||
|
"-day": " днів",
|
||||||
|
hour: "година",
|
||||||
|
"-hour": " години",
|
||||||
|
Response: "Відповідь",
|
||||||
|
Ping: "Пінг",
|
||||||
|
"Monitor Type": "Тип монітора",
|
||||||
|
Keyword: "Ключове слово",
|
||||||
|
"Friendly Name": "Ім'я",
|
||||||
|
URL: "URL",
|
||||||
|
Hostname: "Ім'я хоста",
|
||||||
|
Port: "Порт",
|
||||||
|
"Heartbeat Interval": "Частота опитування",
|
||||||
|
Retries: "Спроб",
|
||||||
|
Advanced: "Додатково",
|
||||||
|
"Upside Down Mode": "Реверс статусу",
|
||||||
|
"Max. Redirects": "Макс. кількість перенаправлень",
|
||||||
|
"Accepted Status Codes": "Припустимі коди статусу",
|
||||||
|
Save: "Зберегти",
|
||||||
|
Notifications: "Повідомлення",
|
||||||
|
"Not available, please setup.": "Доступних сповіщень немає, необхідно створити.",
|
||||||
|
"Setup Notification": "Створити сповіщення",
|
||||||
|
Light: "Світла",
|
||||||
|
Dark: "Темна",
|
||||||
|
Auto: "Авто",
|
||||||
|
"Theme - Heartbeat Bar": "Тема - Смуга частоти опитування",
|
||||||
|
Normal: "Звичайний",
|
||||||
|
Bottom: "Знизу",
|
||||||
|
None: "Відсутня",
|
||||||
|
Timezone: "Часовий пояс",
|
||||||
|
"Search Engine Visibility": "Індексація пошуковими системами:",
|
||||||
|
"Allow indexing": "Дозволити індексування",
|
||||||
|
"Discourage search engines from indexing site": "Заборонити індексування",
|
||||||
|
"Change Password": "Змінити пароль",
|
||||||
|
"Current Password": "Поточний пароль",
|
||||||
|
"New Password": "Новий пароль",
|
||||||
|
"Repeat New Password": "Повтор нового пароля",
|
||||||
|
"Update Password": "Оновити пароль",
|
||||||
|
"Disable Auth": "Вимкнути авторизацію",
|
||||||
|
"Enable Auth": "Увімкнути авторизацію",
|
||||||
|
Logout: "Вийти",
|
||||||
|
Leave: "Відміна",
|
||||||
|
"I understand, please disable": "Я розумію, все одно відключити",
|
||||||
|
Confirm: "Підтвердити",
|
||||||
|
Yes: "Так",
|
||||||
|
No: "Ні",
|
||||||
|
Username: "Логін",
|
||||||
|
Password: "Пароль",
|
||||||
|
"Remember me": "Запам'ятати мене",
|
||||||
|
Login: "Вхід до системи",
|
||||||
|
"No Monitors, please": "Моніторів немає, будь ласка",
|
||||||
|
"No Monitors": "Монітори відсутні",
|
||||||
|
"add one": "створіть новий",
|
||||||
|
"Notification Type": "Тип повідомлення",
|
||||||
|
Email: "Пошта",
|
||||||
|
Test: "Перевірка",
|
||||||
|
"Certificate Info": "Інформація про сертифікат",
|
||||||
|
"Resolver Server": "DNS сервер",
|
||||||
|
"Resource Record Type": "Тип ресурсного запису",
|
||||||
|
"Last Result": "Останній результат",
|
||||||
|
"Create your admin account": "Створіть обліковий запис адміністратора",
|
||||||
|
"Repeat Password": "Повторіть пароль",
|
||||||
|
respTime: "Час відповіді (мс)",
|
||||||
|
notAvailableShort: "Н/д",
|
||||||
|
Create: "Створити",
|
||||||
|
clearEventsMsg: "Ви дійсно хочете видалити всю статистику подій цього монітора?",
|
||||||
|
clearHeartbeatsMsg: "Ви дійсно хочете видалити всю статистику опитувань цього монітора?",
|
||||||
|
confirmClearStatisticsMsg: "Ви дійсно хочете видалити ВСЮ статистику?",
|
||||||
|
"Clear Data": "Видалити статистику",
|
||||||
|
Events: "Події",
|
||||||
|
Heartbeats: "Опитування",
|
||||||
|
"Auto Get": "Авто-отримання",
|
||||||
|
enableDefaultNotificationDescription: "Для кожного нового монітора це повідомлення буде включено за замовчуванням. Ви все ще можете відключити повідомлення в кожному моніторі окремо.",
|
||||||
|
"Default enabled": "Використовувати за промовчанням",
|
||||||
|
"Also apply to existing monitors": "Застосувати до існуючих моніторів",
|
||||||
|
Export: "Експорт",
|
||||||
|
Import: "Імпорт",
|
||||||
|
backupDescription: "Ви можете зберегти резервну копію всіх моніторів та повідомлень у вигляді JSON-файлу",
|
||||||
|
backupDescription2: "P.S.: Історія та події збережені не будуть",
|
||||||
|
backupDescription3: "Важливі дані, такі як токени повідомлень, додаються під час експорту, тому зберігайте файли в безпечному місці",
|
||||||
|
alertNoFile: "Виберіть файл для імпорту.",
|
||||||
|
alertWrongFileType: "Виберіть JSON-файл.",
|
||||||
|
twoFAVerifyLabel: "Будь ласка, введіть свій токен, щоб перевірити роботу 2FA",
|
||||||
|
tokenValidSettingsMsg: "Токен дійсний! Тепер ви можете зберегти налаштування 2FA.",
|
||||||
|
confirmEnableTwoFAMsg: "Ви дійсно хочете увімкнути 2FA?",
|
||||||
|
confirmDisableTwoFAMsg: "Ви дійсно хочете вимкнути 2FA?",
|
||||||
|
"Apply on all existing monitors": "Застосувати до всіх існуючих моніторів",
|
||||||
|
"Verify Token": "Перевірити токен",
|
||||||
|
"Setup 2FA": "Налаштування 2FA",
|
||||||
|
"Enable 2FA": "Увімкнути 2FA",
|
||||||
|
"Disable 2FA": "Вимкнути 2FA",
|
||||||
|
"2FA Settings": "Налаштування 2FA",
|
||||||
|
"Two Factor Authentication": "Двофакторна аутентифікація",
|
||||||
|
Active: "Активно",
|
||||||
|
Inactive: "Неактивно",
|
||||||
|
Token: "Токен",
|
||||||
|
"Show URI": "Показати URI",
|
||||||
|
"Clear all statistics": "Очистити статистику",
|
||||||
|
retryCheckEverySecond: "Повтор кожні {0} секунд",
|
||||||
|
importHandleDescription: "Виберіть \"Пропустити існуючі\", якщо ви хочете пропустити кожен монітор або повідомлення з таким же ім'ям. \"Перезаписати\" видалить кожен існуючий монітор або повідомлення та додасть заново. Варіант \"Не перевіряти\" примусово відновлює всі монітори і повідомлення, навіть якщо вони вже існують.",
|
||||||
|
confirmImportMsg: "Ви дійсно хочете відновити резервну копію? Переконайтеся, що ви вибрали відповідний варіант імпорту.",
|
||||||
|
"Heartbeat Retry Interval": "Інтервал повтору опитування",
|
||||||
|
"Import Backup": "Імпорт",
|
||||||
|
"Export Backup": "Експорт",
|
||||||
|
"Skip existing": "Пропустити існуючі",
|
||||||
|
Overwrite: "Перезаписати",
|
||||||
|
Options: "Опції",
|
||||||
|
"Keep both": "Не перевіряти",
|
||||||
|
Tags: "Теги",
|
||||||
|
"Add New below or Select...": "Додати новий або вибрати...",
|
||||||
|
"Tag with this name already exist.": "Такий тег вже існує.",
|
||||||
|
"Tag with this value already exist.": "Тег із таким значенням вже існує.",
|
||||||
|
color: "колір",
|
||||||
|
"value (optional)": "значення (опціонально)",
|
||||||
|
Gray: "Сірий",
|
||||||
|
Red: "Червоний",
|
||||||
|
Orange: "Помаранчевий",
|
||||||
|
Green: "Зелений",
|
||||||
|
Blue: "Синій",
|
||||||
|
Indigo: "Індиго",
|
||||||
|
Purple: "Пурпурний",
|
||||||
|
Pink: "Рожевий",
|
||||||
|
"Search...": "Пошук...",
|
||||||
|
"Avg. Ping": "Середнє значення пінгу",
|
||||||
|
"Avg. Response": "Середній час відповіді",
|
||||||
|
"Entry Page": "Головна сторінка",
|
||||||
|
statusPageNothing: "Тут порожньо. Додайте групу або монітор.",
|
||||||
|
"No Services": "Немає сервісів",
|
||||||
|
"All Systems Operational": "Всі системи працюють у штатному режимі",
|
||||||
|
"Partially Degraded Service": "Сервіси працюють частково",
|
||||||
|
"Degraded Service": "Всі сервіси не працюють",
|
||||||
|
"Add Group": "Додати групу",
|
||||||
|
"Add a monitor": "Додати монітор",
|
||||||
|
"Edit Status Page": "Редагувати",
|
||||||
|
"Go to Dashboard": "Панель управління",
|
||||||
|
"Status Page": "Сторінка статусу",
|
||||||
|
"Status Pages": "Сторінки статусу",
|
||||||
|
Discard: "Скасування",
|
||||||
|
"Create Incident": "Створити інцидент",
|
||||||
|
"Switch to Dark Theme": "Темна тема",
|
||||||
|
"Switch to Light Theme": "Світла тема",
|
||||||
|
telegram: "Telegram",
|
||||||
|
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 (Підтримка 50+ сервісів повідомлень)",
|
||||||
|
pushbullet: "Pushbullet",
|
||||||
|
line: "Line Messenger",
|
||||||
|
mattermost: "Mattermost",
|
||||||
|
"Primary Base URL": "Основна URL",
|
||||||
|
"Push URL": "URL пуша",
|
||||||
|
needPushEvery: "До цієї URL необхідно звертатися кожні {0} секунд",
|
||||||
|
pushOptionalParams: "Опціональні параметри: {0}",
|
||||||
|
defaultNotificationName: "Моє повідомлення {notification} ({number})",
|
||||||
|
here: "тут",
|
||||||
|
Required: "Потрібно",
|
||||||
|
"Bot Token": "Токен бота",
|
||||||
|
wayToGetTelegramToken: "Ви можете взяти токен тут - {0}.",
|
||||||
|
"Chat ID": "ID чату",
|
||||||
|
supportTelegramChatID: "Підтримуються ID чатів, груп та каналів",
|
||||||
|
wayToGetTelegramChatID: "Ви можете взяти ID вашого чату, відправивши повідомлення боту і перейшовши по цьому URL для перегляду chat_id:",
|
||||||
|
"YOUR BOT TOKEN HERE": "ВАШ ТОКЕН БОТА ТУТ",
|
||||||
|
chatIDNotFound: "ID чату не знайдено; будь ласка, відправте спочатку повідомлення боту",
|
||||||
|
"Post URL": "Post URL",
|
||||||
|
"Content Type": "Тип контенту",
|
||||||
|
webhookJsonDesc: "{0} підходить для будь-яких сучасних HTTP-серверів, наприклад Express.js",
|
||||||
|
webhookFormDataDesc: "{multipart} підходить для PHP. JSON-вивід необхідно буде обробити за допомогою {decodeFunction}",
|
||||||
|
secureOptionNone: "Ні / STARTTLS (25, 587)",
|
||||||
|
secureOptionTLS: "TLS (465)",
|
||||||
|
"Ignore TLS Error": "Ігнорувати помилки TLS",
|
||||||
|
"From Email": "Від кого",
|
||||||
|
emailCustomSubject: "Своя тема",
|
||||||
|
"To Email": "Кому",
|
||||||
|
smtpCC: "Копія",
|
||||||
|
smtpBCC: "Прихована копія",
|
||||||
|
"Discord Webhook URL": "Discord Вебхук URL",
|
||||||
|
wayToGetDiscordURL: "Ви можете створити його в Параметрах сервера -> Інтеграції -> Створити вебхук",
|
||||||
|
"Bot Display Name": "Ім'я бота, що відображається",
|
||||||
|
"Prefix Custom Message": "Свій префікс повідомлення",
|
||||||
|
"Hello @everyone is...": "Привіт {'@'}everyone це...",
|
||||||
|
"Webhook URL": "URL вебхука",
|
||||||
|
wayToGetTeamsURL: "Як створити URL вебхука ви можете дізнатися тут - {0}.",
|
||||||
|
Номер: "Номер",
|
||||||
|
Recipients: "Одержувачі",
|
||||||
|
needSignalAPI: "Вам необхідний клієнт Signal із підтримкою REST API.",
|
||||||
|
wayToCheckSignalURL: "Пройдіть по цьому URL, щоб дізнатися як налаштувати такий клієнт:",
|
||||||
|
signalImportant: "ВАЖЛИВО: Не можна змішувати в Одержувачах групи та номери!",
|
||||||
|
"Application Token": "Токен програми",
|
||||||
|
"Server URL": "URL сервера",
|
||||||
|
Priority: "Пріоритет",
|
||||||
|
"Icon Emoji": "Іконка Emoji",
|
||||||
|
"Channel Name": "Ім'я каналу",
|
||||||
|
"Uptime Kuma URL": "Uptime Kuma URL",
|
||||||
|
aboutWebhooks: "Більше інформації про вебхуки: {0}",
|
||||||
|
aboutChannelName: "Введіть ім'я каналу в поле {0} Ім'я каналу, якщо ви хочете обійти канал вебхука. Наприклад: #other-channel",
|
||||||
|
aboutKumaURL: "Якщо поле Uptime Kuma URL в налаштуваннях залишиться порожнім, за замовчуванням буде використовуватися посилання на проект на GitHub.",
|
||||||
|
emojiCheatSheet: "Шпаргалка по Emoji: {0}",
|
||||||
|
"User Key": "Ключ користувача",
|
||||||
|
Device: "Пристрій",
|
||||||
|
"Message Title": "Заголовок повідомлення",
|
||||||
|
"Notification Sound": "Звук повідомлення",
|
||||||
|
"More info on:": "Більше інформації: {0}",
|
||||||
|
pushoverDesc1: "Екстренний пріоритет (2) має таймуут повтору за замовчуванням 30 секунд і закінчується через 1 годину.",
|
||||||
|
pushoverDesc2: "Якщо ви бажаєте надсилати повідомлення різним пристроям, необхідно заповнити поле Пристрій.",
|
||||||
|
"SMS Type": "Тип SMS",
|
||||||
|
octopushTypePremium: "Преміум (Швидкий - рекомендується для алертів)",
|
||||||
|
octopushTypeLowCost: "Дешевий (Повільний - іноді блокується операторами)",
|
||||||
|
checkPrice: "Тарифи {0}:",
|
||||||
|
octopushLegacyHint: "Ви використовуєте стару версію Octopush (2011-2020) або нову?",
|
||||||
|
"Check octopush prices": "Тарифи Octopush {0}.",
|
||||||
|
octopushPhoneNumber: "Номер телефону (між. формат, наприклад: +380123456789)",
|
||||||
|
octopushSMSSender: "Ім'я відправника SMS: 3-11 символів алвафіту, цифр та пробілів (a-zA-Z0-9)",
|
||||||
|
"LunaSea Device ID": "ID пристрою LunaSea",
|
||||||
|
"Apprise URL": "Apprise URL",
|
||||||
|
"Example:": "Приклад: {0}",
|
||||||
|
"Read more:": "Докладніше: {0}",
|
||||||
|
"Status:": "Статус: {0}",
|
||||||
|
"Read more": "Докладніше",
|
||||||
|
appriseInstalled: "Apprise встановлено.",
|
||||||
|
appriseNotInstalled: "Apprise не встановлено. {0}",
|
||||||
|
"Access Token": "Токен доступу",
|
||||||
|
"Channel access token": "Токен доступу каналу",
|
||||||
|
"Line Developers Console": "Консоль розробників Line",
|
||||||
|
lineDevConsoleTo: "Консоль розробників Line - {0}",
|
||||||
|
"Basic Settings": "Базові налаштування",
|
||||||
|
"User ID": "ID користувача",
|
||||||
|
"Messaging API": "API повідомлень",
|
||||||
|
wayToGetLineChannelToken: "Спочатку зайдіть в {0}, створіть провайдера та канал (API повідомлень), потім ви зможете отримати токен доступу каналу та ID користувача з вищезгаданих пунктів меню.",
|
||||||
|
"Icon URL": "URL іконки",
|
||||||
|
aboutIconURL: "Ви можете надати посилання на іконку в полі \"URL іконки\", щоб перевизначити картинку профілю за замовчуванням. Не використовується, якщо задана іконка Emoji.",
|
||||||
|
aboutMattermostChannelName: "Ви можете перевизначити канал за замовчуванням, в який пише вебхук, ввівши ім'я каналу в полі \"Ім'я каналу\". Це необхідно включити в налаштуваннях вебхука Mattermost. Наприклад: #other-channel",
|
||||||
|
matrix: "Matrix",
|
||||||
|
promosmsTypeEco: "SMS ECO - дешево та повільно, часто перевантажений. Тільки для одержувачів з Польщі.",
|
||||||
|
promosmsTypeFlash: "SMS FLASH - повідомлення автоматично з'являться на пристрої одержувача. Тільки для одержувачів з Польщі.",
|
||||||
|
promosmsTypeFull: "SMS FULL - преміум-рівень SMS, можна використовувати своє ім'я відправника (попередньо зареєструвавши його). Надійно для алертів.",
|
||||||
|
promosmsTypeSpeed: "SMS SPEED - найвищий пріоритет у системі. Дуже швидко і надійно, але дуже дорого (вдвічі дорожче, ніж SMS FULL).",
|
||||||
|
promosmsPhoneNumber: "Номер телефону (для одержувачів з Польщі можна пропустити код регіону)",
|
||||||
|
promosmsSMSSender: "Ім'я відправника SMS: Зареєстроване або одне з імен за замовчуванням: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
|
||||||
|
"Feishu WebHookURL": "Feishu WebHookURL",
|
||||||
|
matrixHomeserverURL: "URL сервера (разом з http(s):// і опціонально порт)",
|
||||||
|
"Internal Room Id": "Внутрішній ID кімнати",
|
||||||
|
matrixDesc1: "Внутрішній ID кімнати можна знайти в Подробицях у параметрах каналу вашого Matrix клієнта. Він повинен виглядати приблизно як !QMdRCpUIfLwsfjxye6:home.server.",
|
||||||
|
matrixDesc2: "Рекомендується створити нового користувача і не використовувати токен доступу особистого користувача Matrix, тому що це спричиняє повний доступ до облікового запису та до кімнат, в яких ви є. Замість цього створіть нового користувача і запросіть його тільки в ту кімнату, в якій ви хочете отримувати повідомлення.Токен доступу можна отримати, виконавши команду {0}",
|
||||||
|
Method: "Метод",
|
||||||
|
Body: "Тіло",
|
||||||
|
Headers: "Заголовки",
|
||||||
|
PushUrl: "URL пуша",
|
||||||
|
HeadersInvalidFormat: "Заголовки запиту некоректні JSON: ",
|
||||||
|
BodyInvalidFormat: "Тіло запиту некоректне JSON: ",
|
||||||
|
"Monitor History": "Статистика",
|
||||||
|
clearDataOlderThan: "Зберігати статистику за {0} днів.",
|
||||||
|
PasswordsDoNotMatch: "Паролі не співпадають.",
|
||||||
|
records: "записів",
|
||||||
|
"One record": "Один запис",
|
||||||
|
steamApiKeyDescription: "Для моніторингу ігрового сервера Steam вам потрібен Web-API ключ Steam. Зареєструвати його можна тут: ",
|
||||||
|
"Certificate Chain": "Ланцюжок сертифікатів",
|
||||||
|
Valid: "Дійсний",
|
||||||
|
"Hide Tags": "Приховати теги",
|
||||||
|
Title: "Назва інциденту:",
|
||||||
|
Content: "Зміст інциденту:",
|
||||||
|
Post: "Опублікувати",
|
||||||
|
Cancel: "Скасувати",
|
||||||
|
Created: "Створено",
|
||||||
|
Unpin: "Відкріпити",
|
||||||
|
"Show Tags": "Показати теги",
|
||||||
|
recent: "Зараз",
|
||||||
|
"3h": "3 години",
|
||||||
|
"6h": "6 годин",
|
||||||
|
"24h": "24 години",
|
||||||
|
"1w": "1 тиждень",
|
||||||
|
"No monitors available.": "Немає доступних моніторів",
|
||||||
|
"Add one": "Додати новий",
|
||||||
|
Backup: "Резервна копія",
|
||||||
|
Security: "Безпека",
|
||||||
|
"Shrink Database": "Стиснути базу даних",
|
||||||
|
"Current User": "Поточний користувач",
|
||||||
|
About: "Про програму",
|
||||||
|
Description: "Опис",
|
||||||
|
"Powered by": "Працює на основі скрипту від",
|
||||||
|
shrinkDatabaseDescription: "Включає VACUUM для бази даних SQLite. Якщо база даних була створена на версії 1.10.0 і більше, AUTO_VACUUM вже включений і ця дія не потрібна.",
|
||||||
|
Style: "Стиль",
|
||||||
|
info: "ІНФО",
|
||||||
|
warning: "УВАГА",
|
||||||
|
danger: "ПОМИЛКА",
|
||||||
|
primary: "ОСНОВНИЙ",
|
||||||
|
light: "СВІТЛИЙ",
|
||||||
|
dark: "ТЕМНИЙ",
|
||||||
|
"New Status Page": "Нова сторінка статусу",
|
||||||
|
"Show update if available": "Показувати доступні оновлення",
|
||||||
|
"Also check beta release": "Перевіряти оновлення для бета версій",
|
||||||
|
"Add New Status Page": "Додати сторінку статусу",
|
||||||
|
Next: "Далі",
|
||||||
|
"Acz 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 Авторизація",
|
||||||
|
PushByTechulus: "Push by Techulus",
|
||||||
|
clicksendsms: "ClickSend SMS",
|
||||||
|
GoogleChat: "Google Chat (тільки Google Workspace)",
|
||||||
|
apiCredentials: "API реквізити",
|
||||||
|
Done: "Готово",
|
||||||
|
Info: "Інфо",
|
||||||
|
"Steam API Key": "Steam API-Ключ",
|
||||||
|
"Pick a RR-Type...": "Виберіть RR-тип...",
|
||||||
|
"Pick Accepted Status Codes...": "Виберіть прийняті коди стану...",
|
||||||
|
Default: "За замовчуванням",
|
||||||
|
"Please input title and content": "Будь ласка, введіть назву та зміст",
|
||||||
|
"Last Updated": "Останнє Оновлення",
|
||||||
|
"Untitled Group": "Група без назви",
|
||||||
|
Services: "Сервіси",
|
||||||
|
serwersms: "SerwerSMS.pl",
|
||||||
|
serwersmsAPIUser: "API Користувач (включаючи префікс webapi_)",
|
||||||
|
serwersmsAPIPassword: "API Пароль",
|
||||||
|
serwersmsPhoneNumber: "Номер телефону",
|
||||||
|
serwersmsSenderName: "SMS ім'я відправника (реєстрований через портал користувача)",
|
||||||
|
stackfield: "Stackfield",
|
||||||
|
smtpDkimSettings: "DKIM Налаштування",
|
||||||
|
smtpDkimDesc: "Повернутися до Nodemailer DKIM {0} для використання.",
|
||||||
|
documentation: "документація",
|
||||||
|
smtpDkimDomain: "Ім'я домена",
|
||||||
|
smtpDkimKeySelector: "Ключ",
|
||||||
|
smtpDkimPrivateKey: "Приватний ключ",
|
||||||
|
smtpDkimHashAlgo: "Алгоритм хеша (опціонально)",
|
||||||
|
smtpDkimheaderFieldNames: "Заголовок ключів для підпису (опціонально)",
|
||||||
|
smtpDkimskipFields: "Заколовок ключів не для підпису (опціонально)",
|
||||||
|
gorush: "Gorush",
|
||||||
|
alerta: "Alerta",
|
||||||
|
alertaApiEndpoint: "Кінцева точка API",
|
||||||
|
alertaEnvironment: "Середовище",
|
||||||
|
alertaApiKey: "Ключ API",
|
||||||
|
alertaAlertState: "Стан алерту",
|
||||||
|
alertaRecoverState: "Стан відновлення",
|
||||||
|
deleteStatusPageMsg: "Дійсно хочете видалити цю сторінку статусів?",
|
||||||
|
};
|
@ -183,6 +183,7 @@ export default {
|
|||||||
"Edit Status Page": "Sửa trang trạng thái",
|
"Edit Status Page": "Sửa trang trạng thái",
|
||||||
"Go to Dashboard": "Đi tới Dashboard",
|
"Go to Dashboard": "Đi tới Dashboard",
|
||||||
"Status Page": "Trang trạng thái",
|
"Status Page": "Trang trạng thái",
|
||||||
|
"Status Pages": "Trang trạng thái",
|
||||||
defaultNotificationName: "My {notification} Alerts ({number})",
|
defaultNotificationName: "My {notification} Alerts ({number})",
|
||||||
here: "tại đây",
|
here: "tại đây",
|
||||||
Required: "Bắt buộc",
|
Required: "Bắt buộc",
|
||||||
|
@ -185,6 +185,7 @@ export default {
|
|||||||
"Edit Status Page": "编辑状态页面",
|
"Edit Status Page": "编辑状态页面",
|
||||||
"Go to Dashboard": "前往仪表盘",
|
"Go to Dashboard": "前往仪表盘",
|
||||||
"Status Page": "状态页面",
|
"Status Page": "状态页面",
|
||||||
|
"Status Pages": "状态页面",
|
||||||
defaultNotificationName: "{notification} 通知({number})",
|
defaultNotificationName: "{notification} 通知({number})",
|
||||||
here: "这里",
|
here: "这里",
|
||||||
Required: "必填",
|
Required: "必填",
|
||||||
|
@ -96,7 +96,7 @@ export default {
|
|||||||
Test: "測試",
|
Test: "測試",
|
||||||
keywordDescription: "搜索 HTML 或 JSON 裡是否有出現關鍵字(注意英文大細階)",
|
keywordDescription: "搜索 HTML 或 JSON 裡是否有出現關鍵字(注意英文大細階)",
|
||||||
"Certificate Info": "憑證詳細資料",
|
"Certificate Info": "憑證詳細資料",
|
||||||
deleteMonitorMsg: "是否確定刪除這個監測器",
|
deleteMonitorMsg: "是否確定刪除這個監測器?",
|
||||||
deleteNotificationMsg: "是否確定刪除這個通知設定?如監測器啟用了這個通知,將會收不到通知。",
|
deleteNotificationMsg: "是否確定刪除這個通知設定?如監測器啟用了這個通知,將會收不到通知。",
|
||||||
"Resolver Server": "DNS 伺服器",
|
"Resolver Server": "DNS 伺服器",
|
||||||
"Resource Record Type": "DNS 記錄類型",
|
"Resource Record Type": "DNS 記錄類型",
|
||||||
@ -180,6 +180,7 @@ export default {
|
|||||||
"Edit Status Page": "編輯 Status Page",
|
"Edit Status Page": "編輯 Status Page",
|
||||||
"Go to Dashboard": "前往主控台",
|
"Go to Dashboard": "前往主控台",
|
||||||
"Status Page": "Status Page",
|
"Status Page": "Status Page",
|
||||||
|
"Status Pages": "Status Pages",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "電郵 (SMTP)",
|
smtp: "電郵 (SMTP)",
|
||||||
@ -198,4 +199,5 @@ export default {
|
|||||||
pushbullet: "Pushbullet",
|
pushbullet: "Pushbullet",
|
||||||
line: "Line Messenger",
|
line: "Line Messenger",
|
||||||
mattermost: "Mattermost",
|
mattermost: "Mattermost",
|
||||||
|
deleteStatusPageMsg: "是否確定刪除這個 Status Page?",
|
||||||
};
|
};
|
||||||
|
@ -183,6 +183,7 @@ export default {
|
|||||||
"Edit Status Page": "編輯狀態頁",
|
"Edit Status Page": "編輯狀態頁",
|
||||||
"Go to Dashboard": "前往儀表板",
|
"Go to Dashboard": "前往儀表板",
|
||||||
"Status Page": "狀態頁",
|
"Status Page": "狀態頁",
|
||||||
|
"Status Pages": "狀態頁",
|
||||||
defaultNotificationName: "我的 {notification} 通知 ({number})",
|
defaultNotificationName: "我的 {notification} 通知 ({number})",
|
||||||
here: "此處",
|
here: "此處",
|
||||||
Required: "必填",
|
Required: "必填",
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
|
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
{{ $root.connectionErrorMsg }}
|
{{ $root.connectionErrorMsg }}
|
||||||
|
<div v-if="$root.showReverseProxyGuide">
|
||||||
|
Using a Reverse Proxy? <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">Check how to config it for WebSocket</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -18,10 +21,10 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="nav nav-pills">
|
<ul class="nav nav-pills">
|
||||||
<li class="nav-item me-2">
|
<li v-if="$root.loggedIn" class="nav-item me-2">
|
||||||
<a href="/status" class="nav-link status-page">
|
<router-link to="/manage-status-page" class="nav-link">
|
||||||
<font-awesome-icon icon="stream" /> {{ $t("Status Page") }}
|
<font-awesome-icon icon="stream" /> {{ $t("Status Pages") }}
|
||||||
</a>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="$root.loggedIn" class="nav-item me-2">
|
<li v-if="$root.loggedIn" class="nav-item me-2">
|
||||||
<router-link to="/dashboard" class="nav-link">
|
<router-link to="/dashboard" class="nav-link">
|
||||||
@ -45,7 +48,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<router-view v-if="$root.loggedIn" />
|
<router-view v-if="$root.loggedIn || forceShowContent" />
|
||||||
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -184,6 +187,9 @@ main {
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
background-color: crimson;
|
background-color: crimson;
|
||||||
color: white;
|
color: white;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 99999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
import jwt_decode from "jwt-decode";
|
import jwt_decode from "jwt-decode";
|
||||||
|
import Favico from "favico.js";
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
let socket;
|
let socket;
|
||||||
|
|
||||||
const noSocketIOPages = [
|
const noSocketIOPages = [
|
||||||
"/status-page",
|
/^\/status-page$/, // /status-page
|
||||||
"/status",
|
/^\/status/, // /status**
|
||||||
"/"
|
/^\/$/ // /
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const favicon = new Favico({
|
||||||
|
animation: "none"
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -33,8 +38,19 @@ export default {
|
|||||||
uptimeList: { },
|
uptimeList: { },
|
||||||
tlsInfoList: {},
|
tlsInfoList: {},
|
||||||
notificationList: [],
|
notificationList: [],
|
||||||
|
statusPageListLoaded: false,
|
||||||
|
statusPageList: [],
|
||||||
proxyList: [],
|
proxyList: [],
|
||||||
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
||||||
|
showReverseProxyGuide: true,
|
||||||
|
cloudflared: {
|
||||||
|
cloudflareTunnelToken: "",
|
||||||
|
installed: null,
|
||||||
|
running: false,
|
||||||
|
message: "",
|
||||||
|
errorMessage: "",
|
||||||
|
currentPassword: "",
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -52,8 +68,12 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No need to connect to the socket.io for status page
|
// No need to connect to the socket.io for status page
|
||||||
if (! bypass && noSocketIOPages.includes(location.pathname)) {
|
if (! bypass && location.pathname) {
|
||||||
return;
|
for (let page of noSocketIOPages) {
|
||||||
|
if (location.pathname.match(page)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.socket.initedSocketIO = true;
|
this.socket.initedSocketIO = true;
|
||||||
@ -104,6 +124,11 @@ export default {
|
|||||||
this.notificationList = data;
|
this.notificationList = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("statusPageList", (data) => {
|
||||||
|
this.statusPageListLoaded = true;
|
||||||
|
this.statusPageList = data;
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("proxyList", (data) => {
|
socket.on("proxyList", (data) => {
|
||||||
this.proxyList = data.map(item => {
|
this.proxyList = data.map(item => {
|
||||||
item.auth = !!item.auth;
|
item.auth = !!item.auth;
|
||||||
@ -180,6 +205,7 @@ export default {
|
|||||||
socket.on("connect_error", (err) => {
|
socket.on("connect_error", (err) => {
|
||||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||||
this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`;
|
this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`;
|
||||||
|
this.showReverseProxyGuide = true;
|
||||||
this.socket.connected = false;
|
this.socket.connected = false;
|
||||||
this.socket.firstConnect = false;
|
this.socket.firstConnect = false;
|
||||||
});
|
});
|
||||||
@ -194,6 +220,7 @@ export default {
|
|||||||
console.log("Connected to the socket server");
|
console.log("Connected to the socket server");
|
||||||
this.socket.connectCount++;
|
this.socket.connectCount++;
|
||||||
this.socket.connected = true;
|
this.socket.connected = true;
|
||||||
|
this.showReverseProxyGuide = false;
|
||||||
|
|
||||||
// Reset Heartbeat list if it is re-connect
|
// Reset Heartbeat list if it is re-connect
|
||||||
if (this.socket.connectCount >= 2) {
|
if (this.socket.connectCount >= 2) {
|
||||||
@ -223,6 +250,12 @@ export default {
|
|||||||
this.socket.firstConnect = false;
|
this.socket.firstConnect = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// cloudflared
|
||||||
|
socket.on("cloudflared_installed", (res) => this.cloudflared.installed = res);
|
||||||
|
socket.on("cloudflared_running", (res) => this.cloudflared.running = res);
|
||||||
|
socket.on("cloudflared_message", (res) => this.cloudflared.message = res);
|
||||||
|
socket.on("cloudflared_errorMessage", (res) => this.cloudflared.errorMessage = res);
|
||||||
|
socket.on("cloudflared_token", (res) => this.cloudflared.cloudflareTunnelToken = res);
|
||||||
},
|
},
|
||||||
|
|
||||||
storage() {
|
storage() {
|
||||||
@ -250,6 +283,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toastSuccess(msg) {
|
||||||
|
toast.success(msg);
|
||||||
|
},
|
||||||
|
|
||||||
|
toastError(msg) {
|
||||||
|
toast.error(msg);
|
||||||
|
},
|
||||||
|
|
||||||
login(username, password, token, callback) {
|
login(username, password, token, callback) {
|
||||||
socket.emit("login", {
|
socket.emit("login", {
|
||||||
username,
|
username,
|
||||||
@ -403,10 +444,49 @@ export default {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
stats() {
|
||||||
|
let result = {
|
||||||
|
up: 0,
|
||||||
|
down: 0,
|
||||||
|
unknown: 0,
|
||||||
|
pause: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let monitorID in this.$root.monitorList) {
|
||||||
|
let beat = this.$root.lastHeartbeatList[monitorID];
|
||||||
|
let monitor = this.$root.monitorList[monitorID];
|
||||||
|
|
||||||
|
if (monitor && ! monitor.active) {
|
||||||
|
result.pause++;
|
||||||
|
} else if (beat) {
|
||||||
|
if (beat.status === 1) {
|
||||||
|
result.up++;
|
||||||
|
} else if (beat.status === 0) {
|
||||||
|
result.down++;
|
||||||
|
} else if (beat.status === 2) {
|
||||||
|
result.up++;
|
||||||
|
} else {
|
||||||
|
result.unknown++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.unknown++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
|
||||||
|
// Update Badge
|
||||||
|
"stats.down"(to, from) {
|
||||||
|
if (to !== from) {
|
||||||
|
favicon.badge(to);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Reload the SPA if the server version is changed.
|
// Reload the SPA if the server version is changed.
|
||||||
"info.version"(to, from) {
|
"info.version"(to, from) {
|
||||||
if (from && from !== to) {
|
if (from && from !== to) {
|
||||||
@ -420,9 +500,15 @@ export default {
|
|||||||
|
|
||||||
// Reconnect the socket io, if status-page to dashboard
|
// Reconnect the socket io, if status-page to dashboard
|
||||||
"$route.fullPath"(newValue, oldValue) {
|
"$route.fullPath"(newValue, oldValue) {
|
||||||
if (noSocketIOPages.includes(newValue)) {
|
|
||||||
return;
|
if (newValue) {
|
||||||
|
for (let page of noSocketIOPages) {
|
||||||
|
if (newValue.match(page)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.initSocketIO();
|
this.initSocketIO();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ export default {
|
|||||||
return "light";
|
return "light";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.path === "/status-page" || this.path === "/status") {
|
if (this.path.startsWith("/status-page") || this.path.startsWith("/status")) {
|
||||||
return this.statusPageTheme;
|
return this.statusPageTheme;
|
||||||
} else {
|
} else {
|
||||||
if (this.userTheme === "auto") {
|
if (this.userTheme === "auto") {
|
||||||
|
79
src/pages/AddStatusPage.vue
Normal file
79
src/pages/AddStatusPage.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="slide-fade" appear>
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-3">
|
||||||
|
{{ $t("Add New Status Page") }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<div class="shadow-box">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">{{ $t("Name") }}</label>
|
||||||
|
<input id="name" v-model="title" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="slug" class="form-label">{{ $t("Slug") }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span id="basic-addon3" class="input-group-text">/status/</span>
|
||||||
|
<input id="slug" v-model="slug" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
<ul>
|
||||||
|
<li>{{ $t("Accept characters:") }} <mark>a-z</mark> <mark>0-9</mark> <mark>-</mark></li>
|
||||||
|
<li>{{ $t("Start or end with") }} <mark>a-z</mark> <mark>0-9</mark> only</li>
|
||||||
|
<li>{{ $t("No consecutive dashes") }} <mark>--</mark></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 mb-1">
|
||||||
|
<button id="monitor-submit-btn" class="btn btn-primary w-100" type="submit" :disabled="processing">{{ $t("Next") }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: "",
|
||||||
|
slug: "",
|
||||||
|
processing: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async submit() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("addStatusPage", this.title, this.slug, (res) => {
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
location.href = "/status/" + this.slug + "?edit";
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (res.msg.includes("UNIQUE constraint")) {
|
||||||
|
this.$root.toastError(this.$t("The slug is already taken. Please choose another slug."));
|
||||||
|
} else {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.shadow-box {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -9,19 +9,19 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3>{{ $t("Up") }}</h3>
|
<h3>{{ $t("Up") }}</h3>
|
||||||
<span class="num">{{ stats.up }}</span>
|
<span class="num">{{ $root.stats.up }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3>{{ $t("Down") }}</h3>
|
<h3>{{ $t("Down") }}</h3>
|
||||||
<span class="num text-danger">{{ stats.down }}</span>
|
<span class="num text-danger">{{ $root.stats.down }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3>{{ $t("Unknown") }}</h3>
|
<h3>{{ $t("Unknown") }}</h3>
|
||||||
<span class="num text-secondary">{{ stats.unknown }}</span>
|
<span class="num text-secondary">{{ $root.stats.unknown }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3>{{ $t("pauseDashboardHome") }}</h3>
|
<h3>{{ $t("pauseDashboardHome") }}</h3>
|
||||||
<span class="num text-secondary">{{ stats.pause }}</span>
|
<span class="num text-secondary">{{ $root.stats.pause }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -89,37 +89,6 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
stats() {
|
|
||||||
let result = {
|
|
||||||
up: 0,
|
|
||||||
down: 0,
|
|
||||||
unknown: 0,
|
|
||||||
pause: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let monitorID in this.$root.monitorList) {
|
|
||||||
let beat = this.$root.lastHeartbeatList[monitorID];
|
|
||||||
let monitor = this.$root.monitorList[monitorID];
|
|
||||||
|
|
||||||
if (monitor && ! monitor.active) {
|
|
||||||
result.pause++;
|
|
||||||
} else if (beat) {
|
|
||||||
if (beat.status === 1) {
|
|
||||||
result.up++;
|
|
||||||
} else if (beat.status === 0) {
|
|
||||||
result.down++;
|
|
||||||
} else if (beat.status === 2) {
|
|
||||||
result.up++;
|
|
||||||
} else {
|
|
||||||
result.unknown++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.unknown++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
importantHeartBeatList() {
|
importantHeartBeatList() {
|
||||||
let result = [];
|
let result = [];
|
||||||
|
118
src/pages/ManageStatusPage.vue
Normal file
118
src/pages/ManageStatusPage.vue
Normal file
@ -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>
|
99
src/pages/NotFound.vue
Normal file
99
src/pages/NotFound.vue
Normal file
@ -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>
|
@ -75,6 +75,9 @@ export default {
|
|||||||
notifications: {
|
notifications: {
|
||||||
title: this.$t("Notifications"),
|
title: this.$t("Notifications"),
|
||||||
},
|
},
|
||||||
|
"reverse-proxy": {
|
||||||
|
title: this.$t("Reverse Proxy"),
|
||||||
|
},
|
||||||
"monitor-history": {
|
"monitor-history": {
|
||||||
title: this.$t("Monitor History"),
|
title: this.$t("Monitor History"),
|
||||||
},
|
},
|
||||||
@ -134,10 +137,18 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
saveSettings() {
|
/**
|
||||||
this.$root.getSocket().emit("setSettings", this.settings, (res) => {
|
* Save Settings
|
||||||
|
* @param currentPassword (Optional) Only need for disableAuth to true
|
||||||
|
*/
|
||||||
|
saveSettings(callback, currentPassword) {
|
||||||
|
this.$root.getSocket().emit("setSettings", this.settings, currentPassword, (res) => {
|
||||||
this.$root.toastRes(res);
|
this.$root.toastRes(res);
|
||||||
this.loadSettings();
|
this.loadSettings();
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -170,6 +181,8 @@ footer {
|
|||||||
margin: 0.5em;
|
margin: 0.5em;
|
||||||
padding: 0.7em 1em;
|
padding: 0.7em 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
border-left-width: 0;
|
||||||
|
transition: all ease-in-out 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item:hover {
|
.menu-item:hover {
|
||||||
|
@ -1,45 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="loadedTheme" class="container mt-3">
|
<div v-if="loadedTheme" class="container mt-3">
|
||||||
<!-- Logo & Title -->
|
<!-- Sidebar for edit mode -->
|
||||||
<h1 class="mb-4">
|
<div v-if="enableEditMode" class="sidebar">
|
||||||
<!-- Logo -->
|
<div class="my-3">
|
||||||
<span class="logo-wrapper" @click="showImageCropUploadMethod">
|
<label for="slug" class="form-label">{{ $t("Slug") }}</label>
|
||||||
<img :src="logoURL" alt class="logo me-2" :class="logoClass" />
|
<div class="input-group">
|
||||||
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
|
<span id="basic-addon3" class="input-group-text">/status/</span>
|
||||||
</span>
|
<input id="slug" v-model="config.slug" type="text" class="form-control">
|
||||||
|
</div>
|
||||||
<!-- Uploader -->
|
|
||||||
<!-- 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>
|
|
||||||
</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">
|
<button class="btn btn-success me-2" @click="save">
|
||||||
<font-awesome-icon icon="save" />
|
<font-awesome-icon icon="save" />
|
||||||
{{ $t("Save") }}
|
{{ $t("Save") }}
|
||||||
@ -49,167 +58,182 @@
|
|||||||
<font-awesome-icon icon="save" />
|
<font-awesome-icon icon="save" />
|
||||||
{{ $t("Discard") }}
|
{{ $t("Discard") }}
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Incident -->
|
<!-- Main Status Page -->
|
||||||
<div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass">
|
<div :class="{ edit: enableEditMode}" class="main">
|
||||||
<strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
|
<!-- Logo & Title -->
|
||||||
<Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" />
|
<h1 class="mb-4 title-flex">
|
||||||
|
<!-- Logo -->
|
||||||
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
|
<span class="logo-wrapper" @click="showImageCropUploadMethod">
|
||||||
<Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
|
<img :src="logoURL" alt class="logo me-2" :class="logoClass" @load="statusPageLogoLoaded" />
|
||||||
|
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
|
||||||
<!-- Incident Date -->
|
|
||||||
<div class="date mt-3">
|
|
||||||
{{ $t("Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br />
|
|
||||||
<span v-if="incident.lastUpdatedDate">
|
|
||||||
{{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }})
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="editMode" class="mt-3">
|
<!-- Uploader -->
|
||||||
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident">
|
<!-- url="/api/status-page/upload-logo" -->
|
||||||
<font-awesome-icon icon="bullhorn" />
|
<ImageCropUpload v-model="showImageCropUpload"
|
||||||
{{ $t("Post") }}
|
field="img"
|
||||||
</button>
|
: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">
|
<!-- Title -->
|
||||||
<font-awesome-icon icon="edit" />
|
<Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" />
|
||||||
{{ $t("Edit") }}
|
</h1>
|
||||||
</button>
|
|
||||||
|
|
||||||
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="cancelIncident">
|
<!-- Admin functions -->
|
||||||
<font-awesome-icon icon="times" />
|
<div v-if="hasToken" class="mb-4">
|
||||||
{{ $t("Cancel") }}
|
<div v-if="!enableEditMode">
|
||||||
</button>
|
<button class="btn btn-info me-2" @click="edit">
|
||||||
|
<font-awesome-icon icon="edit" />
|
||||||
<div v-if="editIncidentMode" class="dropdown d-inline-block me-2">
|
{{ $t("Edit Status Page") }}
|
||||||
<button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
{{ $t("Style") }}: {{ $t(incident.style) }}
|
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
|
|
||||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning") }}</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger") }}</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary") }}</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light") }}</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="unpinIncident">
|
<a href="/manage-status-page" class="btn btn-info">
|
||||||
<font-awesome-icon icon="unlink" />
|
<font-awesome-icon icon="tachometer-alt" />
|
||||||
{{ $t("Unpin") }}
|
{{ $t("Go to Dashboard") }}
|
||||||
</button>
|
</a>
|
||||||
</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") }}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<font-awesome-icon icon="question-circle" style="color: #efefef;" />
|
<button class="btn btn-primary btn-add-group me-2" @click="createIncident">
|
||||||
</div>
|
<font-awesome-icon icon="bullhorn" />
|
||||||
</template>
|
{{ $t("Create Incident") }}
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<!-- 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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
<!-- Incident -->
|
||||||
<div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center">
|
<div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass">
|
||||||
<!-- 👀 Nothing here, please add a group or a monitor. -->
|
<strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
|
||||||
👀 {{ $t("statusPageNothing") }}
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-5 mb-4">
|
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteStatusPage">
|
||||||
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
|
{{ $t("deleteStatusPageMsg") }}
|
||||||
</footer>
|
</Confirm>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -220,16 +244,25 @@ import ImageCropUpload from "vue-image-crop-upload";
|
|||||||
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts";
|
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts";
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import Favico from "favico.js";
|
||||||
|
import { getResBaseURL } from "../util-frontend";
|
||||||
|
import Confirm from "../components/Confirm.vue";
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const leavePageMsg = "Do you really want to leave? you have unsaved changes!";
|
const leavePageMsg = "Do you really want to leave? you have unsaved changes!";
|
||||||
|
|
||||||
let feedInterval;
|
let feedInterval;
|
||||||
|
|
||||||
|
const favicon = new Favico({
|
||||||
|
animation: "none"
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
PublicGroupList,
|
PublicGroupList,
|
||||||
ImageCropUpload
|
ImageCropUpload,
|
||||||
|
Confirm,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Leave Page for vue route change
|
// Leave Page for vue route change
|
||||||
@ -247,6 +280,7 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
slug: null,
|
||||||
enableEditMode: false,
|
enableEditMode: false,
|
||||||
enableEditIncidentMode: false,
|
enableEditIncidentMode: false,
|
||||||
hasToken: false,
|
hasToken: false,
|
||||||
@ -259,6 +293,8 @@ export default {
|
|||||||
loadedTheme: false,
|
loadedTheme: false,
|
||||||
loadedData: false,
|
loadedData: false,
|
||||||
baseURL: "",
|
baseURL: "",
|
||||||
|
clickedEditButton: false,
|
||||||
|
domainNamesPlaceholder: "domain1.com\ndomain2.com\n..."
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -296,15 +332,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isPublished() {
|
isPublished() {
|
||||||
return this.config.statusPagePublished;
|
return this.config.published;
|
||||||
},
|
|
||||||
|
|
||||||
theme() {
|
|
||||||
return this.config.statusPageTheme;
|
|
||||||
},
|
|
||||||
|
|
||||||
tagsVisible() {
|
|
||||||
return this.config.statusPageTags
|
|
||||||
},
|
},
|
||||||
|
|
||||||
logoClass() {
|
logoClass() {
|
||||||
@ -378,13 +406,28 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Set Theme
|
// Set Theme
|
||||||
"config.statusPageTheme"() {
|
"config.theme"() {
|
||||||
this.$root.statusPageTheme = this.config.statusPageTheme;
|
this.$root.statusPageTheme = this.config.theme;
|
||||||
this.loadedTheme = true;
|
this.loadedTheme = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
"config.title"(title) {
|
"config.title"(title) {
|
||||||
document.title = title;
|
document.title = title;
|
||||||
|
},
|
||||||
|
|
||||||
|
"$root.monitorList"() {
|
||||||
|
let count = Object.keys(this.$root.monitorList).length;
|
||||||
|
|
||||||
|
// Since publicGroupList is getting from public rest api, monitors' tags may not present if showTags = false
|
||||||
|
if (count > 0) {
|
||||||
|
for (let group of this.$root.publicGroupList) {
|
||||||
|
for (let monitor of group.monitorList) {
|
||||||
|
if (monitor.tags === undefined && this.$root.monitorList[monitor.id]) {
|
||||||
|
monitor.tags = this.$root.monitorList[monitor.id].tags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -403,28 +446,24 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Special handle for dev
|
// Special handle for dev
|
||||||
const env = process.env.NODE_ENV;
|
this.baseURL = getResBaseURL();
|
||||||
if (env === "development" || localStorage.dev === "dev") {
|
|
||||||
this.baseURL = location.protocol + "//" + location.hostname + ":3001";
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
axios.get("/api/status-page/config").then((res) => {
|
this.slug = this.$route.params.slug;
|
||||||
this.config = res.data;
|
|
||||||
|
|
||||||
if (this.config.logo) {
|
if (!this.slug) {
|
||||||
this.imgDataUrl = this.config.logo;
|
this.slug = "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.get("/api/status-page/" + this.slug).then((res) => {
|
||||||
|
this.config = res.data.config;
|
||||||
|
|
||||||
|
if (this.config.icon) {
|
||||||
|
this.imgDataUrl = this.config.icon;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
axios.get("/api/status-page/incident").then((res) => {
|
this.incident = res.data.incident;
|
||||||
if (res.data.ok) {
|
this.$root.publicGroupList = res.data.publicGroupList;
|
||||||
this.incident = res.data.incident;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
axios.get("/api/status-page/monitor-list").then((res) => {
|
|
||||||
this.$root.publicGroupList = res.data;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5mins a loop
|
// 5mins a loop
|
||||||
@ -432,31 +471,87 @@ export default {
|
|||||||
feedInterval = setInterval(() => {
|
feedInterval = setInterval(() => {
|
||||||
this.updateHeartbeatList();
|
this.updateHeartbeatList();
|
||||||
}, (300 + 10) * 1000);
|
}, (300 + 10) * 1000);
|
||||||
|
|
||||||
|
// Go to edit page if ?edit present
|
||||||
|
// null means ?edit present, but no value
|
||||||
|
if (this.$route.query.edit || this.$route.query.edit === null) {
|
||||||
|
this.edit();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
updateHeartbeatList() {
|
updateHeartbeatList() {
|
||||||
// If editMode, it will use the data from websocket.
|
// If editMode, it will use the data from websocket.
|
||||||
if (! this.editMode) {
|
if (! this.editMode) {
|
||||||
axios.get("/api/status-page/heartbeat").then((res) => {
|
axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => {
|
||||||
this.$root.heartbeatList = res.data.heartbeatList;
|
const { heartbeatList, uptimeList } = res.data;
|
||||||
this.$root.uptimeList = res.data.uptimeList;
|
|
||||||
|
this.$root.heartbeatList = heartbeatList;
|
||||||
|
this.$root.uptimeList = uptimeList;
|
||||||
|
|
||||||
|
const heartbeatIds = Object.keys(heartbeatList);
|
||||||
|
const downMonitors = heartbeatIds.reduce((downMonitorsAmount, currentId) => {
|
||||||
|
const monitorHeartbeats = heartbeatList[currentId];
|
||||||
|
const lastHeartbeat = monitorHeartbeats.at(-1);
|
||||||
|
|
||||||
|
if (lastHeartbeat) {
|
||||||
|
return lastHeartbeat.status === 0 ? downMonitorsAmount + 1 : downMonitorsAmount;
|
||||||
|
} else {
|
||||||
|
return downMonitorsAmount;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
favicon.badge(downMonitors);
|
||||||
|
|
||||||
this.loadedData = true;
|
this.loadedData = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
edit() {
|
edit() {
|
||||||
this.$root.initSocketIO(true);
|
if (this.hasToken) {
|
||||||
this.enableEditMode = true;
|
this.$root.initSocketIO(true);
|
||||||
|
this.enableEditMode = true;
|
||||||
|
this.clickedEditButton = true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
this.$root.getSocket().emit("saveStatusPage", this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
|
let startTime = new Date();
|
||||||
|
this.config.slug = this.config.slug.trim().toLowerCase();
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("saveStatusPage", this.slug, this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.enableEditMode = false;
|
this.enableEditMode = false;
|
||||||
this.$root.publicGroupList = res.publicGroupList;
|
this.$root.publicGroupList = res.publicGroupList;
|
||||||
location.reload();
|
|
||||||
|
// Add some delay, so that the side menu animation would be better
|
||||||
|
let endTime = new Date();
|
||||||
|
let time = 100 - (endTime - startTime) / 1000;
|
||||||
|
|
||||||
|
if (time < 0) {
|
||||||
|
time = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
location.href = "/status/" + this.config.slug;
|
||||||
|
}, time);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteDialog() {
|
||||||
|
this.$refs.confirmDelete.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteStatusPage() {
|
||||||
|
this.$root.getSocket().emit("deleteStatusPage", this.slug, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.enableEditMode = false;
|
||||||
|
location.href = "/manage-status-page";
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
@ -481,30 +576,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
discard() {
|
discard() {
|
||||||
location.reload();
|
location.href = "/status/" + this.slug;
|
||||||
},
|
|
||||||
|
|
||||||
changeTheme(name) {
|
|
||||||
this.config.statusPageTheme = name;
|
|
||||||
},
|
|
||||||
changeTagsVisibilty(newState) {
|
|
||||||
this.config.statusPageTags = newState;
|
|
||||||
|
|
||||||
// On load, the status page will not include tags if it's not enabled for security reasons
|
|
||||||
// Which means if we enable tags, it won't show in the UI until saved
|
|
||||||
// So we have this to enhance UX and load in the tags from the authenticated source instantly
|
|
||||||
this.$root.publicGroupList = this.$root.publicGroupList.map((group) => {
|
|
||||||
return {
|
|
||||||
...group,
|
|
||||||
monitorList: group.monitorList.map((monitor) => {
|
|
||||||
// We only include the tags if visible so we can reuse the logic to hide the tags on disable
|
|
||||||
return {
|
|
||||||
...monitor,
|
|
||||||
tags: newState ? this.$root.monitorList[monitor.id].tags : []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -520,6 +592,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
statusPageLogoLoaded(eventPayload) {
|
||||||
|
// Remark: may not work in dev, due to cros
|
||||||
|
favicon.image(eventPayload.target);
|
||||||
|
},
|
||||||
|
|
||||||
createIncident() {
|
createIncident() {
|
||||||
this.enableEditIncidentMode = true;
|
this.enableEditIncidentMode = true;
|
||||||
|
|
||||||
@ -540,7 +617,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$root.getSocket().emit("postIncident", this.incident, (res) => {
|
this.$root.getSocket().emit("postIncident", this.slug, this.incident, (res) => {
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.enableEditIncidentMode = false;
|
this.enableEditIncidentMode = false;
|
||||||
@ -571,7 +648,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
unpinIncident() {
|
unpinIncident() {
|
||||||
this.$root.getSocket().emit("unpinIncident", () => {
|
this.$root.getSocket().emit("unpinIncident", this.slug, () => {
|
||||||
this.incident = null;
|
this.incident = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -614,6 +691,40 @@ h1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
transition: all ease-in-out 0.1s;
|
||||||
|
|
||||||
|
&.edit {
|
||||||
|
margin-left: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 300px;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 15px 15px 68px 15px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-right: 1px solid #ededed;
|
||||||
|
|
||||||
|
.danger-zone {
|
||||||
|
border-top: 1px solid #ededed;
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 15px;
|
||||||
|
position: absolute;
|
||||||
|
border-top: 1px solid #ededed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@ -623,6 +734,12 @@ footer {
|
|||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title-flex {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.logo-wrapper {
|
.logo-wrapper {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -681,4 +798,19 @@ footer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.sidebar {
|
||||||
|
background-color: $dark-header-bg;
|
||||||
|
border-right-color: $dark-border-color;
|
||||||
|
|
||||||
|
.danger-zone {
|
||||||
|
border-top-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
border-top-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -14,11 +14,15 @@ import Entry from "./pages/Entry.vue";
|
|||||||
import Appearance from "./components/settings/Appearance.vue";
|
import Appearance from "./components/settings/Appearance.vue";
|
||||||
import General from "./components/settings/General.vue";
|
import General from "./components/settings/General.vue";
|
||||||
import Notifications from "./components/settings/Notifications.vue";
|
import Notifications from "./components/settings/Notifications.vue";
|
||||||
|
import ReverseProxy from "./components/settings/ReverseProxy.vue";
|
||||||
import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
||||||
import Security from "./components/settings/Security.vue";
|
import Security from "./components/settings/Security.vue";
|
||||||
import Proxies from "./components/settings/Proxies.vue";
|
import Proxies from "./components/settings/Proxies.vue";
|
||||||
import Backup from "./components/settings/Backup.vue";
|
import Backup from "./components/settings/Backup.vue";
|
||||||
import About from "./components/settings/About.vue";
|
import About from "./components/settings/About.vue";
|
||||||
|
import ManageStatusPage from "./pages/ManageStatusPage.vue";
|
||||||
|
import AddStatusPage from "./pages/AddStatusPage.vue";
|
||||||
|
import NotFound from "./pages/NotFound.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@ -81,6 +85,10 @@ const routes = [
|
|||||||
path: "notifications",
|
path: "notifications",
|
||||||
component: Notifications,
|
component: Notifications,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "reverse-proxy",
|
||||||
|
component: ReverseProxy,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "monitor-history",
|
path: "monitor-history",
|
||||||
component: MonitorHistory,
|
component: MonitorHistory,
|
||||||
@ -103,6 +111,14 @@ const routes = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/manage-status-page",
|
||||||
|
component: ManageStatusPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/add-status-page",
|
||||||
|
component: AddStatusPage,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -119,6 +135,14 @@ const routes = [
|
|||||||
path: "/status",
|
path: "/status",
|
||||||
component: StatusPage,
|
component: StatusPage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/status/:slug",
|
||||||
|
component: StatusPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/:pathMatch(.*)*",
|
||||||
|
component: NotFound,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
|
@ -51,7 +51,19 @@ export function timezoneList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setPageLocale() {
|
export function setPageLocale() {
|
||||||
const html = document.documentElement
|
const html = document.documentElement;
|
||||||
html.setAttribute('lang', currentLocale() )
|
html.setAttribute("lang", currentLocale() );
|
||||||
html.setAttribute('dir', localeDirection() )
|
html.setAttribute("dir", localeDirection() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mainly used for dev, because the backend and the frontend are in different ports.
|
||||||
|
*/
|
||||||
|
export function getResBaseURL() {
|
||||||
|
const env = process.env.NODE_ENV;
|
||||||
|
if (env === "development" || localStorage.dev === "dev") {
|
||||||
|
return location.protocol + "//" + location.hostname + ":3001";
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user