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