Merge branch 'master' into customstatuspage

# Conflicts:
#	src/languages/de-DE.js
#	src/languages/en.js
#	src/pages/StatusPage.vue
pull/1083/head
Louis Lam 3 years ago
commit 4a6d7207ef

@ -28,6 +28,8 @@ SECURITY.md
tsconfig.json tsconfig.json
.env .env
/tmp /tmp
/babel.config.js
/ecosystem.config.js
### .gitignore content (commented rules are duplicated) ### .gitignore content (commented rules are duplicated)
@ -42,4 +44,6 @@ dist-ssr
#!/data/.gitkeep #!/data/.gitkeep
#.vscode #.vscode
### End of .gitignore content ### End of .gitignore content

@ -4,10 +4,10 @@ Fixes #(issue)
## Type of change ## Type of change
Please delete options that are not relevant. Please delete any options that are not relevant.
- Bug fix (non-breaking change which fixes an issue) - Bug fix (non-breaking change which fixes an issue)
- User Interface - User interface (UI)
- New feature (non-breaking change which adds functionality) - New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to not work as expected) - Breaking change (fix or feature that would cause existing functionality to not work as expected)
- Translation update - Translation update
@ -18,7 +18,7 @@ Please delete options that are not relevant.
- [ ] My code follows the style guidelines of this project - [ ] My code follows the style guidelines of this project
- [ ] I ran ESLint and other linters for modified files - [ ] I ran ESLint and other linters for modified files
- [ ] I have performed a self-review of my own code and test it - [ ] I have performed a self-review of my own code and tested it
- [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] My changes generate no new warnings - [ ] My changes generate no new warnings
- [ ] My code needed automated testing. I have added them (this is optional task) - [ ] My code needed automated testing. I have added them (this is optional task)

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

@ -0,0 +1 @@
legacy-peer-deps=true

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

@ -37,7 +37,6 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec
### 🐳 Docker ### 🐳 Docker
```bash ```bash
docker volume create uptime-kuma
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1 docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
``` ```
@ -47,7 +46,10 @@ Browse to http://localhost:3001 after starting.
### 💪🏻 Non-Docker ### 💪🏻 Non-Docker
Required Tools: Node.js >= 14, git and pm2. Required Tools:
- [Node.js](https://nodejs.org/en/download/) >= 14
- [Git](https://git-scm.com/downloads)
- [pm2](https://pm2.keymetrics.io/) - For run in background
```bash ```bash
# Update your npm to the latest version # Update your npm to the latest version
@ -61,12 +63,26 @@ npm run setup
node server/server.js node server/server.js
# (Recommended) Option 2. Run in background using PM2 # (Recommended) Option 2. Run in background using PM2
# Install PM2 if you don't have it: npm install pm2 -g # Install PM2 if you don't have it:
npm install pm2 -g && pm2 install pm2-logrotate
# Start Server
pm2 start server/server.js --name uptime-kuma pm2 start server/server.js --name uptime-kuma
```
```
Browse to http://localhost:3001 after starting. Browse to http://localhost:3001 after starting.
More useful PM2 Commands
```bash
# If you want to see the current console output
pm2 monit
# If you want to add it to startup
pm2 save && pm2 startup
```
### Advanced Installation ### Advanced Installation
If you need more options or need to browse via a reverse proxy, please read: If you need more options or need to browse via a reverse proxy, please read:
@ -93,7 +109,7 @@ https://github.com/louislam/uptime-kuma/projects/1
Thank you so much! (GitHub Sponsors will be updated manually. OpenCollective sponsors will be updated automatically, the list will be cached by GitHub though. It may need some time to be updated) Thank you so much! (GitHub Sponsors will be updated manually. OpenCollective sponsors will be updated automatically, the list will be cached by GitHub though. It may need some time to be updated)
<img src="https://uptime.kuma.pet/sponsors?v=3" alt /> <img src="https://uptime.kuma.pet/sponsors?v=6" alt />
## 🖼 More Screenshots ## 🖼 More Screenshots
@ -115,7 +131,7 @@ Telegram Notification Sample:
## Motivation ## Motivation
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and unmaintained. * I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and no longer maintained.
* Want to build a fancy UI. * Want to build a fancy UI.
* Learn Vue 3 and vite.js. * Learn Vue 3 and vite.js.
* Show the power of Bootstrap 5. * Show the power of Bootstrap 5.
@ -144,4 +160,4 @@ If you want to translate Uptime Kuma into your language, please read: https://gi
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
English proofreading is needed too because my grammar is not that great, sadly. Feel free to correct my grammar in this README, source code, or wiki. Unfortunately, English proofreading is needed too because my grammar is not that great. Feel free to correct my grammar in this README, source code, or wiki.

@ -0,0 +1,7 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD expiry_notification BOOLEAN default 1;
COMMIT;

@ -0,0 +1,23 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
CREATE TABLE proxy (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
user_id INT NOT NULL,
protocol VARCHAR(10) NOT NULL,
host VARCHAR(255) NOT NULL,
port SMALLINT NOT NULL,
auth BOOLEAN NOT NULL,
username VARCHAR(255) NULL,
password VARCHAR(255) NULL,
active BOOLEAN NOT NULL DEFAULT 1,
'default' BOOLEAN NOT NULL DEFAULT 0,
created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL
);
ALTER TABLE monitor ADD COLUMN proxy_id INTEGER REFERENCES proxy(id);
CREATE INDEX proxy_id ON monitor (proxy_id);
CREATE INDEX proxy_user_id ON proxy (user_id);
COMMIT;

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

@ -1,8 +1,8 @@
# DON'T UPDATE TO alpine3.13, 1.14, see #41. # DON'T UPDATE TO alpine3.13, 1.14, see #41.
FROM node:14-alpine3.12 FROM node:16-alpine3.12
WORKDIR /app WORKDIR /app
# Install apprise, iputils for non-root ping, setpriv # Install apprise, iputils for non-root ping, setpriv
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \ RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
pip3 --no-cache-dir install apprise==0.9.6 && \ pip3 --no-cache-dir install apprise==0.9.7 && \
rm -rf /root/.cache rm -rf /root/.cache

@ -1,12 +1,26 @@
# DON'T UPDATE TO node:14-bullseye-slim, see #372. # DON'T UPDATE TO node:14-bullseye-slim, see #372.
# If the image changed, the second stage image should be changed too # If the image changed, the second stage image should be changed too
FROM node:14-buster-slim FROM node:16-buster-slim
ARG TARGETPLATFORM
WORKDIR /app WORKDIR /app
# Install Curl
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv # Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine! # Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine!
RUN apt update && \ RUN apt update && \
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
sqlite3 iputils-ping util-linux dumb-init && \ sqlite3 iputils-ping util-linux dumb-init && \
pip3 --no-cache-dir install apprise==0.9.6 && \ pip3 --no-cache-dir install apprise==0.9.7 && \
rm -rf /var/lib/apt/lists/* 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

@ -5,7 +5,7 @@ version: '3.3'
services: services:
uptime-kuma: uptime-kuma:
image: louislam/uptime-kuma image: louislam/uptime-kuma:1
container_name: uptime-kuma container_name: uptime-kuma
volumes: volumes:
- ./uptime-kuma:/app/data - ./uptime-kuma:/app/data

@ -0,0 +1,71 @@
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 (!version || !version.includes("-beta.")) {
console.error("invalid version, beta version only");
process.exit(1);
}
const exists = tagExists(version);
if (! exists) {
// Process package.json
pkg.version = version;
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
commit(version);
tag(version);
} else {
console.log("version tag exists, please delete the tag or use another tag");
process.exit(1);
}
function commit(version) {
let msg = "Update to " + version;
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
let stdout = res.stdout.toString().trim();
console.log(stdout);
if (stdout.includes("no changes added to commit")) {
throw new Error("commit error");
}
res = child_process.spawnSync("git", ["push", "origin", "master"]);
console.log(res.stdout.toString().trim());
}
function tag(version) {
let res = child_process.spawnSync("git", ["tag", version]);
console.log(res.stdout.toString().trim());
res = child_process.spawnSync("git", ["push", "origin", version]);
console.log(res.stdout.toString().trim());
}
function tagExists(version) {
if (! version) {
throw new Error("invalid version");
}
let res = child_process.spawnSync("git", ["tag", "-l", version]);
return res.stdout.toString().trim() === version;
}
function safeDelete(dir) {
if (fs.existsSync(dir)) {
fs.rmdirSync(dir, {
recursive: true,
});
}
}

@ -0,0 +1,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);
}
});
}

@ -4,6 +4,7 @@ const tar = require("tar");
const packageJSON = require("../package.json"); const packageJSON = require("../package.json");
const fs = require("fs"); const fs = require("fs");
const rmSync = require("./fs-rmSync.js");
const version = packageJSON.version; const version = packageJSON.version;
const filename = "dist.tar.gz"; const filename = "dist.tar.gz";
@ -11,6 +12,12 @@ const filename = "dist.tar.gz";
const url = `https://github.com/louislam/uptime-kuma/releases/download/${version}/${filename}`; const url = `https://github.com/louislam/uptime-kuma/releases/download/${version}/${filename}`;
download(url); download(url);
/**
* Downloads the latest version of the dist from a GitHub release.
* @param {string} url The URL to download from.
*
* Generated by Trelent
*/
function download(url) { function download(url) {
console.log(url); console.log(url);
@ -21,7 +28,7 @@ function download(url) {
if (fs.existsSync("./dist")) { if (fs.existsSync("./dist")) {
if (fs.existsSync("./dist-backup")) { if (fs.existsSync("./dist-backup")) {
fs.rmdirSync("./dist-backup", { rmSync("./dist-backup", {
recursive: true recursive: true
}); });
} }
@ -35,7 +42,7 @@ function download(url) {
tarStream.on("close", () => { tarStream.on("close", () => {
if (fs.existsSync("./dist-backup")) { if (fs.existsSync("./dist-backup")) {
fs.rmdirSync("./dist-backup", { rmSync("./dist-backup", {
recursive: true recursive: true
}); });
} }

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

@ -0,0 +1,23 @@
const fs = require("fs");
/**
* Detect if `fs.rmSync` is available
* to avoid the runtime deprecation warning triggered for using `fs.rmdirSync` with `{ recursive: true }` in Node.js v16,
* or the `recursive` property removing completely in the future Node.js version.
* See the link below.
*
* @todo Once we drop the support for Node.js v14 (or at least versions before v14.14.0), we can safely replace this function with `fs.rmSync`, since `fs.rmSync` was add in Node.js v14.14.0 and currently we supports all the Node.js v14 versions that include the versions before the v14.14.0, and this function have almost the same signature with `fs.rmSync`.
* @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true- the deprecation infomation of `fs.rmdirSync`
* @link https://nodejs.org/docs/latest-v16.x/api/fs.html#fsrmsyncpath-options the document of `fs.rmSync`
* @param {fs.PathLike} path Valid types for path values in "fs".
* @param {fs.RmDirOptions} [options] options for `fs.rmdirSync`, if `fs.rmSync` is available and property `recursive` is true, it will automatically have property `force` with value `true`.
*/
const rmSync = (path, options) => {
if (typeof fs.rmSync === "function") {
if (options.recursive) {
options.force = true;
}
return fs.rmSync(path, options);
}
return fs.rmdirSync(path, options);
};
module.exports = rmSync;

@ -20,7 +20,7 @@ if (sslKey && sslCert) {
// Dual-stack support for (::) // Dual-stack support for (::)
let hostname = process.env.UPTIME_KUMA_HOST; let hostname = process.env.UPTIME_KUMA_HOST;
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD // Also read HOST if not *BSD, as HOST is a system environment variable in FreeBSD
if (!hostname && !FBSD) { if (!hostname && !FBSD) {
hostname = process.env.HOST; hostname = process.env.HOST;
} }

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

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

@ -1,7 +1,5 @@
console.log("== Uptime Kuma Reset Password Tool =="); console.log("== Uptime Kuma Reset Password Tool ==");
console.log("Loading the database");
const Database = require("../server/database"); const Database = require("../server/database");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const readline = require("readline"); const readline = require("readline");
@ -13,8 +11,9 @@ const rl = readline.createInterface({
}); });
const main = async () => { const main = async () => {
console.log("Connecting the database");
Database.init(args); Database.init(args);
await Database.connect(); await Database.connect(false, false, true);
try { try {
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now. // No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.

@ -3,6 +3,7 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import util from "util"; import util from "util";
import rmSync from "../fs-rmSync.js";
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js // https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
/** /**
@ -30,7 +31,7 @@ console.log("Arguments:", process.argv);
const baseLangCode = process.argv[2] || "en"; const baseLangCode = process.argv[2] || "en";
console.log("Base Lang: " + baseLangCode); console.log("Base Lang: " + baseLangCode);
if (fs.existsSync("./languages")) { if (fs.existsSync("./languages")) {
fs.rmdirSync("./languages", { recursive: true }); rmSync("./languages", { recursive: true });
} }
copyRecursiveSync("../../src/languages", "./languages"); copyRecursiveSync("../../src/languages", "./languages");
@ -82,5 +83,5 @@ for (const file of files) {
fs.writeFileSync(`../../src/languages/${file}`, code); fs.writeFileSync(`../../src/languages/${file}`, code);
} }
fs.rmdirSync("./languages", { recursive: true }); rmSync("./languages", { recursive: true });
console.log("Done. Fixing formatting by ESLint..."); console.log("Done. Fixing formatting by ESLint...");

@ -1,14 +1,13 @@
const pkg = require("../package.json"); const pkg = require("../package.json");
const fs = require("fs"); const fs = require("fs");
const rmSync = require("./fs-rmSync.js");
const child_process = require("child_process"); const child_process = require("child_process");
const util = require("../src/util"); const util = require("../src/util");
util.polyfill(); util.polyfill();
const oldVersion = pkg.version; const newVersion = process.env.VERSION;
const newVersion = process.argv[2];
console.log("Old Version: " + oldVersion);
console.log("New Version: " + newVersion); console.log("New Version: " + newVersion);
if (! newVersion) { if (! newVersion) {
@ -22,23 +21,26 @@ if (! exists) {
// Process package.json // Process package.json
pkg.version = newVersion; pkg.version = newVersion;
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion); // Replace the version: https://regex101.com/r/hmj2Bc/1
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion); pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion);
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
commit(newVersion); commit(newVersion);
tag(newVersion); tag(newVersion);
updateWiki(oldVersion, newVersion);
} else { } else {
console.log("version exists"); console.log("version exists");
} }
/**
* Updates the version number in package.json and commits it to git.
* @param {string} version - The new version number
*
* Generated by Trelent
*/
function commit(version) { function commit(version) {
let msg = "update to " + version; let msg = "Update to " + version;
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]); let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
let stdout = res.stdout.toString().trim(); let stdout = res.stdout.toString().trim();
@ -54,6 +56,12 @@ function tag(version) {
console.log(res.stdout.toString().trim()); console.log(res.stdout.toString().trim());
} }
/**
* Checks if a given version is already tagged in the git repository.
* @param {string} version - The version to check for.
*
* Generated by Trelent
*/
function tagExists(version) { function tagExists(version) {
if (! version) { if (! version) {
throw new Error("invalid version"); throw new Error("invalid version");
@ -63,38 +71,3 @@ function tagExists(version) {
return res.stdout.toString().trim() === version; return res.stdout.toString().trim() === version;
} }
function updateWiki(oldVersion, newVersion) {
const wikiDir = "./tmp/wiki";
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
safeDelete(wikiDir);
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
let content = fs.readFileSync(howToUpdateFilename).toString();
content = content.replaceAll(`git checkout ${oldVersion}`, `git checkout ${newVersion}`);
fs.writeFileSync(howToUpdateFilename, content);
child_process.spawnSync("git", ["add", "-A"], {
cwd: wikiDir,
});
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion} from ${oldVersion}`], {
cwd: wikiDir,
});
console.log("Pushing to Github");
child_process.spawnSync("git", ["push"], {
cwd: wikiDir,
});
safeDelete(wikiDir);
}
function safeDelete(dir) {
if (fs.existsSync(dir)) {
fs.rmdirSync(dir, {
recursive: true,
});
}
}

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

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

24056
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.11.1", "version": "1.14.0",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,18 +27,17 @@
"jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js", "jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
"tsc": "tsc", "tsc": "tsc",
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js", "vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine", "build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push", "build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push", "build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
"build-docker-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.11.1-alpine --target release . --push", "build-docker-alpine": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:$VERSION-alpine --target release . --push",
"build-docker-debian": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.11.1 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.11.1-debian --target release . --push", "build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
"build-docker-nightly": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push", "build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push", "build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", "build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain", "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.11.1 && npm ci --production && npm run download-dist", "setup": "git checkout 1.14.0 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js", "download-dist": "node extra/download-dist.js",
"update-version": "node extra/update-version.js",
"mark-as-nightly": "node extra/mark-as-nightly.js", "mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js", "reset-password": "node extra/reset-password.js",
"remove-2fa": "node extra/remove-2fa.js", "remove-2fa": "node extra/remove-2fa.js",
@ -51,7 +50,10 @@
"simple-dns-server": "node extra/simple-dns-server.js", "simple-dns-server": "node extra/simple-dns-server.js",
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix", "update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix", "update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
"ncu-patch": "ncu -u -t patch" "ncu-patch": "npm-check-updates -u -t patch",
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
"git-remove-tag": "git tag -d"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "~1.2.36", "@fortawesome/fontawesome-svg-core": "~1.2.36",
@ -61,61 +63,66 @@
"@louislam/sqlite3": "~6.0.1", "@louislam/sqlite3": "~6.0.1",
"@popperjs/core": "~2.10.2", "@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0", "args-parser": "~1.3.0",
"axios": "~0.21.4", "axios": "~0.26.1",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"bootstrap": "5.1.3", "bootstrap": "5.1.3",
"bree": "~7.1.0", "bree": "~7.1.5",
"chardet": "^1.3.0", "chardet": "^1.3.0",
"chart.js": "~3.6.0", "chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0", "chartjs-adapter-dayjs": "~1.0.0",
"check-password-strength": "^2.0.3", "check-password-strength": "^2.0.5",
"command-exists": "~1.2.9", "command-exists": "~1.2.9",
"compare-versions": "~3.6.0", "compare-versions": "~3.6.0",
"dayjs": "~1.10.7", "dayjs": "~1.10.8",
"express": "~4.17.1", "express": "~4.17.3",
"express-basic-auth": "~1.2.0", "express-basic-auth": "~1.2.1",
"favico.js": "^0.3.10",
"form-data": "~4.0.0", "form-data": "~4.0.0",
"http-graceful-shutdown": "~3.1.5", "http-graceful-shutdown": "~3.1.7",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.0",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"jsonwebtoken": "~8.5.1", "jsonwebtoken": "~8.5.1",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"limiter": "^2.1.0", "limiter": "^2.1.0",
"node-cloudflared-tunnel": "~1.0.9",
"nodemailer": "~6.6.5", "nodemailer": "~6.6.5",
"notp": "~2.0.3", "notp": "~2.0.3",
"password-hash": "~1.2.2", "password-hash": "~1.2.2",
"postcss-rtlcss": "~3.4.1", "postcss-rtlcss": "~3.4.1",
"postcss-scss": "~4.0.2", "postcss-scss": "~4.0.3",
"prom-client": "~13.2.0", "prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.0", "prometheus-api-metrics": "~3.2.1",
"qrcode": "~1.5.0", "qrcode": "~1.5.0",
"redbean-node": "0.1.3", "redbean-node": "0.1.3",
"socket.io": "~4.2.0", "socket.io": "~4.4.1",
"socket.io-client": "~4.2.0", "socket.io-client": "~4.4.1",
"socks-proxy-agent": "^6.1.1",
"tar": "^6.1.11", "tar": "^6.1.11",
"tcp-ping": "~0.1.1", "tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2", "thirty-two": "~1.0.2",
"timezones-list": "~3.0.1", "timezones-list": "~3.0.1",
"v-pagination-3": "~0.1.7", "v-pagination-3": "~0.1.7",
"vue": "next", "vue": "next",
"vue-chart-3": "~0.5.11", "vue-chart-3": "3.0.9",
"vue-confirm-dialog": "~1.0.2", "vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "~3.0.4", "vue-contenteditable": "~3.0.4",
"vue-i18n": "~9.1.9", "vue-i18n": "~9.1.9",
"vue-image-crop-upload": "~3.0.3", "vue-image-crop-upload": "~3.0.3",
"vue-multiselect": "~3.0.0-alpha.2", "vue-multiselect": "~3.0.0-alpha.2",
"vue-qrcode": "~1.0.0", "vue-qrcode": "~1.0.0",
"vue-router": "~4.0.12", "vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5", "vue-toastification": "~2.0.0-rc.5",
"vuedraggable": "~4.1.0" "vuedraggable": "~4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "~5.0.0", "@actions/github": "~5.0.1",
"@babel/eslint-parser": "~7.15.8", "@babel/eslint-parser": "~7.15.8",
"@babel/preset-env": "^7.15.8", "@babel/preset-env": "^7.15.8",
"@types/bootstrap": "~5.1.6", "@types/bootstrap": "~5.1.9",
"@vitejs/plugin-legacy": "~1.6.3", "@vitejs/plugin-legacy": "~1.6.4",
"@vitejs/plugin-vue": "~1.9.4", "@vitejs/plugin-vue": "~1.9.4",
"@vue/compiler-sfc": "~3.2.22", "@vue/compiler-sfc": "~3.2.31",
"babel-plugin-rewire": "~1.2.0", "babel-plugin-rewire": "~1.2.0",
"core-js": "~3.18.3", "core-js": "~3.18.3",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
@ -123,11 +130,12 @@
"eslint": "~7.32.0", "eslint": "~7.32.0",
"eslint-plugin-vue": "~7.18.0", "eslint-plugin-vue": "~7.18.0",
"jest": "~27.2.5", "jest": "~27.2.5",
"jest-puppeteer": "~6.0.0", "jest-puppeteer": "~6.0.3",
"puppeteer": "~10.4.0", "npm-check-updates": "^12.5.5",
"puppeteer": "~13.1.3",
"sass": "~1.42.1", "sass": "~1.42.1",
"stylelint": "~13.13.1", "stylelint": "~14.2.0",
"stylelint-config-standard": "~22.0.0", "stylelint-config-standard": "~24.0.0",
"typescript": "~4.4.4", "typescript": "~4.4.4",
"vite": "~2.6.14" "vite": "~2.6.14"
} }

@ -12,6 +12,10 @@ const { loginRateLimiter } = require("./rate-limiter");
* @returns {Promise<Bean|null>} * @returns {Promise<Bean|null>}
*/ */
exports.login = async function (username, password) { exports.login = async function (username, password) {
if (typeof username !== "string" || typeof password !== "string") {
return null;
}
let user = await R.findOne("user", " username = ? AND active = 1 ", [ let user = await R.findOne("user", " username = ? AND active = 1 ", [
username, username,
]); ]);
@ -30,11 +34,14 @@ exports.login = async function (username, password) {
return null; return null;
}; };
/**
* A function that checks if a user is logged in.
* @param {string} username The username of the user to check for.
* @param {function} callback The callback to call when done, with an error and result parameter.
*
* Generated by Trelent
*/
function myAuthorizer(username, password, callback) { function myAuthorizer(username, password, callback) {
setting("disableAuth").then((result) => {
if (result) {
callback(null, true);
} else {
// Login Rate Limit // Login Rate Limit
loginRateLimiter.pass(null, 0).then((pass) => { loginRateLimiter.pass(null, 0).then((pass) => {
if (pass) { if (pass) {
@ -49,13 +56,20 @@ function myAuthorizer(username, password, callback) {
callback(null, false); callback(null, false);
} }
}); });
}
});
} }
exports.basicAuth = basicAuth({ exports.basicAuth = async function (req, res, next) {
const middleware = basicAuth({
authorizer: myAuthorizer, authorizer: myAuthorizer,
authorizeAsync: true, authorizeAsync: true,
challenge: true, challenge: true,
}); });
const disabledAuth = await setting("disableAuth");
if (!disabledAuth) {
middleware(req, res, next);
} else {
next();
}
};

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

@ -7,6 +7,12 @@ const { io } = require("./server");
const { setting } = require("./util-server"); const { setting } = require("./util-server");
const checkVersion = require("./check-version"); const checkVersion = require("./check-version");
/**
* Send a list of notifications to the user.
* @param {Socket} socket The socket object that is connected to the client.
*
* Generated by Trelent
*/
async function sendNotificationList(socket) { async function sendNotificationList(socket) {
const timeLogger = new TimeLogger(); const timeLogger = new TimeLogger();
@ -83,6 +89,29 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
} }
/**
* Delivers proxy list
*
* @param socket
* @return {Promise<Bean[]>}
*/
async function sendProxyList(socket) {
const timeLogger = new TimeLogger();
const list = await R.find("proxy", " user_id = ? ", [socket.userID]);
io.to(socket.userID).emit("proxyList", list.map(bean => bean.export()));
timeLogger.print("Send Proxy List");
return list;
}
/**
* Emits the version information to the client.
* @param {Socket} socket The socket object that is connected to the client.
*
* Generated by Trelent
*/
async function sendInfo(socket) { async function sendInfo(socket) {
socket.emit("info", { socket.emit("info", {
version: checkVersion.version, version: checkVersion.version,
@ -95,6 +124,6 @@ module.exports = {
sendNotificationList, sendNotificationList,
sendImportantHeartbeatList, sendImportantHeartbeatList,
sendHeartbeatList, sendHeartbeatList,
sendInfo sendProxyList,
sendInfo,
}; };

@ -53,6 +53,9 @@ class Database {
"patch-2fa-invalidate-used-token.sql": true, "patch-2fa-invalidate-used-token.sql": true,
"patch-notification_sent_history.sql": true, "patch-notification_sent_history.sql": true,
"patch-monitor-basic-auth.sql": true, "patch-monitor-basic-auth.sql": true,
"patch-status-page.sql": true,
"patch-proxy.sql": true,
"patch-monitor-expiry-notification.sql": true,
} }
/** /**
@ -80,7 +83,7 @@ class Database {
console.log(`Data Dir: ${Database.dataDir}`); console.log(`Data Dir: ${Database.dataDir}`);
} }
static async connect(testMode = false) { static async connect(testMode = false, autoloadModels = true, noLog = false) {
const acquireConnectionTimeout = 120 * 1000; const acquireConnectionTimeout = 120 * 1000;
const Dialect = require("knex/lib/dialects/sqlite3/index.js"); const Dialect = require("knex/lib/dialects/sqlite3/index.js");
@ -110,7 +113,10 @@ class Database {
// Auto map the model to a bean object // Auto map the model to a bean object
R.freeze(true); R.freeze(true);
if (autoloadModels) {
await R.autoloadModels("./server/model"); await R.autoloadModels("./server/model");
}
await R.exec("PRAGMA foreign_keys = ON"); await R.exec("PRAGMA foreign_keys = ON");
if (testMode) { if (testMode) {
@ -123,11 +129,18 @@ class Database {
await R.exec("PRAGMA cache_size = -12000"); await R.exec("PRAGMA cache_size = -12000");
await R.exec("PRAGMA auto_vacuum = FULL"); await R.exec("PRAGMA auto_vacuum = FULL");
// This ensures that an operating system crash or power failure will not corrupt the database.
// FULL synchronous is very safe, but it is also slower.
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
await R.exec("PRAGMA synchronous = FULL");
if (!noLog) {
console.log("SQLite config:"); console.log("SQLite config:");
console.log(await R.getAll("PRAGMA journal_mode")); console.log(await R.getAll("PRAGMA journal_mode"));
console.log(await R.getAll("PRAGMA cache_size")); console.log(await R.getAll("PRAGMA cache_size"));
console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()")); console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
} }
}
static async patch() { static async patch() {
let version = parseInt(await setting("database_version")); let version = parseInt(await setting("database_version"));
@ -170,6 +183,7 @@ class Database {
} }
await this.patch2(); await this.patch2();
await this.migrateNewStatusPage();
} }
/** /**
@ -211,6 +225,74 @@ class Database {
await setSetting("databasePatchedFiles", databasePatchedFiles); await setSetting("databasePatchedFiles", databasePatchedFiles);
} }
/**
* Migrate status page value in setting to "status_page" table
* @returns {Promise<void>}
*/
static async migrateNewStatusPage() {
// Fix 1.13.0 empty slug bug
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
let title = await setting("title");
if (title) {
console.log("Migrating Status Page");
let statusPageCheck = await R.findOne("status_page", " slug = 'default' ");
if (statusPageCheck !== null) {
console.log("Migrating Status Page - Skip, default slug record is already existing");
return;
}
let statusPage = R.dispense("status_page");
statusPage.slug = "default";
statusPage.title = title;
statusPage.description = await setting("description");
statusPage.icon = await setting("icon");
statusPage.theme = await setting("statusPageTheme");
statusPage.published = !!await setting("statusPagePublished");
statusPage.search_engine_index = !!await setting("searchEngineIndex");
statusPage.show_tags = !!await setting("statusPageTags");
statusPage.password = null;
if (!statusPage.title) {
statusPage.title = "My Status Page";
}
if (!statusPage.icon) {
statusPage.icon = "";
}
if (!statusPage.theme) {
statusPage.theme = "light";
}
let id = await R.store(statusPage);
await R.exec("UPDATE incident SET status_page_id = ? WHERE status_page_id IS NULL", [
id
]);
await R.exec("UPDATE [group] SET status_page_id = ? WHERE status_page_id IS NULL", [
id
]);
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
// Migrate Entry Page if it is status page
let entryPage = await setting("entryPage");
if (entryPage === "statusPage") {
await setSetting("entryPage", "statusPage-default", "general");
}
console.log("Migrating Status Page - Done");
}
}
/** /**
* Used it patch2() only * Used it patch2() only
* @param sqlFilename * @param sqlFilename

@ -6,6 +6,12 @@ let fs = require("fs");
let ImageDataURI = (() => { let ImageDataURI = (() => {
/**
* @param {string} dataURI - A string that is a valid Data URI.
* @returns {?Object} An object with properties "imageType" and "dataBase64". The former is the image type, e.g., "png", and the latter is a base64 encoded string of the image's binary data. If it fails to parse, returns null instead of an object.
*
* Generated by Trelent
*/
function decode(dataURI) { function decode(dataURI) {
if (!/data:image\//.test(dataURI)) { if (!/data:image\//.test(dataURI)) {
console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\""); console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\"");
@ -20,6 +26,13 @@ let ImageDataURI = (() => {
}; };
} }
/**
* @param {Buffer} data - The image data to be encoded.
* @param {String} mediaType - The type of the image, e.g., "image/png".
* @returns {String|null} A string representing the base64-encoded version of the given Buffer object or null if an error occurred.
*
* Generated by Trelent
*/
function encode(data, mediaType) { function encode(data, mediaType) {
if (!data || !mediaType) { if (!data || !mediaType) {
console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType "); console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType ");
@ -33,6 +46,13 @@ let ImageDataURI = (() => {
return dataImgBase64; return dataImgBase64;
} }
/**
* Converts a data URI to a file path.
* @param {string} dataURI The Data URI of the image.
* @param {string} [filePath] The path where the image will be saved, defaults to "./".
*
* Generated by Trelent
*/
function outputFile(dataURI, filePath) { function outputFile(dataURI, filePath) {
filePath = filePath || "./"; filePath = filePath || "./";
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

@ -1,7 +1,7 @@
const path = require("path"); const path = require("path");
const Bree = require("bree"); const Bree = require("bree");
const { SHARE_ENV } = require("worker_threads"); const { SHARE_ENV } = require("worker_threads");
let bree;
const jobs = [ const jobs = [
{ {
name: "clear-old-data", name: "clear-old-data",
@ -10,7 +10,7 @@ const jobs = [
]; ];
const initBackgroundJobs = function (args) { const initBackgroundJobs = function (args) {
const bree = new Bree({ bree = new Bree({
root: path.resolve("server", "jobs"), root: path.resolve("server", "jobs"),
jobs, jobs,
worker: { worker: {
@ -26,6 +26,13 @@ const initBackgroundJobs = function (args) {
return bree; return bree;
}; };
const stopBackgroundJobs = function () {
if (bree) {
bree.stop();
}
};
module.exports = { module.exports = {
initBackgroundJobs initBackgroundJobs,
stopBackgroundJobs
}; };

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

@ -11,6 +11,7 @@ const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalCli
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification"); const { Notification } = require("../notification");
const { Proxy } = require("../proxy");
const { demoMode } = require("../config"); const { demoMode } = require("../config");
const version = require("../../package.json").version; const version = require("../../package.json").version;
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
@ -24,18 +25,22 @@ const apicache = require("../modules/apicache");
class Monitor extends BeanModel { class Monitor extends BeanModel {
/** /**
* Return a object that ready to parse to JSON for public * Return an object that ready to parse to JSON for public
* Only show necessary data to public * Only show necessary data to public
*/ */
async toPublicJSON() { async toPublicJSON(showTags = false) {
return { let obj = {
id: this.id, id: this.id,
name: this.name, name: this.name,
}; };
if (showTags) {
obj.tags = await this.getTags();
}
return obj;
} }
/** /**
* Return a object that ready to parse to JSON * Return an object that ready to parse to JSON
*/ */
async toJSON() { async toJSON() {
@ -49,7 +54,7 @@ class Monitor extends BeanModel {
notificationIDList[bean.notification_id] = true; notificationIDList[bean.notification_id] = true;
} }
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]); const tags = await this.getTags();
return { return {
id: this.id, id: this.id,
@ -69,6 +74,7 @@ class Monitor extends BeanModel {
interval: this.interval, interval: this.interval,
retryInterval: this.retryInterval, retryInterval: this.retryInterval,
keyword: this.keyword, keyword: this.keyword,
expiryNotification: this.isEnabledExpiryNotification(),
ignoreTls: this.getIgnoreTls(), ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(), upsideDown: this.isUpsideDown(),
maxredirects: this.maxredirects, maxredirects: this.maxredirects,
@ -77,11 +83,16 @@ class Monitor extends BeanModel {
dns_resolve_server: this.dns_resolve_server, dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result, dns_last_result: this.dns_last_result,
pushToken: this.pushToken, pushToken: this.pushToken,
proxyId: this.proxy_id,
notificationIDList, notificationIDList,
tags: tags, tags: tags,
}; };
} }
async getTags() {
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
}
/** /**
* Encode user and password to Base64 encoding * Encode user and password to Base64 encoding
* for HTTP "basic" auth, as per RFC-7617 * for HTTP "basic" auth, as per RFC-7617
@ -91,6 +102,10 @@ class Monitor extends BeanModel {
return Buffer.from(user + ":" + pass).toString("base64"); return Buffer.from(user + ":" + pass).toString("base64");
} }
isEnabledExpiryNotification() {
return Boolean(this.expiryNotification);
}
/** /**
* Parse to boolean * Parse to boolean
* @returns {boolean} * @returns {boolean}
@ -119,6 +134,19 @@ class Monitor extends BeanModel {
const beat = async () => { const beat = async () => {
let beatInterval = this.interval;
if (! beatInterval) {
beatInterval = 1;
}
if (demoMode) {
if (beatInterval < 20) {
console.log("beat interval too low, reset to 20s");
beatInterval = 20;
}
}
// Expose here for prometheus update // Expose here for prometheus update
// undefined if not https // undefined if not https
let tlsInfo = undefined; let tlsInfo = undefined;
@ -160,6 +188,11 @@ class Monitor extends BeanModel {
}; };
} }
const httpsAgentOptions = {
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
};
debug(`[${this.name}] Prepare Options for axios`); debug(`[${this.name}] Prepare Options for axios`);
const options = { const options = {
@ -173,17 +206,33 @@ class Monitor extends BeanModel {
...(this.headers ? JSON.parse(this.headers) : {}), ...(this.headers ? JSON.parse(this.headers) : {}),
...(basicAuthHeader), ...(basicAuthHeader),
}, },
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: ! this.getIgnoreTls(),
}),
maxRedirects: this.maxredirects, maxRedirects: this.maxredirects,
validateStatus: (status) => { validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes()); return checkStatusCode(status, this.getAcceptedStatuscodes());
}, },
}; };
if (this.proxy_id) {
const proxy = await R.load("proxy", this.proxy_id);
if (proxy && proxy.active) {
const { httpAgent, httpsAgent } = Proxy.createAgents(proxy, {
httpsAgentOptions: httpsAgentOptions,
});
options.proxy = false;
options.httpAgent = httpAgent;
options.httpsAgent = httpsAgent;
}
}
if (!options.httpsAgent) {
options.httpsAgent = new https.Agent(httpsAgentOptions);
}
debug(`[${this.name}] Axios Options: ${JSON.stringify(options)}`);
debug(`[${this.name}] Axios Request`); debug(`[${this.name}] Axios Request`);
let res = await axios.request(options); let res = await axios.request(options);
bean.msg = `${res.status} - ${res.statusText}`; bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;
@ -196,7 +245,7 @@ class Monitor extends BeanModel {
let tlsInfoObject = checkCertificate(res); let tlsInfoObject = checkCertificate(res);
tlsInfo = await this.updateTlsInfo(tlsInfoObject); tlsInfo = await this.updateTlsInfo(tlsInfoObject);
if (!this.getIgnoreTls()) { if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
debug(`[${this.name}] call sendCertNotification`); debug(`[${this.name}] call sendCertNotification`);
await this.sendCertNotification(tlsInfoObject); await this.sendCertNotification(tlsInfoObject);
} }
@ -303,7 +352,7 @@ class Monitor extends BeanModel {
} else { } else {
// No need to insert successful heartbeat for push type, so end here // No need to insert successful heartbeat for push type, so end here
retries = 0; retries = 0;
this.heartbeatInterval = setTimeout(beat, this.interval * 1000); this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
return; return;
} }
@ -377,8 +426,6 @@ class Monitor extends BeanModel {
} }
} }
let beatInterval = this.interval;
debug(`[${this.name}] Check isImportant`); debug(`[${this.name}] Check isImportant`);
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status); let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
@ -422,14 +469,6 @@ class Monitor extends BeanModel {
previousBeat = bean; previousBeat = bean;
if (! this.isStop) { if (! this.isStop) {
if (demoMode) {
if (beatInterval < 20) {
console.log("beat interval too low, reset to 20s");
beatInterval = 20;
}
}
debug(`[${this.name}] SetTimeout for next check.`); debug(`[${this.name}] SetTimeout for next check.`);
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000); this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
} else { } else {
@ -466,6 +505,12 @@ class Monitor extends BeanModel {
stop() { stop() {
clearTimeout(this.heartbeatInterval); clearTimeout(this.heartbeatInterval);
this.isStop = true; this.isStop = true;
this.prometheus().remove();
}
prometheus() {
return new Prometheus(this);
} }
/** /**

@ -0,0 +1,21 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
class Proxy extends BeanModel {
toJSON() {
return {
id: this._id,
userId: this._user_id,
protocol: this._protocol,
host: this._host,
port: this._port,
auth: !!this._auth,
username: this._username,
password: this._password,
active: !!this._active,
default: !!this._default,
createdDate: this._created_date,
};
}
}
module.exports = Proxy;

@ -0,0 +1,126 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
class StatusPage extends BeanModel {
static domainMappingList = { };
/**
* Return object like this: { "test-uptime.kuma.pet": "default" }
* @returns {Promise<void>}
*/
static async loadDomainMappingList() {
StatusPage.domainMappingList = await R.getAssoc(`
SELECT domain, slug
FROM status_page, status_page_cname
WHERE status_page.id = status_page_cname.status_page_id
`);
}
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 updateDomainNameList(domainNameList) {
if (!Array.isArray(domainNameList)) {
throw new Error("Invalid array");
}
let trx = await R.begin();
await trx.exec("DELETE FROM status_page_cname WHERE status_page_id = ?", [
this.id,
]);
try {
for (let domain of domainNameList) {
if (typeof domain !== "string") {
throw new Error("Invalid domain");
}
if (domain.trim() === "") {
continue;
}
// If the domain name is used in another status page, delete it
await trx.exec("DELETE FROM status_page_cname WHERE domain = ?", [
domain,
]);
let mapping = trx.dispense("status_page_cname");
mapping.status_page_id = this.id;
mapping.domain = domain;
await trx.store(mapping);
}
await trx.commit();
} catch (error) {
await trx.rollback();
throw error;
}
}
getDomainNameList() {
let domainList = [];
for (let domain in StatusPage.domainMappingList) {
let s = StatusPage.domainMappingList[domain];
if (this.slug === s) {
domainList.push(domain);
}
}
return domainList;
}
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,
domainNameList: this.getDomainNameList(),
};
}
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;

@ -68,6 +68,15 @@ function ApiCache() {
instances.push(this); instances.push(this);
this.id = instances.length; this.id = instances.length;
/**
* Logs a message to the console if the `DEBUG` environment variable is set.
* @param {string} a - The first argument to log.
* @param {string} b - The second argument to log.
* @param {string} c - The third argument to log.
* @param {string} d - The fourth argument to log, and so on... (optional)
*
* Generated by Trelent
*/
function debug(a, b, c, d) { function debug(a, b, c, d) {
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) { let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) {
return arg !== undefined; return arg !== undefined;
@ -77,6 +86,13 @@ function ApiCache() {
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr); return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
} }
/**
* Returns true if the given request and response should be logged.
* @param {Object} request The HTTP request object.
* @param {Object} response The HTTP response object.
*
* Generated by Trelent
*/
function shouldCacheResponse(request, response, toggle) { function shouldCacheResponse(request, response, toggle) {
let opt = globalOptions; let opt = globalOptions;
let codes = opt.statusCodes; let codes = opt.statusCodes;
@ -99,6 +115,12 @@ function ApiCache() {
return true; return true;
} }
/**
* Adds a key to the index.
* @param {string} key The key to add.
*
* Generated by Trelent
*/
function addIndexEntries(key, req) { function addIndexEntries(key, req) {
let groupName = req.apicacheGroup; let groupName = req.apicacheGroup;
@ -111,6 +133,13 @@ function ApiCache() {
index.all.unshift(key); index.all.unshift(key);
} }
/**
* Returns a new object containing only the whitelisted headers.
* @param {Object} headers The original object of header names and values.
* @param {Array.<string>} globalOptions.headerWhitelist An array of strings representing the whitelisted header names to keep in the output object.
*
* Generated by Trelent
*/
function filterBlacklistedHeaders(headers) { function filterBlacklistedHeaders(headers) {
return Object.keys(headers) return Object.keys(headers)
.filter(function (key) { .filter(function (key) {
@ -122,6 +151,12 @@ function ApiCache() {
}, {}); }, {});
} }
/**
* @param {Object} headers The response headers to filter.
* @returns {Object} A new object containing only the whitelisted response headers.
*
* Generated by Trelent
*/
function createCacheObject(status, headers, data, encoding) { function createCacheObject(status, headers, data, encoding) {
return { return {
status: status, status: status,
@ -132,6 +167,14 @@ function ApiCache() {
}; };
} }
/**
* Sets a cache value for the given key.
* @param {string} key The cache key to set.
* @param {*} value The cache value to set.
* @param {number} duration How long in milliseconds the cached response should be valid for (defaults to 1 hour).
*
* Generated by Trelent
*/
function cacheResponse(key, value, duration) { function cacheResponse(key, value, duration) {
let redis = globalOptions.redisClient; let redis = globalOptions.redisClient;
let expireCallback = globalOptions.events.expire; let expireCallback = globalOptions.events.expire;
@ -154,6 +197,12 @@ function ApiCache() {
}, Math.min(duration, 2147483647)); }, Math.min(duration, 2147483647));
} }
/**
* Appends content to the response.
* @param {string|Buffer} content The content to append.
*
* Generated by Trelent
*/
function accumulateContent(res, content) { function accumulateContent(res, content) {
if (content) { if (content) {
if (typeof content == "string") { if (typeof content == "string") {
@ -179,6 +228,13 @@ function ApiCache() {
} }
} }
/**
* Monkeypatches the response object to add cache control headers and create a cache object.
* @param {Object} req - The request object.
* @param {Object} res - The response object.
*
* Generated by Trelent
*/
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) { function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
// monkeypatch res.end to create cache object // monkeypatch res.end to create cache object
res._apicache = { res._apicache = {
@ -245,6 +301,13 @@ function ApiCache() {
next(); next();
} }
/**
* @param {Request} request
* @param {Response} response
* @returns {boolean|undefined} true if the request should be cached, false otherwise. If undefined, defaults to true.
*
* Generated by Trelent
*/
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) { function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
if (toggle && !toggle(request, response)) { if (toggle && !toggle(request, response)) {
return next(); return next();
@ -365,6 +428,13 @@ function ApiCache() {
return this.getIndex(); return this.getIndex();
}; };
/**
* Converts a duration string to an integer number of milliseconds.
* @param {string} duration - The string to convert.
* @returns {number} The converted value in milliseconds, or the defaultDuration if it can't be parsed.
*
* Generated by Trelent
*/
function parseDuration(duration, defaultDuration) { function parseDuration(duration, defaultDuration) {
if (typeof duration === "number") { if (typeof duration === "number") {
return duration; return duration;

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

@ -0,0 +1,47 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setting } = require("../util-server");
const { getMonitorRelativeURL } = require("../../src/util");
const { DOWN, UP } = require("../../src/util");
class GoogleChat extends NotificationProvider {
name = "GoogleChat";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
// Google Chat message formatting: https://developers.google.com/chat/api/guides/message-formats/basic
let textMsg = ''
if (heartbeatJSON && heartbeatJSON.status === UP) {
textMsg = `✅ Application is back online\n`;
} else if (heartbeatJSON && heartbeatJSON.status === DOWN) {
textMsg = `🔴 Application went down\n`;
}
if (monitorJSON && monitorJSON.name) {
textMsg += `*${monitorJSON.name}*\n`;
}
textMsg += `${msg}`;
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorJSON) {
textMsg += `\n${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
}
const data = {
"text": textMsg,
};
await axios.post(notification.googleChatWebhookURL, data);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = GoogleChat;

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

@ -15,12 +15,17 @@ class Mattermost extends NotificationProvider {
let mattermostTestData = { let mattermostTestData = {
username: mattermostUserName, username: mattermostUserName,
text: msg, text: msg,
} };
await axios.post(notification.mattermostWebhookUrl, mattermostTestData) await axios.post(notification.mattermostWebhookUrl, mattermostTestData);
return okMsg; return okMsg;
} }
const mattermostChannel = notification.mattermostchannel; let mattermostChannel;
if (typeof notification.mattermostchannel === "string") {
mattermostChannel = notification.mattermostchannel.toLowerCase();
}
const mattermostIconEmoji = notification.mattermosticonemo; const mattermostIconEmoji = notification.mattermosticonemo;
const mattermostIconUrl = notification.mattermosticonurl; const mattermostIconUrl = notification.mattermosticonurl;

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

@ -14,16 +14,20 @@ class SMTP extends NotificationProvider {
secure: notification.smtpSecure, secure: notification.smtpSecure,
tls: { tls: {
rejectUnauthorized: notification.smtpIgnoreTLSError || false, rejectUnauthorized: notification.smtpIgnoreTLSError || false,
}, }
dkim: { };
// Fix #1129
if (notification.smtpDkimDomain) {
config.dkim = {
domainName: notification.smtpDkimDomain, domainName: notification.smtpDkimDomain,
keySelector: notification.smtpDkimKeySelector, keySelector: notification.smtpDkimKeySelector,
privateKey: notification.smtpDkimPrivateKey, privateKey: notification.smtpDkimPrivateKey,
hashAlgo: notification.smtpDkimHashAlgo, hashAlgo: notification.smtpDkimHashAlgo,
headerFieldNames: notification.smtpDkimheaderFieldNames, headerFieldNames: notification.smtpDkimheaderFieldNames,
skipFields: notification.smtpDkimskipFields, skipFields: notification.smtpDkimskipFields,
}
}; };
}
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904 // Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
if (notification.smtpUsername || notification.smtpPassword) { if (notification.smtpUsername || notification.smtpPassword) {

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

@ -0,0 +1,47 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class WeCom extends NotificationProvider {
name = "WeCom";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let WeComUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + notification.weComBotKey;
let config = {
headers: {
"Content-Type": "application/json"
}
};
let body = this.composeMessage(heartbeatJSON, msg);
await axios.post(WeComUrl, body, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
composeMessage(heartbeatJSON, msg) {
let title;
if (msg != null && heartbeatJSON != null && heartbeatJSON['status'] == UP) {
title = "UptimeKuma Monitor Up";
}
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == DOWN) {
title = "UptimeKuma Monitor Down";
}
if (msg != null) {
title = "UptimeKuma Message";
}
return {
msgtype: "text",
text: {
content: title + msg
}
};
}
}
module.exports = WeCom;

@ -12,6 +12,7 @@ const ClickSendSMS = require("./notification-providers/clicksendsms");
const Pushbullet = require("./notification-providers/pushbullet"); const Pushbullet = require("./notification-providers/pushbullet");
const Pushover = require("./notification-providers/pushover"); const Pushover = require("./notification-providers/pushover");
const Pushy = require("./notification-providers/pushy"); const Pushy = require("./notification-providers/pushy");
const TechulusPush = require("./notification-providers/techulus-push");
const RocketChat = require("./notification-providers/rocket-chat"); const RocketChat = require("./notification-providers/rocket-chat");
const Signal = require("./notification-providers/signal"); const Signal = require("./notification-providers/signal");
const Slack = require("./notification-providers/slack"); const Slack = require("./notification-providers/slack");
@ -25,6 +26,10 @@ const DingDing = require("./notification-providers/dingding");
const Bark = require("./notification-providers/bark"); const Bark = require("./notification-providers/bark");
const SerwerSMS = require("./notification-providers/serwersms"); const SerwerSMS = require("./notification-providers/serwersms");
const Stackfield = require("./notification-providers/stackfield"); const Stackfield = require("./notification-providers/stackfield");
const WeCom = require("./notification-providers/wecom");
const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
const Alerta = require("./notification-providers/alerta");
class Notification { class Notification {
@ -53,6 +58,7 @@ class Notification {
new Pushbullet(), new Pushbullet(),
new Pushover(), new Pushover(),
new Pushy(), new Pushy(),
new TechulusPush(),
new RocketChat(), new RocketChat(),
new Signal(), new Signal(),
new Slack(), new Slack(),
@ -62,6 +68,10 @@ class Notification {
new Bark(), new Bark(),
new SerwerSMS(), new SerwerSMS(),
new Stackfield(), new Stackfield(),
new WeCom(),
new GoogleChat(),
new Gorush(),
new Alerta(),
]; ];
for (let item of list) { for (let item of list) {
@ -144,6 +154,13 @@ class Notification {
} }
/**
* Adds a new monitor to the database.
* @param {number} userID The ID of the user that owns this monitor.
* @param {string} name The name of this monitor.
*
* Generated by Trelent
*/
async function applyNotificationEveryMonitor(notificationID, userID) { async function applyNotificationEveryMonitor(notificationID, userID) {
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [ let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [
userID userID

@ -8,6 +8,13 @@ const util = require("./util-server");
module.exports = Ping; module.exports = Ping;
/**
* @param {string} host - The host to ping
* @param {object} [options] - Options for the ping command
* @param {array|string} [options.args] - Arguments to pass to the ping command
*
* Generated by Trelent
*/
function Ping(host, options) { function Ping(host, options) {
if (!host) { if (!host) {
throw new Error("You must specify a host to ping!"); throw new Error("You must specify a host to ping!");
@ -48,7 +55,7 @@ function Ping(host, options) {
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ]; this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
this._regmatch = /=([0-9.]+?) ms/; this._regmatch = /=([0-9.]+?) ms/;
} else if (util.FBSD) { } else if (util.BSD) {
this._bin = "/sbin/ping"; this._bin = "/sbin/ping";
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ]; const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
@ -125,6 +132,11 @@ Ping.prototype.send = function (callback) {
} }
}); });
/**
* @param {Function} callback
*
* Generated by Trelent
*/
function onEnd() { function onEnd() {
let stdout = this.stdout._stdout; let stdout = this.stdout._stdout;
let stderr = this.stderr._stderr; let stderr = this.stderr._stderr;

@ -60,7 +60,9 @@ class Prometheus {
} }
try { try {
if (tlsInfo.certInfo != null) {
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining); monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@ -84,6 +86,16 @@ class Prometheus {
} }
} }
remove() {
try {
monitor_cert_days_remaining.remove(this.monitorLabelValues);
monitor_cert_is_valid.remove(this.monitorLabelValues);
monitor_response_time.remove(this.monitorLabelValues);
monitor_status.remove(this.monitorLabelValues);
} catch (e) {
console.error(e);
}
}
} }
module.exports = { module.exports = {

@ -0,0 +1,187 @@
const { R } = require("redbean-node");
const HttpProxyAgent = require("http-proxy-agent");
const HttpsProxyAgent = require("https-proxy-agent");
const SocksProxyAgent = require("socks-proxy-agent");
const { debug } = require("../src/util");
const server = require("./server");
class Proxy {
static SUPPORTED_PROXY_PROTOCOLS = ["http", "https", "socks", "socks5", "socks4"]
/**
* Saves and updates given proxy entity
*
* @param proxy
* @param proxyID
* @param userID
* @return {Promise<Bean>}
*/
static async save(proxy, proxyID, userID) {
let bean;
if (proxyID) {
bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [proxyID, userID]);
if (!bean) {
throw new Error("proxy not found");
}
} else {
bean = R.dispense("proxy");
}
// Make sure given proxy protocol is supported
if (!this.SUPPORTED_PROXY_PROTOCOLS.includes(proxy.protocol)) {
throw new Error(`
Unsupported proxy protocol "${proxy.protocol}.
Supported protocols are ${this.SUPPORTED_PROXY_PROTOCOLS.join(", ")}."`
);
}
// When proxy is default update deactivate old default proxy
if (proxy.default) {
await R.exec("UPDATE proxy SET `default` = 0 WHERE `default` = 1");
}
bean.user_id = userID;
bean.protocol = proxy.protocol;
bean.host = proxy.host;
bean.port = proxy.port;
bean.auth = proxy.auth;
bean.username = proxy.username;
bean.password = proxy.password;
bean.active = proxy.active || true;
bean.default = proxy.default || false;
await R.store(bean);
if (proxy.applyExisting) {
await applyProxyEveryMonitor(bean.id, userID);
}
return bean;
}
/**
* Deletes proxy with given id and removes it from monitors
*
* @param proxyID
* @param userID
* @return {Promise<void>}
*/
static async delete(proxyID, userID) {
const bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [proxyID, userID]);
if (!bean) {
throw new Error("proxy not found");
}
// Delete removed proxy from monitors if exists
await R.exec("UPDATE monitor SET proxy_id = null WHERE proxy_id = ?", [proxyID]);
// Delete proxy from list
await R.trash(bean);
}
/**
* Create HTTP and HTTPS agents related with given proxy bean object
*
* @param proxy proxy bean object
* @param options http and https agent options
* @return {{httpAgent: Agent, httpsAgent: Agent}}
*/
static createAgents(proxy, options) {
const { httpAgentOptions, httpsAgentOptions } = options || {};
let agent;
let httpAgent;
let httpsAgent;
const proxyOptions = {
protocol: proxy.protocol,
host: proxy.host,
port: proxy.port,
};
if (proxy.auth) {
proxyOptions.auth = `${proxy.username}:${proxy.password}`;
}
debug(`Proxy Options: ${JSON.stringify(proxyOptions)}`);
debug(`HTTP Agent Options: ${JSON.stringify(httpAgentOptions)}`);
debug(`HTTPS Agent Options: ${JSON.stringify(httpsAgentOptions)}`);
switch (proxy.protocol) {
case "http":
case "https":
httpAgent = new HttpProxyAgent({
...httpAgentOptions || {},
...proxyOptions
});
httpsAgent = new HttpsProxyAgent({
...httpsAgentOptions || {},
...proxyOptions,
});
break;
case "socks":
case "socks5":
case "socks4":
agent = new SocksProxyAgent({
...httpAgentOptions,
...httpsAgentOptions,
...proxyOptions,
});
httpAgent = agent;
httpsAgent = agent;
break;
default: throw new Error(`Unsupported proxy protocol provided. ${proxy.protocol}`);
}
return {
httpAgent,
httpsAgent
};
}
/**
* Reload proxy settings for current monitors
* @returns {Promise<void>}
*/
static async reloadProxy() {
let updatedList = await R.getAssoc("SELECT id, proxy_id FROM monitor");
for (let monitorID in server.monitorList) {
let monitor = server.monitorList[monitorID];
if (updatedList[monitorID]) {
monitor.proxy_id = updatedList[monitorID].proxy_id;
}
}
}
}
/**
* Applies given proxy id to monitors
*
* @param proxyID
* @param userID
* @return {Promise<void>}
*/
async function applyProxyEveryMonitor(proxyID, userID) {
// Find all monitors with id and proxy id
const monitors = await R.getAll("SELECT id, proxy_id FROM monitor WHERE user_id = ?", [userID]);
// Update proxy id not match with given proxy id
for (const monitor of monitors) {
if (monitor.proxy_id !== proxyID) {
await R.exec("UPDATE monitor SET proxy_id = ? WHERE id = ?", [proxyID, monitor.id]);
}
}
}
module.exports = {
Proxy,
};

@ -34,6 +34,14 @@ const loginRateLimiter = new KumaRateLimiter({
errorMessage: "Too frequently, try again later." errorMessage: "Too frequently, try again later."
}); });
const twoFaRateLimiter = new KumaRateLimiter({
tokensPerInterval: 30,
interval: "minute",
fireImmediately: true,
errorMessage: "Too frequently, try again later."
});
module.exports = { module.exports = {
loginRateLimiter loginRateLimiter,
twoFaRateLimiter,
}; };

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

@ -1,4 +1,15 @@
console.log("Welcome to Uptime Kuma"); console.log("Welcome to Uptime Kuma");
// Check Node.js Version
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
const requiredVersion = 14;
console.log(`Your Node.js version: ${nodeVersion}`);
if (nodeVersion < requiredVersion) {
console.error(`Error: Your Node.js version is not supported, please upgrade to Node.js >= ${requiredVersion}.`);
process.exit(-1);
}
const args = require("args-parser")(process.argv); const args = require("args-parser")(process.argv);
const { sleep, debug, getRandomInt, genSecret } = require("../src/util"); const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
const config = require("./config"); const config = require("./config");
@ -37,22 +48,46 @@ debug("Importing 2FA Modules");
const notp = require("notp"); const notp = require("notp");
const base32 = require("thirty-two"); const base32 = require("thirty-two");
/**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
* @type {UptimeKumaServer}
*/
class UptimeKumaServer {
/**
* Main monitor list
* @type {{}}
*/
monitorList = {};
entryPage = "dashboard";
async sendMonitorList(socket) {
let list = await getMonitorJSONList(socket.userID);
io.to(socket.userID).emit("monitorList", list);
return list;
}
}
const server = module.exports = new UptimeKumaServer();
console.log("Importing this project modules"); console.log("Importing this project modules");
debug("Importing Monitor"); debug("Importing Monitor");
const Monitor = require("./model/monitor"); const Monitor = require("./model/monitor");
debug("Importing Settings"); debug("Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog } = require("./util-server"); const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog, doubleCheckPassword } = require("./util-server");
debug("Importing Notification"); debug("Importing Notification");
const { Notification } = require("./notification"); const { Notification } = require("./notification");
Notification.init(); Notification.init();
debug("Importing Proxy");
const { Proxy } = require("./proxy");
debug("Importing Database"); debug("Importing Database");
const Database = require("./database"); const Database = require("./database");
debug("Importing Background Jobs"); debug("Importing Background Jobs");
const { initBackgroundJobs } = require("./jobs"); const { initBackgroundJobs, stopBackgroundJobs } = require("./jobs");
const { loginRateLimiter } = require("./rate-limiter"); const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter");
const { basicAuth } = require("./auth"); const { basicAuth } = require("./auth");
const { login } = require("./auth"); const { login } = require("./auth");
@ -63,23 +98,23 @@ console.info("Version: " + checkVersion.version);
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise. // If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::) // Dual-stack support for (::)
let hostname = process.env.UPTIME_KUMA_HOST || args.host;
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD // Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
if (!hostname && !FBSD) { let hostEnv = FBSD ? null : process.env.HOST;
hostname = process.env.HOST; let hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
}
if (hostname) { if (hostname) {
console.log("Custom hostname: " + hostname); console.log("Custom hostname: " + hostname);
} }
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.port || 3001); const port = [args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001]
.map(portValue => parseInt(portValue))
.find(portValue => !isNaN(portValue));
// SSL // SSL
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined; const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined; const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false; const disableFrameSameOrigin = args["disable-frame-sameorigin"] || !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || false;
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
// 2FA / notp verification defaults // 2FA / notp verification defaults
const twofa_verification_opts = { const twofa_verification_opts = {
@ -100,27 +135,30 @@ if (config.demoMode) {
console.log("Creating express and socket.io instance"); console.log("Creating express and socket.io instance");
const app = express(); const app = express();
let server; let httpServer;
if (sslKey && sslCert) { if (sslKey && sslCert) {
console.log("Server Type: HTTPS"); console.log("Server Type: HTTPS");
server = https.createServer({ httpServer = https.createServer({
key: fs.readFileSync(sslKey), key: fs.readFileSync(sslKey),
cert: fs.readFileSync(sslCert) cert: fs.readFileSync(sslCert)
}, app); }, app);
} else { } else {
console.log("Server Type: HTTP"); console.log("Server Type: HTTP");
server = http.createServer(app); httpServer = http.createServer(app);
} }
const io = new Server(server); const io = new Server(httpServer);
module.exports.io = io; module.exports.io = io;
// Must be after io instantiation // Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo } = require("./client"); const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client");
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler"); const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
const databaseSocketHandler = require("./socket-handlers/database-socket-handler"); const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
const TwoFA = require("./2fa"); const TwoFA = require("./2fa");
const StatusPage = require("./model/status_page");
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
app.use(express.json()); app.use(express.json());
@ -145,12 +183,6 @@ let totalClient = 0;
*/ */
let jwtSecret = null; let jwtSecret = null;
/**
* Main monitor list
* @type {{}}
*/
let monitorList = {};
/** /**
* Show Setup Page * Show Setup Page
* @type {boolean} * @type {boolean}
@ -173,13 +205,12 @@ try {
} }
} }
exports.entryPage = "dashboard";
(async () => { (async () => {
Database.init(args); Database.init(args);
await initDatabase(testMode); await initDatabase(testMode);
exports.entryPage = await setting("entryPage"); exports.entryPage = await setting("entryPage");
await StatusPage.loadDomainMappingList();
console.log("Adding route"); console.log("Adding route");
@ -188,9 +219,14 @@ exports.entryPage = "dashboard";
// *************************** // ***************************
// Entry Page // Entry Page
app.get("/", async (_request, response) => { app.get("/", async (request, response) => {
if (exports.entryPage === "statusPage") { debug(`Request Domain: ${request.hostname}`);
response.redirect("/status");
if (request.hostname in StatusPage.domainMappingList) {
debug("This is a status page domain");
response.send(indexHTML);
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
} else { } else {
response.redirect("/dashboard"); response.redirect("/dashboard");
} }
@ -293,6 +329,15 @@ exports.entryPage = "dashboard";
socket.on("login", async (data, callback) => { socket.on("login", async (data, callback) => {
console.log("Login"); console.log("Login");
// Checking
if (typeof callback !== "function") {
return;
}
if (!data) {
return;
}
// Login Rate Limit // Login Rate Limit
if (! await loginRateLimiter.pass(callback)) { if (! await loginRateLimiter.pass(callback)) {
return; return;
@ -351,14 +396,27 @@ exports.entryPage = "dashboard";
}); });
socket.on("logout", async (callback) => { socket.on("logout", async (callback) => {
// Rate Limit
if (! await loginRateLimiter.pass(callback)) {
return;
}
socket.leave(socket.userID); socket.leave(socket.userID);
socket.userID = null; socket.userID = null;
if (typeof callback === "function") {
callback(); callback();
}
}); });
socket.on("prepare2FA", async (callback) => { socket.on("prepare2FA", async (currentPassword, callback) => {
try { try {
if (! await twoFaRateLimiter.pass(callback)) {
return;
}
checkLogin(socket); checkLogin(socket);
await doubleCheckPassword(socket, currentPassword);
let user = await R.findOne("user", " id = ? AND active = 1 ", [ let user = await R.findOne("user", " id = ? AND active = 1 ", [
socket.userID, socket.userID,
@ -393,14 +451,19 @@ exports.entryPage = "dashboard";
} catch (error) { } catch (error) {
callback({ callback({
ok: false, ok: false,
msg: "Error while trying to prepare 2FA.", msg: error.message,
}); });
} }
}); });
socket.on("save2FA", async (callback) => { socket.on("save2FA", async (currentPassword, callback) => {
try { try {
if (! await twoFaRateLimiter.pass(callback)) {
return;
}
checkLogin(socket); checkLogin(socket);
await doubleCheckPassword(socket, currentPassword);
await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [ await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
socket.userID, socket.userID,
@ -413,14 +476,19 @@ exports.entryPage = "dashboard";
} catch (error) { } catch (error) {
callback({ callback({
ok: false, ok: false,
msg: "Error while trying to change 2FA.", msg: error.message,
}); });
} }
}); });
socket.on("disable2FA", async (callback) => { socket.on("disable2FA", async (currentPassword, callback) => {
try { try {
if (! await twoFaRateLimiter.pass(callback)) {
return;
}
checkLogin(socket); checkLogin(socket);
await doubleCheckPassword(socket, currentPassword);
await TwoFA.disable2FA(socket.userID); await TwoFA.disable2FA(socket.userID);
callback({ callback({
@ -430,12 +498,16 @@ exports.entryPage = "dashboard";
} catch (error) { } catch (error) {
callback({ callback({
ok: false, ok: false,
msg: "Error while trying to change 2FA.", msg: error.message,
}); });
} }
}); });
socket.on("verifyToken", async (token, callback) => { socket.on("verifyToken", async (token, currentPassword, callback) => {
try {
checkLogin(socket);
await doubleCheckPassword(socket, currentPassword);
let user = await R.findOne("user", " id = ? AND active = 1 ", [ let user = await R.findOne("user", " id = ? AND active = 1 ", [
socket.userID, socket.userID,
]); ]);
@ -454,12 +526,19 @@ exports.entryPage = "dashboard";
valid: false, valid: false,
}); });
} }
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
}); });
socket.on("twoFAStatus", async (callback) => { socket.on("twoFAStatus", async (callback) => {
try {
checkLogin(socket); checkLogin(socket);
try {
let user = await R.findOne("user", " id = ? AND active = 1 ", [ let user = await R.findOne("user", " id = ? AND active = 1 ", [
socket.userID, socket.userID,
]); ]);
@ -476,9 +555,10 @@ exports.entryPage = "dashboard";
}); });
} }
} catch (error) { } catch (error) {
console.log(error);
callback({ callback({
ok: false, ok: false,
msg: "Error while trying to get 2FA status.", msg: error.message,
}); });
} }
}); });
@ -539,7 +619,7 @@ exports.entryPage = "dashboard";
await updateMonitorNotification(bean.id, notificationIDList); await updateMonitorNotification(bean.id, notificationIDList);
await sendMonitorList(socket); await server.sendMonitorList(socket);
await startMonitor(socket.userID, bean.id); await startMonitor(socket.userID, bean.id);
callback({ callback({
@ -567,6 +647,9 @@ exports.entryPage = "dashboard";
throw new Error("Permission denied."); throw new Error("Permission denied.");
} }
// Reset Prometheus labels
server.monitorList[monitor.id]?.prometheus()?.remove();
bean.name = monitor.name; bean.name = monitor.name;
bean.type = monitor.type; bean.type = monitor.type;
bean.url = monitor.url; bean.url = monitor.url;
@ -582,12 +665,14 @@ exports.entryPage = "dashboard";
bean.port = monitor.port; bean.port = monitor.port;
bean.keyword = monitor.keyword; bean.keyword = monitor.keyword;
bean.ignoreTls = monitor.ignoreTls; bean.ignoreTls = monitor.ignoreTls;
bean.expiryNotification = monitor.expiryNotification;
bean.upsideDown = monitor.upsideDown; bean.upsideDown = monitor.upsideDown;
bean.maxredirects = monitor.maxredirects; bean.maxredirects = monitor.maxredirects;
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
bean.dns_resolve_type = monitor.dns_resolve_type; bean.dns_resolve_type = monitor.dns_resolve_type;
bean.dns_resolve_server = monitor.dns_resolve_server; bean.dns_resolve_server = monitor.dns_resolve_server;
bean.pushToken = monitor.pushToken; bean.pushToken = monitor.pushToken;
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
await R.store(bean); await R.store(bean);
@ -597,7 +682,7 @@ exports.entryPage = "dashboard";
await restartMonitor(socket.userID, bean.id); await restartMonitor(socket.userID, bean.id);
} }
await sendMonitorList(socket); await server.sendMonitorList(socket);
callback({ callback({
ok: true, ok: true,
@ -617,7 +702,7 @@ exports.entryPage = "dashboard";
socket.on("getMonitorList", async (callback) => { socket.on("getMonitorList", async (callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
await sendMonitorList(socket); await server.sendMonitorList(socket);
callback({ callback({
ok: true, ok: true,
}); });
@ -691,7 +776,7 @@ exports.entryPage = "dashboard";
try { try {
checkLogin(socket); checkLogin(socket);
await startMonitor(socket.userID, monitorID); await startMonitor(socket.userID, monitorID);
await sendMonitorList(socket); await server.sendMonitorList(socket);
callback({ callback({
ok: true, ok: true,
@ -710,7 +795,7 @@ exports.entryPage = "dashboard";
try { try {
checkLogin(socket); checkLogin(socket);
await pauseMonitor(socket.userID, monitorID); await pauseMonitor(socket.userID, monitorID);
await sendMonitorList(socket); await server.sendMonitorList(socket);
callback({ callback({
ok: true, ok: true,
@ -731,9 +816,9 @@ exports.entryPage = "dashboard";
console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`); console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`);
if (monitorID in monitorList) { if (monitorID in server.monitorList) {
monitorList[monitorID].stop(); server.monitorList[monitorID].stop();
delete monitorList[monitorID]; delete server.monitorList[monitorID];
} }
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [ await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
@ -746,7 +831,7 @@ exports.entryPage = "dashboard";
msg: "Deleted Successfully.", msg: "Deleted Successfully.",
}); });
await sendMonitorList(socket); await server.sendMonitorList(socket);
// Clear heartbeat list on client // Clear heartbeat list on client
await sendImportantHeartbeatList(socket, monitorID, true, true); await sendImportantHeartbeatList(socket, monitorID, true, true);
@ -924,21 +1009,13 @@ exports.entryPage = "dashboard";
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length."); throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
} }
let user = await R.findOne("user", " id = ? AND active = 1 ", [ let user = await doubleCheckPassword(socket, password.currentPassword);
socket.userID, await user.resetPassword(password.newPassword);
]);
if (user && passwordHash.verify(password.currentPassword, user.password)) {
user.resetPassword(password.newPassword);
callback({ callback({
ok: true, ok: true,
msg: "Password has been updated successfully.", msg: "Password has been updated successfully.",
}); });
} else {
throw new Error("Incorrect current password");
}
} catch (e) { } catch (e) {
callback({ callback({
@ -965,10 +1042,14 @@ exports.entryPage = "dashboard";
} }
}); });
socket.on("setSettings", async (data, callback) => { socket.on("setSettings", async (data, currentPassword, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
if (data.disableAuth) {
await doubleCheckPassword(socket, currentPassword);
}
await setSettings("general", data); await setSettings("general", data);
exports.entryPage = data.entryPage; exports.entryPage = data.entryPage;
@ -1068,6 +1149,7 @@ exports.entryPage = "dashboard";
console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`); console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`);
let notificationListData = backupData.notificationList; let notificationListData = backupData.notificationList;
let proxyListData = backupData.proxyList;
let monitorListData = backupData.monitorList; let monitorListData = backupData.monitorList;
let version17x = compareVersions.compare(backupData.version, "1.7.0", ">="); let version17x = compareVersions.compare(backupData.version, "1.7.0", ">=");
@ -1075,8 +1157,8 @@ exports.entryPage = "dashboard";
// If the import option is "overwrite" it'll clear most of the tables, except "settings" and "user" // If the import option is "overwrite" it'll clear most of the tables, except "settings" and "user"
if (importHandle == "overwrite") { if (importHandle == "overwrite") {
// Stops every monitor first, so it doesn't execute any heartbeat while importing // Stops every monitor first, so it doesn't execute any heartbeat while importing
for (let id in monitorList) { for (let id in server.monitorList) {
let monitor = monitorList[id]; let monitor = server.monitorList[id];
await monitor.stop(); await monitor.stop();
} }
await R.exec("DELETE FROM heartbeat"); await R.exec("DELETE FROM heartbeat");
@ -1086,6 +1168,7 @@ exports.entryPage = "dashboard";
await R.exec("DELETE FROM monitor_tag"); await R.exec("DELETE FROM monitor_tag");
await R.exec("DELETE FROM tag"); await R.exec("DELETE FROM tag");
await R.exec("DELETE FROM monitor"); await R.exec("DELETE FROM monitor");
await R.exec("DELETE FROM proxy");
} }
// Only starts importing if the backup file contains at least one notification // Only starts importing if the backup file contains at least one notification
@ -1105,6 +1188,24 @@ exports.entryPage = "dashboard";
} }
} }
// Only starts importing if the backup file contains at least one proxy
if (proxyListData.length >= 1) {
const proxies = await R.findAll("proxy");
// Loop over proxy list and save proxies
for (const proxy of proxyListData) {
const exists = proxies.find(item => item.id === proxy.id);
// Do not process when proxy already exists in import handle is skip and keep
if (["skip", "keep"].includes(importHandle) && !exists) {
return;
}
// Save proxy as new entry if exists update exists one
await Proxy.save(proxy, exists ? proxy.id : undefined, proxy.userId);
}
}
// Only starts importing if the backup file contains at least one monitor // Only starts importing if the backup file contains at least one monitor
if (monitorListData.length >= 1) { if (monitorListData.length >= 1) {
// Get every existing monitor name and puts them in one simple string // Get every existing monitor name and puts them in one simple string
@ -1154,6 +1255,7 @@ exports.entryPage = "dashboard";
dns_resolve_type: monitorListData[i].dns_resolve_type, dns_resolve_type: monitorListData[i].dns_resolve_type,
dns_resolve_server: monitorListData[i].dns_resolve_server, dns_resolve_server: monitorListData[i].dns_resolve_server,
notificationIDList: {}, notificationIDList: {},
proxy_id: monitorListData[i].proxy_id || null,
}; };
if (monitorListData[i].pushToken) { if (monitorListData[i].pushToken) {
@ -1219,7 +1321,7 @@ exports.entryPage = "dashboard";
} }
await sendNotificationList(socket); await sendNotificationList(socket);
await sendMonitorList(socket); await server.sendMonitorList(socket);
} }
callback({ callback({
@ -1307,7 +1409,9 @@ exports.entryPage = "dashboard";
// Status Page Socket Handler for admin only // Status Page Socket Handler for admin only
statusPageSocketHandler(socket); statusPageSocketHandler(socket);
cloudflaredSocketHandler(socket);
databaseSocketHandler(socket); databaseSocketHandler(socket);
proxySocketHandler(socket);
debug("added all socket handlers"); debug("added all socket handlers");
@ -1328,12 +1432,12 @@ exports.entryPage = "dashboard";
console.log("Init the server"); console.log("Init the server");
server.once("error", async (err) => { httpServer.once("error", async (err) => {
console.error("Cannot listen: " + err.message); console.error("Cannot listen: " + err.message);
await Database.close(); await shutdownFunction();
}); });
server.listen(port, hostname, () => { httpServer.listen(port, hostname, () => {
if (hostname) { if (hostname) {
console.log(`Listening on ${hostname}:${port}`); console.log(`Listening on ${hostname}:${port}`);
} else { } else {
@ -1349,8 +1453,18 @@ exports.entryPage = "dashboard";
initBackgroundJobs(args); initBackgroundJobs(args);
// Start cloudflared at the end if configured
await cloudflaredAutoStart(cloudflaredToken);
})(); })();
/**
* Adds or removes notifications from a monitor.
* @param {number} monitorID The ID of the monitor to add/remove notifications from.
* @param {Array.<number>} notificationIDList An array of IDs for the notifications to add/remove.
*
* Generated by Trelent
*/
async function updateMonitorNotification(monitorID, notificationIDList) { async function updateMonitorNotification(monitorID, notificationIDList) {
await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [ await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
monitorID, monitorID,
@ -1366,6 +1480,13 @@ async function updateMonitorNotification(monitorID, notificationIDList) {
} }
} }
/**
* This function checks if the user owns a monitor with the given ID.
* @param {number} monitorID - The ID of the monitor to check ownership for.
* @param {number} userID - The ID of the user who is trying to access this data.
*
* Generated by Trelent
*/
async function checkOwner(userID, monitorID) { async function checkOwner(userID, monitorID) {
let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [ let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [
monitorID, monitorID,
@ -1377,21 +1498,22 @@ async function checkOwner(userID, monitorID) {
} }
} }
async function sendMonitorList(socket) { /**
let list = await getMonitorJSONList(socket.userID); * This function is used to send the heartbeat list of a monitor.
io.to(socket.userID).emit("monitorList", list); * @param {Socket} socket - The socket object that will be used to send the data.
return list; */
}
async function afterLogin(socket, user) { async function afterLogin(socket, user) {
socket.userID = user.id; socket.userID = user.id;
socket.join(user.id); socket.join(user.id);
let monitorList = await sendMonitorList(socket); let monitorList = await server.sendMonitorList(socket);
sendNotificationList(socket); sendNotificationList(socket);
sendProxyList(socket);
await sleep(500); await sleep(500);
await StatusPage.sendStatusPageList(io, socket);
for (let monitorID in monitorList) { for (let monitorID in monitorList) {
await sendHeartbeatList(socket, monitorID); await sendHeartbeatList(socket, monitorID);
} }
@ -1405,6 +1527,13 @@ async function afterLogin(socket, user) {
} }
} }
/**
* Get a list of monitors for the given user.
* @param {string} userID - The ID of the user to get monitors for.
* @returns {Promise<Object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
*
* Generated by Trelent
*/
async function getMonitorJSONList(userID) { async function getMonitorJSONList(userID) {
let result = {}; let result = {};
@ -1419,6 +1548,11 @@ async function getMonitorJSONList(userID) {
return result; return result;
} }
/**
* Connect to the database and patch it if necessary.
*
* Generated by Trelent
*/
async function initDatabase(testMode = false) { async function initDatabase(testMode = false) {
if (! fs.existsSync(Database.path)) { if (! fs.existsSync(Database.path)) {
console.log("Copying Database"); console.log("Copying Database");
@ -1453,6 +1587,13 @@ async function initDatabase(testMode = false) {
jwtSecret = jwtSecretBean.value; jwtSecret = jwtSecretBean.value;
} }
/**
* Resume a monitor.
* @param {string} userID - The ID of the user who owns the monitor.
* @param {string} monitorID - The ID of the monitor to resume.
*
* Generated by Trelent
*/
async function startMonitor(userID, monitorID) { async function startMonitor(userID, monitorID) {
await checkOwner(userID, monitorID); await checkOwner(userID, monitorID);
@ -1467,11 +1608,11 @@ async function startMonitor(userID, monitorID) {
monitorID, monitorID,
]); ]);
if (monitor.id in monitorList) { if (monitor.id in server.monitorList) {
monitorList[monitor.id].stop(); server.monitorList[monitor.id].stop();
} }
monitorList[monitor.id] = monitor; server.monitorList[monitor.id] = monitor;
monitor.start(io); monitor.start(io);
} }
@ -1479,6 +1620,13 @@ async function restartMonitor(userID, monitorID) {
return await startMonitor(userID, monitorID); return await startMonitor(userID, monitorID);
} }
/**
* Pause a monitor.
* @param {string} userID - The ID of the user who owns the monitor.
* @param {string} monitorID - The ID of the monitor to pause.
*
* Generated by Trelent
*/
async function pauseMonitor(userID, monitorID) { async function pauseMonitor(userID, monitorID) {
await checkOwner(userID, monitorID); await checkOwner(userID, monitorID);
@ -1489,8 +1637,8 @@ async function pauseMonitor(userID, monitorID) {
userID, userID,
]); ]);
if (monitorID in monitorList) { if (monitorID in server.monitorList) {
monitorList[monitorID].stop(); server.monitorList[monitorID].stop();
} }
} }
@ -1501,7 +1649,7 @@ async function startMonitors() {
let list = await R.find("monitor", " active = 1 "); let list = await R.find("monitor", " active = 1 ");
for (let monitor of list) { for (let monitor of list) {
monitorList[monitor.id] = monitor; server.monitorList[monitor.id] = monitor;
} }
for (let monitor of list) { for (let monitor of list) {
@ -1511,24 +1659,33 @@ async function startMonitors() {
} }
} }
/**
* Stops all monitors and closes the database connection.
* @param {string} signal The signal that triggered this function to be called.
*
* Generated by Trelent
*/
async function shutdownFunction(signal) { async function shutdownFunction(signal) {
console.log("Shutdown requested"); console.log("Shutdown requested");
console.log("Called signal: " + signal); console.log("Called signal: " + signal);
console.log("Stopping all monitors"); console.log("Stopping all monitors");
for (let id in monitorList) { for (let id in server.monitorList) {
let monitor = monitorList[id]; let monitor = server.monitorList[id];
monitor.stop(); monitor.stop();
} }
await sleep(2000); await sleep(2000);
await Database.close(); await Database.close();
stopBackgroundJobs();
await cloudflaredStop();
} }
function finalFunction() { function finalFunction() {
console.log("Graceful shutdown successful!"); console.log("Graceful shutdown successful!");
} }
gracefulShutdown(server, { gracefulShutdown(httpServer, {
signals: "SIGINT SIGTERM", signals: "SIGINT SIGTERM",
timeout: 30000, // timeout: 30 secs timeout: 30000, // timeout: 30 secs
development: false, // not in dev mode development: false, // not in dev mode

@ -0,0 +1,90 @@
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();
}
};
module.exports.stop = async () => {
console.log("Stop cloudflared");
cloudflared.stop();
};

@ -0,0 +1,53 @@
const { checkLogin } = require("../util-server");
const { Proxy } = require("../proxy");
const { sendProxyList } = require("../client");
const server = require("../server");
module.exports.proxySocketHandler = (socket) => {
socket.on("addProxy", async (proxy, proxyID, callback) => {
try {
checkLogin(socket);
const proxyBean = await Proxy.save(proxy, proxyID, socket.userID);
await sendProxyList(socket);
if (proxy.applyExisting) {
await Proxy.reloadProxy();
await server.sendMonitorList(socket);
}
callback({
ok: true,
msg: "Saved",
id: proxyBean.id,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("deleteProxy", async (proxyID, callback) => {
try {
checkLogin(socket);
await Proxy.delete(proxyID, socket.userID);
await sendProxyList(socket);
await Proxy.reloadProxy();
callback({
ok: true,
msg: "Deleted",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
};

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

@ -1,9 +1,8 @@
const tcpp = require("tcp-ping"); const tcpp = require("tcp-ping");
const Ping = require("./ping-lite"); const Ping = require("./ping-lite");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { debug } = require("../src/util"); const { debug, genSecret } = require("../src/util");
const passwordHash = require("./password-hash"); const passwordHash = require("./password-hash");
const dayjs = require("dayjs");
const { Resolver } = require("dns"); const { Resolver } = require("dns");
const child_process = require("child_process"); const child_process = require("child_process");
const iconv = require("iconv-lite"); const iconv = require("iconv-lite");
@ -16,6 +15,7 @@ exports.WIN = /^win/.test(process.platform);
exports.LIN = /^linux/.test(process.platform); exports.LIN = /^linux/.test(process.platform);
exports.MAC = /^darwin/.test(process.platform); exports.MAC = /^darwin/.test(process.platform);
exports.FBSD = /^freebsd/.test(process.platform); exports.FBSD = /^freebsd/.test(process.platform);
exports.BSD = /bsd$/.test(process.platform);
/** /**
* Init or reset JWT secret * Init or reset JWT secret
@ -31,7 +31,7 @@ exports.initJWTSecret = async () => {
jwtSecretBean.key = "jwtSecret"; jwtSecretBean.key = "jwtSecret";
} }
jwtSecretBean.value = passwordHash.generate(dayjs() + ""); jwtSecretBean.value = passwordHash.generate(genSecret());
await R.store(jwtSecretBean); await R.store(jwtSecretBean);
return jwtSecretBean; return jwtSecretBean;
}; };
@ -320,6 +320,28 @@ exports.checkLogin = (socket) => {
} }
}; };
/**
* For logged-in users, double-check the password
* @param socket
* @param currentPassword
* @returns {Promise<Bean>}
*/
exports.doubleCheckPassword = async (socket, currentPassword) => {
if (typeof currentPassword !== "string") {
throw new Error("Wrong data type?");
}
let user = await R.findOne("user", " id = ? AND active = 1 ", [
socket.userID,
]);
if (!user || !passwordHash.verify(currentPassword, user.password)) {
throw new Error("Incorrect current password");
}
return user;
};
exports.startUnitTest = async () => { exports.startUnitTest = async () => {
console.log("Starting unit test..."); console.log("Starting unit test...");
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm"; const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";

@ -22,6 +22,18 @@ textarea.form-control {
width: 10px; width: 10px;
} }
.list-group {
border-radius: 0.75rem;
.dark & {
.list-group-item {
background-color: $dark-bg;
color: $dark-font-color;
border-color: $dark-border-color;
}
}
}
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #ccc; background: #ccc;
border-radius: 20px; border-radius: 20px;
@ -92,6 +104,10 @@ textarea.form-control {
} }
} }
.btn-dark {
background-color: #161B22;
}
@media (max-width: 550px) { @media (max-width: 550px) {
.table-shadow-box { .table-shadow-box {
padding: 10px !important; padding: 10px !important;
@ -144,6 +160,10 @@ textarea.form-control {
background-color: #090c10; background-color: #090c10;
color: $dark-font-color; color: $dark-font-color;
mark, .mark {
background-color: #b6ad86;
}
&::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
background: $dark-border-color; background: $dark-border-color;
} }
@ -156,13 +176,24 @@ textarea.form-control {
.form-check-input { .form-check-input {
background-color: $dark-bg2; background-color: $dark-bg2;
border-color: $dark-border-color;
}
.input-group-text {
background-color: #282f39;
border-color: $dark-border-color;
color: $dark-font-color;
}
.form-check-input:checked {
border-color: $primary; // Re-apply bootstrap border
} }
.form-switch .form-check-input { .form-switch .form-check-input {
background-color: #232f3b; background-color: #232f3b;
} }
a, a:not(.btn),
.table, .table,
.nav-link { .nav-link {
color: $dark-font-color; color: $dark-font-color;
@ -329,11 +360,8 @@ textarea.form-control {
.monitor-list { .monitor-list {
&.scrollbar { &.scrollbar {
min-height: calc(100vh - 240px);
max-height: calc(100vh - 30px);
overflow-y: auto; overflow-y: auto;
position: sticky; height: calc(100% - 65px);
top: 10px;
} }
.item { .item {
@ -396,6 +424,10 @@ textarea.form-control {
background-color: rgba(239, 239, 239, 0.7); background-color: rgba(239, 239, 239, 0.7);
border-radius: 8px; border-radius: 8px;
&.no-bg {
background-color: transparent !important;
}
&:focus { &:focus {
outline: 0 solid #eee; outline: 0 solid #eee;
background-color: rgba(245, 245, 245, 0.9); background-color: rgba(245, 245, 245, 0.9);
@ -433,6 +465,10 @@ textarea.form-control {
border-radius: 10px !important; border-radius: 10px !important;
} }
.spinner {
color: $primary;
}
// Localization // Localization
@import "localization.scss"; @import "localization.scss";

@ -11,23 +11,23 @@
<table class="text-start"> <table class="text-start">
<tbody> <tbody>
<tr class="my-3"> <tr class="my-3">
<td class="px-3">Subject:</td> <td class="px-3">{{ $t("Subject:") }}</td>
<td>{{ formatSubject(cert.subject) }}</td> <td>{{ formatSubject(cert.subject) }}</td>
</tr> </tr>
<tr class="my-3"> <tr class="my-3">
<td class="px-3">Valid To:</td> <td class="px-3">{{ $t("Valid To:") }}</td>
<td><Datetime :value="cert.validTo" /></td> <td><Datetime :value="cert.validTo" /></td>
</tr> </tr>
<tr class="my-3"> <tr class="my-3">
<td class="px-3">Days Remaining:</td> <td class="px-3">{{ $t("Days Remaining:") }}</td>
<td>{{ cert.daysRemaining }}</td> <td>{{ cert.daysRemaining }}</td>
</tr> </tr>
<tr class="my-3"> <tr class="my-3">
<td class="px-3">Issuer:</td> <td class="px-3">{{ $t("Issuer:") }}</td>
<td>{{ formatSubject(cert.issuer) }}</td> <td>{{ formatSubject(cert.issuer) }}</td>
</tr> </tr>
<tr class="my-3"> <tr class="my-3">
<td class="px-3">Fingerprint:</td> <td class="px-3">{{ $t("Fingerprint:") }}</td>
<td>{{ cert.fingerprint }}</td> <td>{{ cert.fingerprint }}</td>
</tr> </tr>
</tbody> </tbody>

@ -16,8 +16,8 @@
<div v-if="tokenRequired"> <div v-if="tokenRequired">
<div class="form-floating mt-3"> <div class="form-floating mt-3">
<input id="floatingToken" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456"> <input id="otp" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
<label for="floatingToken">{{ $t("Token") }}</label> <label for="otp">{{ $t("Token") }}</label>
</div> </div>
</div> </div>

@ -1,5 +1,5 @@
<template> <template>
<div class="shadow-box mb-3"> <div class="shadow-box mb-3" :style="boxStyle">
<div class="list-header"> <div class="list-header">
<div class="placeholder"></div> <div class="placeholder"></div>
<div class="search-wrapper"> <div class="search-wrapper">
@ -9,7 +9,9 @@
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText"> <a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
<font-awesome-icon icon="times" /> <font-awesome-icon icon="times" />
</a> </a>
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" /> <form>
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" autocomplete="off" />
</form>
</div> </div>
</div> </div>
<div class="monitor-list" :class="{ scrollbar: scrollbar }"> <div class="monitor-list" :class="{ scrollbar: scrollbar }">
@ -34,7 +36,7 @@
</div> </div>
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row"> <div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12"> <div class="col-12 bottom-style">
<HeartbeatBar size="small" :monitor-id="item.id" /> <HeartbeatBar size="small" :monitor-id="item.id" />
</div> </div>
</div> </div>
@ -63,9 +65,16 @@ export default {
data() { data() {
return { return {
searchText: "", searchText: "",
windowTop: 0,
}; };
}, },
computed: { computed: {
boxStyle() {
return {
height: `calc(100vh - 160px + ${this.windowTop}px)`,
};
},
sortedMonitorList() { sortedMonitorList() {
let result = Object.values(this.$root.monitorList); let result = Object.values(this.$root.monitorList);
@ -108,7 +117,20 @@ export default {
return result; return result;
}, },
}, },
mounted() {
window.addEventListener("scroll", this.onScroll);
},
beforeUnmount() {
window.removeEventListener("scroll", this.onScroll);
},
methods: { methods: {
onScroll() {
if (window.top.scrollY <= 133) {
this.windowTop = window.top.scrollY;
} else {
this.windowTop = 133;
}
},
monitorURL(id) { monitorURL(id) {
return getMonitorRelativeURL(id); return getMonitorRelativeURL(id);
}, },
@ -122,6 +144,12 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../assets/vars.scss"; @import "../assets/vars.scss";
.shadow-box {
height: calc(100vh - 150px);
position: sticky;
top: 10px;
}
.small-padding { .small-padding {
padding-left: 5px !important; padding-left: 5px !important;
padding-right: 5px !important; padding-right: 5px !important;
@ -142,6 +170,12 @@ export default {
} }
} }
.dark {
.footer {
// background-color: $dark-bg;
}
}
@media (max-width: 770px) { @media (max-width: 770px) {
.list-header { .list-header {
margin: -20px; margin: -20px;
@ -169,9 +203,16 @@ export default {
} }
.tags { .tags {
padding-left: 62px; margin-top: 4px;
padding-left: 67px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0; gap: 0;
} }
.bottom-style {
padding-left: 67px;
margin-top: 5px;
}
</style> </style>

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

@ -0,0 +1,206 @@
<template>
<form @submit.prevent="submit">
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 id="exampleModalLabel" class="modal-title">
{{ $t("Setup Proxy") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
<div class="modal-body">
<div class="mb-3">
<label for="proxy-protocol" class="form-label">{{ $t("Proxy Protocol") }}</label>
<select id="proxy-protocol" v-model="proxy.protocol" class="form-select">
<option value="https">HTTPS</option>
<option value="http">HTTP</option>
<option value="socks">SOCKS</option>
<option value="socks5">SOCKS v5</option>
<option value="socks4">SOCKS v4</option>
</select>
</div>
<div class="mb-3">
<label for="proxy-host" class="form-label">{{ $t("Proxy Server") }}</label>
<div class="d-flex">
<input id="proxy-host" v-model="proxy.host" type="text" class="form-control" required :placeholder="$t('Server Address')">
<input v-model="proxy.port" type="number" class="form-control ms-2" style="width: 100px" required min="1" max="65535" :placeholder="$t('Port')">
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input id="mark-auth" v-model="proxy.auth" class="form-check-input" type="checkbox">
<label for="mark-auth" class="form-check-label">{{ $t("Proxy server has authentication") }}</label>
</div>
</div>
<div v-if="proxy.auth" class="mb-3">
<label for="proxy-username" class="form-label">{{ $t("User") }}</label>
<input id="proxy-username" v-model="proxy.username" type="text" class="form-control" required>
</div>
<div v-if="proxy.auth" class="mb-3">
<label for="proxy-password" class="form-label">{{ $t("Password") }}</label>
<input id="proxy-password" v-model="proxy.password" type="password" class="form-control" required>
</div>
<div class="mb-3 mt-4">
<hr class="dropdown-divider mb-4">
<div class="form-check form-switch">
<input id="mark-active" v-model="proxy.active" class="form-check-input" type="checkbox">
<label for="mark-active" class="form-check-label">{{ $t("enabled") }}</label>
</div>
<div class="form-text">
{{ $t("enableProxyDescription") }}
</div>
<br />
<div class="form-check form-switch">
<input id="mark-default" v-model="proxy.default" class="form-check-input" type="checkbox">
<label for="mark-default" class="form-check-label">{{ $t("setAsDefault") }}</label>
</div>
<div class="form-text">
{{ $t("setAsDefaultProxyDescription") }}
</div>
<br />
<div class="form-check form-switch">
<input id="apply-existing" v-model="proxy.applyExisting" class="form-check-input" type="checkbox">
<label class="form-check-label" for="apply-existing">{{ $t("Apply on all existing monitors") }}</label>
</div>
</div>
</div>
<div class="modal-footer">
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
{{ $t("Delete") }}
</button>
<button type="submit" class="btn btn-primary" :disabled="processing">
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
{{ $t("Save") }}
</button>
</div>
</div>
</div>
</div>
</form>
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteProxy">
{{ $t("deleteProxyMsg") }}
</Confirm>
</template>
<script lang="ts">
import { Modal } from "bootstrap";
import Confirm from "./Confirm.vue";
export default {
components: {
Confirm,
},
props: {},
emits: ["added"],
data() {
return {
model: null,
processing: false,
id: null,
proxy: {
protocol: null,
host: null,
port: null,
auth: false,
username: null,
password: null,
active: false,
default: false,
applyExisting: false,
}
};
},
mounted() {
this.modal = new Modal(this.$refs.modal);
},
methods: {
deleteConfirm() {
this.modal.hide();
this.$refs.confirmDelete.show();
},
show(proxyID) {
if (proxyID) {
this.id = proxyID;
for (let proxy of this.$root.proxyList) {
if (proxy.id === proxyID) {
this.proxy = proxy;
break;
}
}
} else {
this.id = null;
this.proxy = {
protocol: "https",
host: null,
port: null,
auth: false,
username: null,
password: null,
active: true,
default: false,
applyExisting: false,
};
}
this.modal.show();
},
submit() {
this.processing = true;
this.$root.getSocket().emit("addProxy", this.proxy, this.id, (res) => {
this.$root.toastRes(res);
this.processing = false;
if (res.ok) {
this.modal.hide();
// Emit added event, doesn't emit edit.
if (! this.id) {
this.$emit("added", res.id);
}
}
});
},
deleteProxy() {
this.processing = true;
this.$root.getSocket().emit("deleteProxy", this.id, (res) => {
this.$root.toastRes(res);
this.processing = false;
if (res.ok) {
this.modal.hide();
}
});
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.dark {
.modal-dialog .form-text, .modal-dialog p {
color: $dark-font-color;
}
}
</style>

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

@ -19,6 +19,19 @@
</div> </div>
<p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p> <p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
<div v-if="!(uri && twoFAStatus == false)" class="mb-3">
<label for="current-password" class="form-label">
{{ $t("Current Password") }}
</label>
<input
id="current-password"
v-model="currentPassword"
type="password"
class="form-control"
required
/>
</div>
<button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()"> <button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
{{ $t("Enable 2FA") }} {{ $t("Enable 2FA") }}
</button> </button>
@ -59,11 +72,11 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Modal } from "bootstrap" import { Modal } from "bootstrap";
import Confirm from "./Confirm.vue"; import Confirm from "./Confirm.vue";
import VueQrcode from "vue-qrcode" import VueQrcode from "vue-qrcode";
import { useToast } from "vue-toastification" import { useToast } from "vue-toastification";
const toast = useToast() const toast = useToast();
export default { export default {
components: { components: {
@ -73,35 +86,36 @@ export default {
props: {}, props: {},
data() { data() {
return { return {
currentPassword: "",
processing: false, processing: false,
uri: null, uri: null,
tokenValid: false, tokenValid: false,
twoFAStatus: null, twoFAStatus: null,
token: null, token: null,
showURI: false, showURI: false,
} };
}, },
mounted() { mounted() {
this.modal = new Modal(this.$refs.modal) this.modal = new Modal(this.$refs.modal);
this.getStatus(); this.getStatus();
}, },
methods: { methods: {
show() { show() {
this.modal.show() this.modal.show();
}, },
confirmEnableTwoFA() { confirmEnableTwoFA() {
this.$refs.confirmEnableTwoFA.show() this.$refs.confirmEnableTwoFA.show();
}, },
confirmDisableTwoFA() { confirmDisableTwoFA() {
this.$refs.confirmDisableTwoFA.show() this.$refs.confirmDisableTwoFA.show();
}, },
prepare2FA() { prepare2FA() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("prepare2FA", (res) => { this.$root.getSocket().emit("prepare2FA", this.currentPassword, (res) => {
this.processing = false; this.processing = false;
if (res.ok) { if (res.ok) {
@ -109,49 +123,51 @@ export default {
} else { } else {
toast.error(res.msg); toast.error(res.msg);
} }
}) });
}, },
save2FA() { save2FA() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("save2FA", (res) => { this.$root.getSocket().emit("save2FA", this.currentPassword, (res) => {
this.processing = false; this.processing = false;
if (res.ok) { if (res.ok) {
this.$root.toastRes(res) this.$root.toastRes(res);
this.getStatus(); this.getStatus();
this.currentPassword = "";
this.modal.hide(); this.modal.hide();
} else { } else {
toast.error(res.msg); toast.error(res.msg);
} }
}) });
}, },
disable2FA() { disable2FA() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("disable2FA", (res) => { this.$root.getSocket().emit("disable2FA", this.currentPassword, (res) => {
this.processing = false; this.processing = false;
if (res.ok) { if (res.ok) {
this.$root.toastRes(res) this.$root.toastRes(res);
this.getStatus(); this.getStatus();
this.currentPassword = "";
this.modal.hide(); this.modal.hide();
} else { } else {
toast.error(res.msg); toast.error(res.msg);
} }
}) });
}, },
verifyToken() { verifyToken() {
this.$root.getSocket().emit("verifyToken", this.token, (res) => { this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
if (res.ok) { if (res.ok) {
this.tokenValid = res.valid; this.tokenValid = res.valid;
} else { } else {
toast.error(res.msg); toast.error(res.msg);
} }
}) });
}, },
getStatus() { getStatus() {
@ -161,10 +177,10 @@ export default {
} else { } else {
toast.error(res.msg); toast.error(res.msg);
} }
}) });
}, },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

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

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

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

@ -0,0 +1,13 @@
<template>
<div class="mb-3">
<label for="google-chat-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="google-chat-webhook-url" v-model="$parent.notification.googleChatWebhookURL" type="text" class="form-control" required>
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
<a href="https://developers.google.com/chat/how-tos/webhooks" target="_blank">https://developers.google.com/chat/how-tos/webhooks</a>
</i18n-t>
</div>
</div>
</template>

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

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

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

@ -0,0 +1,12 @@
<template>
<div class="mb-3">
<label for="WeCom Bot Key" class="form-label">{{ $t("WeCom Bot Key") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="WeCom Bot Key" v-model="$parent.notification.weComBotKey" type="text" class="form-control" required>
<div class="form-text">
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
</div>
<i18n-t tag="p" keypath="Read more:">
<a href="https://work.weixin.qq.com/api/doc/90000/90136/91770" target="_blank">https://work.weixin.qq.com/api/doc/90000/90136/91770</a>
</i18n-t>
</div>
</template>

@ -1,4 +1,4 @@
import STMP from "./SMTP.vue" import STMP from "./SMTP.vue";
import Telegram from "./Telegram.vue"; import Telegram from "./Telegram.vue";
import Discord from "./Discord.vue"; import Discord from "./Discord.vue";
import Webhook from "./Webhook.vue"; import Webhook from "./Webhook.vue";
@ -9,6 +9,7 @@ import RocketChat from "./RocketChat.vue";
import Teams from "./Teams.vue"; import Teams from "./Teams.vue";
import Pushover from "./Pushover.vue"; import Pushover from "./Pushover.vue";
import Pushy from "./Pushy.vue"; import Pushy from "./Pushy.vue";
import TechulusPush from "./TechulusPush.vue";
import Octopush from "./Octopush.vue"; import Octopush from "./Octopush.vue";
import PromoSMS from "./PromoSMS.vue"; import PromoSMS from "./PromoSMS.vue";
import ClickSendSMS from "./ClickSendSMS.vue"; import ClickSendSMS from "./ClickSendSMS.vue";
@ -24,6 +25,10 @@ import DingDing from "./DingDing.vue";
import Bark from "./Bark.vue"; import Bark from "./Bark.vue";
import SerwerSMS from "./SerwerSMS.vue"; import SerwerSMS from "./SerwerSMS.vue";
import Stackfield from './Stackfield.vue'; import Stackfield from './Stackfield.vue';
import WeCom from "./WeCom.vue";
import GoogleChat from "./GoogleChat.vue";
import Gorush from "./Gorush.vue";
import Alerta from "./Alerta.vue";
/** /**
* Manage all notification form. * Manage all notification form.
@ -42,6 +47,7 @@ const NotificationFormList = {
"rocket.chat": RocketChat, "rocket.chat": RocketChat,
"pushover": Pushover, "pushover": Pushover,
"pushy": Pushy, "pushy": Pushy,
"PushByTechulus": TechulusPush,
"octopush": Octopush, "octopush": Octopush,
"promosms": PromoSMS, "promosms": PromoSMS,
"clicksendsms": ClickSendSMS, "clicksendsms": ClickSendSMS,
@ -57,6 +63,10 @@ const NotificationFormList = {
"Bark": Bark, "Bark": Bark,
"serwersms": SerwerSMS, "serwersms": SerwerSMS,
"stackfield": Stackfield, "stackfield": Stackfield,
} "WeCom": WeCom,
"GoogleChat": GoogleChat,
"gorush": Gorush,
"alerta": Alerta,
};
export default NotificationFormList export default NotificationFormList;

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

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

@ -0,0 +1,48 @@
<template>
<div>
<!-- Proxies -->
<div class="proxy-list my-4">
<p v-if="$root.proxyList.length === 0">
{{ $t("Not available, please setup.") }}
</p>
<p v-else>
{{ $t("proxyDescription") }}
</p>
<ul class="list-group mb-3" style="border-radius: 1rem;">
<li v-for="(proxy, index) in $root.proxyList" :key="index" class="list-group-item">
{{ proxy.host }}:{{ proxy.port }} ({{ proxy.protocol }})
<span v-if="proxy.default === true" class="badge bg-primary ms-2">{{ $t("Default") }}</span><br>
<a href="#" @click="$refs.proxyDialog.show(proxy.id)">{{ $t("Edit") }}</a>
</li>
</ul>
<button class="btn btn-primary me-2" type="button" @click="$refs.proxyDialog.show()">
{{ $t("Setup Proxy") }}
</button>
</div>
<ProxyDialog ref="proxyDialog" />
</div>
</template>
<script>
import ProxyDialog from "../../components/ProxyDialog.vue";
export default {
components: {
ProxyDialog
},
};
</script>
<style lang="scss" scoped>
@import "../../assets/vars.scss";
.dark {
.list-group-item {
background-color: $dark-bg2;
color: $dark-font-color;
}
}
</style>

@ -0,0 +1,144 @@
<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">
{{ $t("Message:") }}
<textarea v-model="errorMessage" class="form-control" readonly></textarea>
</div>
<i18n-t v-if="installed === false" tag="p" keypath="wayToGetCloudflaredURL">
<a
href="https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/"
target="_blank"
>{{ $t("cloudflareWebsite") }}</a>
</i18n-t>
</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>
{{ $t("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">
{{ $t("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">{{ $t("Other Software") }}</h4>
<div>
{{ $t("For example: nginx, Apache and Traefik.") }} <br />
{{ $t("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>

@ -175,9 +175,9 @@
</template> </template>
<template v-else-if="$i18n.locale === 'it-IT' "> <template v-else-if="$i18n.locale === 'it-IT' ">
<p>Si è certi di voler <strong>disabilitare l'autenticazione</strong>?</p> <p><strong>Disabilitare l'autenticazione?</strong></p>
<p>È per <strong>chi ha l'autenticazione gestita da terze parti</strong> messa davanti ad Uptime Kuma, ad esempio Cloudflare Access.</p> <p><strong>Questa opzione è per chi un sistema di autenticazione gestito da terze parti</strong> messo davanti ad Uptime Kuma, ad esempio Cloudflare Access.</p>
<p>Utilizzare con attenzione.</p> <p>Utilizzare con attenzione!</p>
</template> </template>
<template v-else-if="$i18n.locale === 'id-ID' "> <template v-else-if="$i18n.locale === 'id-ID' ">
@ -192,6 +192,12 @@
<p>Пожалуйста, используйте с осторожностью.</p> <p>Пожалуйста, используйте с осторожностью.</p>
</template> </template>
<template v-else-if="$i18n.locale === 'uk-UA' ">
<p>Ви впевнені, що бажаєте <strong>вимкнути авторизацію</strong>?</p>
<p>Це підходить для <strong>тих, у кого встановлена інша авторизація</strong> пееред відкриттям Uptime Kuma, наприклад Cloudflare Access.</p>
<p>Будь ласка, використовуйте з обережністю.</p>
</template>
<template v-else-if="$i18n.locale === 'fa' "> <template v-else-if="$i18n.locale === 'fa' ">
<p>آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?</p> <p>آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?</p>
<p>این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کردهاند</strong>، مانند Cloudflare Access.</p> <p>این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کردهاند</strong>، مانند Cloudflare Access.</p>
@ -216,12 +222,37 @@
<p>Vennligst vær forsiktig.</p> <p>Vennligst vær forsiktig.</p>
</template> </template>
<template v-else-if="$i18n.locale === 'cs-CZ' ">
<p>Opravdu chcete <strong>deaktivovat autentifikaci</strong>?</p>
<p>Tato možnost je určena pro případy, kdy <strong>máte autentifikaci zajištěnou třetí stranou</strong> ještě před přístupem do Uptime Kuma, například prostřednictvím Cloudflare Access.</p>
<p>Používejte ji prosím s rozmyslem.</p>
</template>
<template v-else-if="$i18n.locale === 'vi-VN' ">
<p>Bạn muốn <strong>TẮT XÁC THỰC</strong> không?</p>
<p>Điều này rất nguy hiểm<strong>BẤT KỲ AI</strong> cũng thể truy cập cướp quyền điều khiển.</p>
<p>Vui lòng <strong>cẩn thận</strong>.</p>
</template>
<!-- English (en) --> <!-- English (en) -->
<template v-else> <template v-else>
<p>Are you sure want to <strong>disable auth</strong>?</p> <p>Are you sure want to <strong>disable authentication</strong>?</p>
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p> <p>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 it carefully.</p> <p>Please use this option carefully!</p>
</template> </template>
<div class="mb-3">
<label for="current-password2" class="form-label">
{{ $t("Current Password") }}
</label>
<input
id="current-password2"
v-model="password.currentPassword"
type="password"
class="form-control"
required
/>
</div>
</Confirm> </Confirm>
</div> </div>
</template> </template>
@ -298,7 +329,12 @@ export default {
disableAuth() { disableAuth() {
this.settings.disableAuth = true; this.settings.disableAuth = true;
this.saveSettings();
// Need current password to disable auth
// Set it to empty if done
this.saveSettings(() => {
this.password.currentPassword = "";
}, this.password.currentPassword);
}, },
enableAuth() { enableAuth() {

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

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

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "Пауза", pauseDashboardHome: "Пауза",
deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?", deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?",
deleteNotificationMsg: "Наистина ли желаете да изтриете това известяване за всички монитори?", deleteNotificationMsg: "Наистина ли желаете да изтриете това известяване за всички монитори?",
resoverserverDescription: "Cloudflare е сървърът по подразбиране, но можете да го промените по всяко време.", resolverserverDescription: "Cloudflare е сървърът по подразбиране, но можете да го промените по всяко време.",
rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате", rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате",
pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?", pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?",
enableDefaultNotificationDescription: "За всеки нов монитор това известяване ще бъде активирано по подразбиране. Можете да го изключите за всеки отделен монитор.", enableDefaultNotificationDescription: "За всеки нов монитор това известяване ще бъде активирано по подразбиране. Можете да го изключите за всеки отделен монитор.",
@ -131,8 +131,8 @@ export default {
Events: "Събития", Events: "Събития",
Heartbeats: "Проверки", Heartbeats: "Проверки",
"Auto Get": "Авт. попълване", "Auto Get": "Авт. попълване",
backupDescription: "Можете да архивирате всички монитори и всички известия в JSON файл.", backupDescription: "Можете да архивирате всички монитори и всички известявания в JSON файл.",
backupDescription2: "PS: Данни за история и събития не са включени.", backupDescription2: "PS: Имайте предвид, че данните за история и събития няма да бъдат включени.",
backupDescription3: "Чувствителни данни, като токен кодове за известяване, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.", backupDescription3: "Чувствителни данни, като токен кодове за известяване, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.",
alertNoFile: "Моля, изберете файл за импортиране.", alertNoFile: "Моля, изберете файл за импортиране.",
alertWrongFileType: "Моля, изберете JSON файл.", alertWrongFileType: "Моля, изберете JSON файл.",
@ -197,6 +197,7 @@ export default {
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
"Status Page": "Статус страница", "Status Page": "Статус страница",
"Status Pages": "Статус страница",
"Primary Base URL": "Основен базов URL адрес", "Primary Base URL": "Основен базов URL адрес",
"Push URL": "Генериран Push URL адрес", "Push URL": "Генериран Push URL адрес",
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди", needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди",
@ -351,4 +352,23 @@ export default {
serwersmsPhoneNumber: "Телефон номер", serwersmsPhoneNumber: "Телефон номер",
serwersmsSenderName: "SMS Подател име (регистриран през клиентския портал)", serwersmsSenderName: "SMS Подател име (регистриран през клиентския портал)",
stackfield: "Stackfield", stackfield: "Stackfield",
smtpDkimSettings: "DKIM Настройки",
smtpDkimDesc: "Моля, вижте Nodemailer DKIM {0} за инструкции.",
documentation: "документация",
smtpDkimDomain: "Домейн",
smtpDkimKeySelector: "Селектор на ключ",
smtpDkimPrivateKey: "Частен ключ",
smtpDkimHashAlgo: "Хеш алгоритъм (по желание)",
smtpDkimheaderFieldNames: "Хедър ключове за подписване (по желание)",
smtpDkimskipFields: "Хедър ключове, които да не се подписват (по желание)",
PushByTechulus: "Push от Techulus",
GoogleChat: "Google Chat (Само за работното пространство на Google)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "Крайна точка на API",
alertaEnvironment: "Среда",
alertaApiKey: "API Ключ",
alertaAlertState: "Състояние на тревога",
alertaRecoverState: "Състояние на възстановяване",
deleteStatusPageMsg: "Сигурни ли сте, че желаете да изтриете тази статус страница?",
}; };

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

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

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

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "Pause", pauseDashboardHome: "Pause",
deleteMonitorMsg: "Are you sure want to delete this monitor?", deleteMonitorMsg: "Are you sure want to delete this monitor?",
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?", deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?",
resoverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.", resolverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.",
rrtypeDescription: "Select the RR type you want to monitor", rrtypeDescription: "Select the RR type you want to monitor",
pauseMonitorMsg: "Are you sure want to pause?", pauseMonitorMsg: "Are you sure want to pause?",
enableDefaultNotificationDescription: "This notification will be enabled by default for new monitors. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "This notification will be enabled by default for new monitors. You can still disable the notification separately for each monitor.",
@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Edit Status Page", "Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard", "Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page", "Status Page": "Status Page",
"Status Pages": "Status Pages",
defaultNotificationName: "My {notification} Alert ({number})", defaultNotificationName: "My {notification} Alert ({number})",
here: "here", here: "here",
Required: "Required", Required: "Required",
@ -238,11 +239,13 @@ export default {
"rocket.chat": "Rocket.Chat", "rocket.chat": "Rocket.Chat",
pushover: "Pushover", pushover: "Pushover",
pushy: "Pushy", pushy: "Pushy",
PushByTechulus: "Push by Techulus",
octopush: "Octopush", octopush: "Octopush",
promosms: "PromoSMS", promosms: "PromoSMS",
clicksendsms: "ClickSend SMS", clicksendsms: "ClickSend SMS",
lunasea: "LunaSea", lunasea: "LunaSea",
apprise: "Apprise (Support 50+ Notification services)", apprise: "Apprise (Support 50+ Notification services)",
GoogleChat: "Google Chat (Google Workspace only)",
pushbullet: "Pushbullet", pushbullet: "Pushbullet",
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
@ -363,4 +366,83 @@ export default {
smtpDkimHashAlgo: "Hash Algorithm (Optional)", smtpDkimHashAlgo: "Hash Algorithm (Optional)",
smtpDkimheaderFieldNames: "Header Keys to sign (Optional)", smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
smtpDkimskipFields: "Header Keys not to sign (Optional)", smtpDkimskipFields: "Header Keys not to sign (Optional)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "Environment",
alertaApiKey: "API Key",
alertaAlertState: "Alert State",
alertaRecoverState: "Recover State",
deleteStatusPageMsg: "Are you sure want to delete this status page?",
Proxies: "Proxies",
default: "Default",
enabled: "Enabled",
setAsDefault: "Set As Default",
deleteProxyMsg: "Are you sure want to delete this proxy for all monitors?",
proxyDescription: "Proxies must be assigned to a monitor to function.",
enableProxyDescription: "This proxy will not effect on monitor requests until it is activated. You can control temporarily disable the proxy from all monitors by activation status.",
setAsDefaultProxyDescription: "This proxy will be enabled by default for new monitors. You can still disable the proxy separately for each monitor.",
"Certificate Chain": "Certificate Chain",
Valid: "Valid",
Invalid: "Invalid",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "PhoneNumbers",
TemplateCode: "TemplateCode",
SignName: "SignName",
"Sms template must contain parameters: ": "Sms template must contain parameters: ",
"Bark Endpoint": "Bark Endpoint",
WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey",
"For safety, must use secret key": "For safety, must use secret key",
"Device Token": "Device Token",
Platform: "Platform",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "High",
Retry: "Retry",
Topic: "Topic",
"WeCom Bot Key": "WeCom Bot Key",
"Setup Proxy": "Setup Proxy",
"Proxy Protocol": "Proxy Protocol",
"Proxy Server": "Proxy Server",
"Proxy server has authentication": "Proxy server has authentication",
User: "User",
Installed: "Installed",
"Not installed": "Not installed",
Running: "Running",
"Not running": "Not running",
"Remove Token": "Remove Token",
Start: "Start",
Stop: "Stop",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "Add New Status Page",
Slug: "Slug",
"Accept characters:": "Accept characters:",
"startOrEndWithOnly": "Start or end with {0} only",
"No consecutive dashes": "No consecutive dashes",
Next: "Next",
"The slug is already taken. Please choose another slug.": "The slug is already taken. Please choose another slug.",
"No Proxy": "No Proxy",
"HTTP Basic Auth": "HTTP Basic Auth",
"New Status Page": "New Status Page",
"Page Not Found": "Page Not Found",
"Reverse Proxy": "Reverse Proxy",
Backup: "Backup",
About: "About",
wayToGetCloudflaredURL: "(Download cloudflared from {0})",
cloudflareWebsite: "Cloudflare Website",
"Message:": "Message:",
"Don't know how to get the token? Please read the guide:": "Don't know how to get the token? Please read the guide:",
"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.": "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.",
"Other Software": "Other Software",
"For example: nginx, Apache and Traefik.": "For example: nginx, Apache and Traefik.",
"Please read": "Please read",
"Subject:": "Subject:",
"Valid To:": "Valid To:",
"Days Remaining:": "Days Remaining:",
"Issuer:": "Issuer:",
"Fingerprint:": "Fingerprint:",
"No status pages": "No status pages",
}; };

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

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

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

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "En pause", pauseDashboardHome: "En pause",
deleteMonitorMsg: "Êtes-vous sûr de vouloir supprimer cette sonde ?", deleteMonitorMsg: "Êtes-vous sûr de vouloir supprimer cette sonde ?",
deleteNotificationMsg: "Êtes-vous sûr de vouloir supprimer ce type de notifications ? Une fois désactivée, les services qui l'utilisent ne pourront plus envoyer de notifications.", deleteNotificationMsg: "Êtes-vous sûr de vouloir supprimer ce type de notifications ? Une fois désactivée, les services qui l'utilisent ne pourront plus envoyer de notifications.",
resoverserverDescription: "Le DNS de Cloudflare est utilisé par défaut, mais vous pouvez le changer si vous le souhaitez.", resolverserverDescription: "Le DNS de Cloudflare est utilisé par défaut, mais vous pouvez le changer si vous le souhaitez.",
rrtypeDescription: "Veuillez sélectionner un type d'enregistrement DNS", rrtypeDescription: "Veuillez sélectionner un type d'enregistrement DNS",
pauseMonitorMsg: "Êtes-vous sûr de vouloir mettre en pause cette sonde ?", pauseMonitorMsg: "Êtes-vous sûr de vouloir mettre en pause cette sonde ?",
enableDefaultNotificationDescription: "Pour chaque nouvelle sonde, cette notification sera activée par défaut. Vous pouvez toujours désactiver la notification séparément pour chaque sonde.", enableDefaultNotificationDescription: "Pour chaque nouvelle sonde, cette notification sera activée par défaut. Vous pouvez toujours désactiver la notification séparément pour chaque sonde.",
@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Modifier la page de statut", "Edit Status Page": "Modifier la page de statut",
"Go to Dashboard": "Accéder au tableau de bord", "Go to Dashboard": "Accéder au tableau de bord",
"Status Page": "Status Page", "Status Page": "Status Page",
"Status Pages": "Status Pages",
defaultNotificationName: "Ma notification {notification} numéro ({number})", defaultNotificationName: "Ma notification {notification} numéro ({number})",
here: "ici", here: "ici",
Required: "Requis", Required: "Requis",
@ -207,7 +208,7 @@ export default {
"Discord Webhook URL": "Discord Webhook URL", "Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "Vous pouvez l'obtenir en allant dans 'Paramètres du Serveur' -> 'Intégrations' -> 'Créer un Webhook'", wayToGetDiscordURL: "Vous pouvez l'obtenir en allant dans 'Paramètres du Serveur' -> 'Intégrations' -> 'Créer un Webhook'",
"Bot Display Name": "Nom du bot (affiché)", "Bot Display Name": "Nom du bot (affiché)",
"Prefix Custom Message": "Prefix Custom Message", "Prefix Custom Message": "Prefixe du message personnalisé",
"Hello @everyone is...": "Bonjour {'@'}everyone il...", "Hello @everyone is...": "Bonjour {'@'}everyone il...",
teams: "Microsoft Teams", teams: "Microsoft Teams",
"Webhook URL": "Webhook URL", "Webhook URL": "Webhook URL",
@ -229,7 +230,7 @@ export default {
aboutWebhooks: "Plus d'informations sur les Webhooks ici : {0}", aboutWebhooks: "Plus d'informations sur les Webhooks ici : {0}",
aboutChannelName: "Mettez le nom du salon dans {0} dans 'Channel Name' si vous voulez bypass le salon Webhook. Ex : #autre-salon", aboutChannelName: "Mettez le nom du salon dans {0} dans 'Channel Name' si vous voulez bypass le salon Webhook. Ex : #autre-salon",
aboutKumaURL: "Si vous laissez l'URL d'Uptime Kuma vierge, elle redirigera vers la page du projet GitHub.", aboutKumaURL: "Si vous laissez l'URL d'Uptime Kuma vierge, elle redirigera vers la page du projet GitHub.",
emojiCheatSheet: "Emoji cheat sheet : {0}", emojiCheatSheet: "Aide emoji : {0}",
"rocket.chat": "Rocket.chat", "rocket.chat": "Rocket.chat",
pushover: "Pushover", pushover: "Pushover",
pushy: "Pushy", pushy: "Pushy",
@ -262,9 +263,9 @@ export default {
appriseInstalled: "Apprise est installé.", appriseInstalled: "Apprise est installé.",
appriseNotInstalled: "Apprise n'est pas installé. {0}", appriseNotInstalled: "Apprise n'est pas installé. {0}",
"Access Token": "Access Token", "Access Token": "Access Token",
"Channel access token": "Channel access token", "Channel access token": "Token d'accès au canal",
"Line Developers Console": "Line Developers Console", "Line Developers Console": "Ligne console de développeurs",
lineDevConsoleTo: "Line Developers Console - {0}", lineDevConsoleTo: "Ligne console de développeurs - {0}",
"Basic Settings": "Paramètres de base", "Basic Settings": "Paramètres de base",
"User ID": "Identifiant utilisateur", "User ID": "Identifiant utilisateur",
"Messaging API": "Messaging API", "Messaging API": "Messaging API",
@ -304,4 +305,9 @@ export default {
steamApiKeyDescription: "Pour surveiller un serveur Steam, vous avez besoin d'une clé Steam Web-API. Vous pouvez enregistrer votre clé ici : ", steamApiKeyDescription: "Pour surveiller un serveur Steam, vous avez besoin d'une clé Steam Web-API. Vous pouvez enregistrer votre clé ici : ",
"Current User": "Utilisateur actuel", "Current User": "Utilisateur actuel",
recent: "Récent", recent: "Récent",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "Environement",
alertaApiKey: "Clé de l'API",
alertaAlertState: "État de l'Alerte",
alertaRecoverState: "État de récupération",
}; };

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

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

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

@ -2,49 +2,49 @@ export default {
languageName: "Italiano (Italian)", languageName: "Italiano (Italian)",
checkEverySecond: "controlla ogni {0} secondi", checkEverySecond: "controlla ogni {0} secondi",
retryCheckEverySecond: "Riprova ogni {0} secondi.", retryCheckEverySecond: "Riprova ogni {0} secondi.",
retriesDescription: "Tentativi da fare prima che il servizio venga marcato come \"giù\" e che una notifica venga inviata.", retriesDescription: "Tentativi prima che il servizio venga marcato come \"DOWN\" e che una notifica venga inviata.",
ignoreTLSError: "Ignora gli errori TLS/SSL per i siti in HTTPS.", ignoreTLSError: "Ignora gli errori TLS/SSL per i siti HTTPS.",
upsideDownModeDescription: "Capovolgi lo stato. Se il servizio è raggiungibile viene marcato come \"GIÙ\".", upsideDownModeDescription: "Se il servizio risulta raggiungibile viene marcato come \"DOWN\".",
maxRedirectDescription: "Numero massimo di redirezionamenti consentito. Per disabilitare impostare \"0\".", maxRedirectDescription: "Numero massimo di redirezionamenti consentito. Per disabilitare, impostare \"0\".",
acceptedStatusCodesDescription: "Inserire i codici di stato considerati come risposte corrette.", acceptedStatusCodesDescription: "Elenco di codici di stato HTTP che sono considerati validi.",
passwordNotMatchMsg: "La password non coincide.", passwordNotMatchMsg: "La password non coincide.",
notificationDescription: "Assegnare la notifica a uno o più oggetti monitorati per metterla in funzione.", notificationDescription: "Assegnare la notifica a uno o più oggetti monitorati per metterla in funzione.",
keywordDescription: "Cerca la parola chiave nella risposta in html o JSON e fai distinzione tra maiuscole e minuscole", keywordDescription: "Cerca la parola chiave nella risposta in html o JSON e fai distinzione tra maiuscole e minuscole",
pauseDashboardHome: "In Pausa", pauseDashboardHome: "In Pausa",
deleteMonitorMsg: "Si è certi di voler eliminare questo oggetto monitorato?", deleteMonitorMsg: "Si è certi di voler eliminare questo oggetto monitorato?",
deleteNotificationMsg: "Si è certi di voler eliminare questa notifica per tutti gli oggetti monitorati?", deleteNotificationMsg: "Si è certi di voler eliminare questa notifica per tutti gli oggetti monitorati?",
resoverserverDescription: "Cloudflare è il server predefinito, è possibile cambiare il server DNS.", resolverserverDescription: "Cloudflare è il server predefinito, è possibile cambiare il server DNS.",
rrtypeDescription: "Scegliere il tipo di RR che si vuole monitorare", rrtypeDescription: "Scegliere il tipo di RR che si vuole monitorare",
pauseMonitorMsg: "Si è certi di voler mettere in pausa?", pauseMonitorMsg: "Si è certi di voler mettere in pausa?",
enableDefaultNotificationDescription: "Per ogni nuovo oggetto monitorato questa notifica sarà abilitata di default. È comunque possibile disabilitare la notifica separatamente per ogni oggetto.", enableDefaultNotificationDescription: "Per ogni nuovo monitor questa notifica sarà abilitata di default. È comunque possibile disabilitare la notifica singolarmente.",
clearEventsMsg: "Si è certi di voler eliminare tutti gli eventi per questo servizio?", clearEventsMsg: "Si è certi di voler eliminare tutti gli eventi per questo servizio?",
clearHeartbeatsMsg: "Si è certi di voler eliminare tutti gli intervalli di controllo per questo servizio?", clearHeartbeatsMsg: "Si è certi di voler eliminare tutti gli intervalli di controllo per questo servizio?",
confirmClearStatisticsMsg: "Si è certi di voler eliminare TUTTE le statistiche?", confirmClearStatisticsMsg: "Si è certi di voler eliminare TUTTE le statistiche?",
importHandleDescription: "Selezionare 'Ignora gli esistenti' si vuole ignorare l'importazione degli oggetti monitorati o delle notifiche con lo stesso nome. 'Sovrascrivi' eliminerà ogni oggetto e notifica esistente.", importHandleDescription: "Selezionare \"Ignora esistenti\" se si vuole ignorare l'importazione dei monitor o delle notifiche con lo stesso nome. \"Sovrascrivi\" rimpiazzerà tutti i monitor e le notifiche presenti con quelli nel backup.",
confirmImportMsg: "Si è certi di voler importare il backup? Essere certi di aver selezionato l'opzione corretta di importazione.", confirmImportMsg: "Si è certi di voler importare il backup? Essere certi di aver selezionato l'opzione corretta di importazione.",
twoFAVerifyLabel: "Scrivi il token per verificare che l'autenticazione a due fattori funzioni", twoFAVerifyLabel: "Digita il token per verificare che l'autenticazione a due fattori funzioni correttamente:",
tokenValidSettingsMsg: "Il token è valido! È ora possibile salvare le impostazioni.", tokenValidSettingsMsg: "Il token è valido! È ora possibile salvare le impostazioni.",
confirmEnableTwoFAMsg: "Si è certi di voler abilitare l'autenticazione a due fattori?", confirmEnableTwoFAMsg: "Si è certi di voler abilitare l'autenticazione a due fattori?",
confirmDisableTwoFAMsg: "Si è certi di voler disabilitare l'autenticazione a due fattori?", confirmDisableTwoFAMsg: "Si è certi di voler disabilitare l'autenticazione a due fattori?",
Settings: "Impostazioni", Settings: "Impostazioni",
Dashboard: "Cruscotto", Dashboard: "Dashboard",
"New Update": "Nuovo Aggiornamento Disponibile", "New Update": "Nuovo aggiornamento disponibile!",
Language: "Lingua", Language: "Lingua",
Appearance: "Aspetto", Appearance: "Aspetto",
Theme: "Tema", Theme: "Tema",
General: "Generali", General: "Generale",
"Primary Base URL": "URL base primario", "Primary Base URL": "URL base primario",
Version: "Versione", Version: "Versione",
"Check Update On GitHub": "Controlla aggiornamenti su GitHub", "Check Update On GitHub": "Controlla aggiornamenti su GitHub",
List: "Lista", List: "Lista",
Add: "Aggiungi", Add: "Aggiungi",
"Add New Monitor": "Aggiungi un nuovo oggetto da monitorare", "Add New Monitor": "Aggiungi nuovo monitor",
"Quick Stats": "Statistiche rapide", "Quick Stats": "Statistiche rapide",
Up: "Su", Up: "Up",
Down: "Giù", Down: "Down",
Pending: "Pendente", Pending: "In attesa",
Unknown: "Sconosciuti", Unknown: "Sconosciuti",
Pause: "Metti in Pausa", Pause: "Metti in pausa",
Name: "Nome", Name: "Nome",
Status: "Stato", Status: "Stato",
DateTime: "Data e Ora", DateTime: "Data e Ora",
@ -65,15 +65,15 @@ export default {
Ping: "Ping", Ping: "Ping",
"Monitor Type": "Modalità di monitoraggio", "Monitor Type": "Modalità di monitoraggio",
Keyword: "Parola chiave", Keyword: "Parola chiave",
"Friendly Name": "Nomignolo", "Friendly Name": "Nome",
URL: "URL", URL: "URL",
Hostname: "Nome Host", Hostname: "Nome Host",
Port: "Porta", Port: "Porta",
"Heartbeat Interval": "Intervallo di controllo", "Heartbeat Interval": "Intervallo di controllo",
Retries: "Tentativi", Retries: "Tentativi",
"Heartbeat Retry Interval": "Intervallo tra un tentativo di controllo e l'altro", "Heartbeat Retry Interval": "Intervallo tra i tentativo di controllo",
Advanced: "Avanzate", Advanced: "Avanzate",
"Upside Down Mode": "Modalità capovolta", "Upside Down Mode": "Modalità invertita",
"Max. Redirects": "Reindirizzamenti massimi", "Max. Redirects": "Reindirizzamenti massimi",
"Accepted Status Codes": "Codici di stato accettati", "Accepted Status Codes": "Codici di stato accettati",
"Push URL": "Push URL", "Push URL": "Push URL",
@ -81,52 +81,52 @@ export default {
pushOptionalParams: "Parametri aggiuntivi: {0}", pushOptionalParams: "Parametri aggiuntivi: {0}",
Save: "Salva", Save: "Salva",
Notifications: "Notifiche", Notifications: "Notifiche",
"Not available, please setup.": "Non disponibili, da impostare.", "Not available, please setup.": "Non disponibili, da configurare.",
"Setup Notification": "Imposta le notifiche", "Setup Notification": "Configura le notifiche",
Light: "Chiaro", Light: "Chiaro",
Dark: "Scuro", Dark: "Scuro",
Auto: "Automatico", Auto: "Automatico",
"Theme - Heartbeat Bar": "Tema - Barra di Stato", "Theme - Heartbeat Bar": "Tema (barra di stato)",
Normal: "Normale", Normal: "Normale",
Bottom: "Sotto", Bottom: "Sotto",
None: "Nessuna", None: "Nessuna",
Timezone: "Fuso Orario", Timezone: "Fuso Orario",
"Search Engine Visibility": "Visibilità ai motori di ricerca", "Search Engine Visibility": "Visibilità ai motori di ricerca",
"Allow indexing": "Permetti l'indicizzazione", "Allow indexing": "Consenti l'indicizzazione",
"Discourage search engines from indexing site": "Scoraggia l'indicizzazione da parte dei motori di ricerca", "Discourage search engines from indexing site": "Evita l'indicizzazione ai motori di ricerca",
"Change Password": "Cambio Password", "Change Password": "Cambia password",
"Current Password": "Password Corrente", "Current Password": "Password corrente",
"New Password": "Nuova Password", "New Password": "Nuova password",
"Repeat New Password": "Ripetere la nuova Password", "Repeat New Password": "Ripeti nuova password",
"Update Password": "Modifica Password", "Update Password": "Modifica password",
"Disable Auth": "Disabilita l'autenticazione", "Disable Auth": "Disabilita autenticazione",
"Enable Auth": "Abilita Autenticazione", "Enable Auth": "Abilita autenticazione",
Logout: "Esci", Logout: "Esci",
Leave: "Annulla", Leave: "Annulla",
"I understand, please disable": "Lo capisco, disabilitare l'autenticazione.", "I understand, please disable": "Lo capisco, disabilitare l'autenticazione.",
Confirm: "Conferma", Confirm: "Conferma",
Yes: "Sì", Yes: "Sì",
No: "No", No: "No",
Username: "Nome Utente", Username: "Nome utente",
Password: "Password", Password: "Password",
"Remember me": "Ricordami", "Remember me": "Ricorda credenziali",
Login: "Accesso", Login: "Accesso",
"No Monitors, please": "Nessun oggetto monitorato,", "No Monitors, please": "Nessun monitor presente,",
"add one": "aggiungerne uno", "add one": "aggiungine uno!",
"Notification Type": "Tipo di notifica", "Notification Type": "Servizio di notifica",
Email: "E-mail", Email: "E-mail",
Test: "Prova", Test: "Fai una prova",
"Certificate Info": "Informazioni sul certificato", "Certificate Info": "Informazioni sul certificato",
"Resolver Server": "Server DNS", "Resolver Server": "Server DNS",
"Resource Record Type": "Tipo di Resource Record", "Resource Record Type": "Tipo di Resource Record",
"Last Result": "Ultimo risultato", "Last Result": "Ultimo risultato",
"Create your admin account": "Crea l'account amministratore", "Create your admin account": "Crea l'account amministratore",
"Repeat Password": "Ripeti Password", "Repeat Password": "Ripeti password",
"Import Backup": "Importa Backup", "Import Backup": "Importa backup",
"Export Backup": "Esporta Backup", "Export Backup": "Esporta backup",
Export: "Esporta", Export: "Esporta",
Import: "Importa", Import: "Importa",
respTime: "Tempo di Risposta (ms)", respTime: "Tempo di risposta (ms)",
notAvailableShort: "N/D", notAvailableShort: "N/D",
"Default enabled": "Abilitato di default", "Default enabled": "Abilitato di default",
"Apply on all existing monitors": "Applica su tutti i monitoraggi", "Apply on all existing monitors": "Applica su tutti i monitoraggi",
@ -136,21 +136,21 @@ export default {
Heartbeats: "Controlli", Heartbeats: "Controlli",
"Auto Get": "Rileva", "Auto Get": "Rileva",
backupDescription: "È possibile fare il backup di tutti i monitoraggi e di tutte le notifiche in un file JSON.", backupDescription: "È possibile fare il backup di tutti i monitoraggi e di tutte le notifiche in un file JSON.",
backupDescription2: "P.S.: lo storico e i dati relativi agli eventi non saranno inclusi", backupDescription2: "NOTA: lo storico e i dati relativi agli eventi non saranno inclusi nel backup",
backupDescription3: "Dati sensibili come i token di autenticazione saranno inclusi nel backup, tenere quindi in un luogo sicuro.", backupDescription3: "Dati sensibili come i token di autenticazione saranno inclusi nel backup, custodisci il file in un luogo sicuro!",
alertNoFile: "Selezionare il file da importare.", alertNoFile: "Selezionare il file da importare.",
alertWrongFileType: "Selezionare un file JSON.", alertWrongFileType: "Selezionare un file JSON.",
"Clear all statistics": "Pulisci tutte le statistiche", "Clear all statistics": "Cancella tutte le statistiche",
"Skip existing": "Ignora gli esistenti", "Skip existing": "Ignora esistenti",
Overwrite: "Sovrascrivi", Overwrite: "Sovrascrivi",
Options: "Opzioni", Options: "Opzioni",
"Keep both": "Mantieni entrambi", "Keep both": "Mantieni entrambi",
"Verify Token": "Verifica Token", "Verify Token": "Verifica token",
"Setup 2FA": "Imposta l'autenticazione a due fattori", "Setup 2FA": "Configura 2FA",
"Enable 2FA": "Abilita l'autenticazione a due fattori", "Enable 2FA": "Abilita 2FA",
"Disable 2FA": "Disabilita l'autenticazione a due fattori", "Disable 2FA": "Disabilita 2FA",
"2FA Settings": "Impostazioni autenticazione a due fattori", "2FA Settings": "Gestisci l'autenticazione a due fattori",
"Two Factor Authentication": "Autenticazione a due fattori", "Two Factor Authentication": "Autenticazione a due fattori (2FA)",
Active: "Attivata", Active: "Attivata",
Inactive: "Disattivata", Inactive: "Disattivata",
Token: "Token", Token: "Token",
@ -173,31 +173,32 @@ export default {
"Avg. Ping": "Tempo medio di risposta al ping", "Avg. Ping": "Tempo medio di risposta al ping",
"Avg. Response": "Tempo medio di risposta", "Avg. Response": "Tempo medio di risposta",
"Entry Page": "Pagina Principale", "Entry Page": "Pagina Principale",
statusPageNothing: "Non c'è nulla qui, aggiungere un gruppo oppure un oggetto da monitorare.", statusPageNothing: "Non c'è nulla qui, aggiungi un gruppo oppure un monitor.",
"No Services": "Nessun Servizio", "No Services": "Nessun servizio",
"All Systems Operational": "Tutti i sistemi sono funzionali", "All Systems Operational": "Tutti i sistemi sono funzionali",
"Partially Degraded Service": "Servizio parzialmente degradato", "Partially Degraded Service": "Servizio parzialmente degradato",
"Degraded Service": "Servizio degradato", "Degraded Service": "Servizio degradato",
"Add Group": "Aggiungi Gruppo", "Add Group": "Aggiungi gruppo",
"Add a monitor": "Aggiungi un oggetto", "Add a monitor": "Aggiungi monitor",
"Edit Status Page": "Modifica pagina di stato", "Edit Status Page": "Modifica pagina di stato",
"Go to Dashboard": "Vai al Cruscotto", "Go to Dashboard": "Vai alla dashboard",
"Status Page": "Pagina di stato", "Status Page": "Pagina di stato",
defaultNotificationName: "Allarme {notification} ({number})", "Status Pages": "Pagina di stato",
defaultNotificationName: "Notifica {notification} ({number})",
here: "qui", here: "qui",
Required: "Richiesto", Required: "Obbligatorio",
telegram: "Telegram", telegram: "Telegram",
"Bot Token": "Token del Bot", "Bot Token": "Token del bot",
wayToGetTelegramToken: "È possibile ricevere un token da {0}.", wayToGetTelegramToken: "Puoi ottenere il token da {0}.",
"Chat ID": "ID Chat", "Chat ID": "ID Chat",
supportTelegramChatID: "Supporta Chat dirette / di Gruppo / ID Canale", supportTelegramChatID: "Supporta chat private, gruppi e canali.",
wayToGetTelegramChatID: "È possibile ricereve l'ID chat mandando un messaggio al bot e poi andando in questo URL per visualizzare il chat_id:", wayToGetTelegramChatID: "È possibile ricereve l'ID chat mandando un messaggio al bot e poi andando in questo URL per visualizzare il chat_id:",
"YOUR BOT TOKEN HERE": "QUI IL TOKEN DEL BOT", "YOUR BOT TOKEN HERE": "QUI IL TOKEN DEL BOT",
chatIDNotFound: "Non trovo l'ID chat. Prima bisogna mandare un messaggio al bot", chatIDNotFound: "Non trovo l'ID chat. Prima bisogna mandare un messaggio al bot",
webhook: "Webhook", webhook: "Webhook",
"Post URL": "Post URL", "Post URL": "Post URL",
"Content Type": "Content Type", "Content Type": "Content Type",
webhookJsonDesc: "{0} va bene per qualsiasi server http moderno ad esempio express.js", webhookJsonDesc: "{0} va bene per qualsiasi server HTTP moderno ad esempio express.js",
webhookFormDataDesc: "{multipart} va bene per PHP, c'è solo bisogno di analizzare il json con {decodeFunction}", webhookFormDataDesc: "{multipart} va bene per PHP, c'è solo bisogno di analizzare il json con {decodeFunction}",
smtp: "E-mail (SMTP)", smtp: "E-mail (SMTP)",
secureOptionNone: "Nessuno / STARTTLS (25, 587)", secureOptionNone: "Nessuno / STARTTLS (25, 587)",
@ -294,29 +295,29 @@ export default {
matrixDesc1: "È possibile recuperare l'ID della stanza all'interno delle impostazioni avanzate della stanza nel client Matrix. Dovrebbe essere simile a !QMdRCpUIfLwsfjxye6:server.di.casa.", matrixDesc1: "È possibile recuperare l'ID della stanza all'interno delle impostazioni avanzate della stanza nel client Matrix. Dovrebbe essere simile a !QMdRCpUIfLwsfjxye6:server.di.casa.",
matrixDesc2: "È altamente raccomandata la creazione di un nuovo utente e di non utilizare il proprio token di accesso Matrix poiché darà pieno controllo al proprio account e a tutte le stanze in cui si ha accesso. Piuttosto, si crei un nuovo utente per invitarlo nella stanza dove si vuole ricevere le notifiche. Si può accedere al token eseguendo {0}", matrixDesc2: "È altamente raccomandata la creazione di un nuovo utente e di non utilizare il proprio token di accesso Matrix poiché darà pieno controllo al proprio account e a tutte le stanze in cui si ha accesso. Piuttosto, si crei un nuovo utente per invitarlo nella stanza dove si vuole ricevere le notifiche. Si può accedere al token eseguendo {0}",
Method: "Metodo", Method: "Metodo",
Body: "Corpo", Body: "Body",
Headers: "Intestazioni", Headers: "Intestazioni",
PushUrl: "URL di Push", PushUrl: "URL di Push",
HeadersInvalidFormat: "L'intestazione di richiesta non è un JSON valido: ", HeadersInvalidFormat: "L'intestazione di richiesta non è un JSON valido: ",
BodyInvalidFormat: "Il corpo di richiesta non è un JSON valido: ", BodyInvalidFormat: "Il corpo di richiesta non è un JSON valido: ",
"Monitor History": "Storicizzazione", "Monitor History": "Storico monitor",
clearDataOlderThan: "Mantieni lo storico per {0} giorni.", clearDataOlderThan: "Mantieni lo storico per {0} giorni.",
PasswordsDoNotMatch: "Le password non corrispondono.", PasswordsDoNotMatch: "Le password non corrispondono!",
records: "records", records: "records",
"One record": "One record", "One record": "One record",
steamApiKeyDescription: "Per monitorare un server di gioco Steam si necessita della chiave Web-API di Steam. È possibile registrare la propria chiave API qui: ", steamApiKeyDescription: "Per monitorare un server di gioco Steam è necessaria una Web-API Key di Steam. È possibile registrarne una qui: ",
"Current User": "Utente corrente", "Current User": "Utente corrente",
recent: "Recenti", recent: "Recenti",
Done: "Fatto", Done: "Fatto",
Info: "Info", Info: "Info",
Security: "Sicurezza", Security: "Sicurezza",
"Steam API Key": "Chiave API di Steam", "Steam API Key": "API Key di Steam",
"Shrink Database": "Comprimi Database", "Shrink Database": "Comprimi database",
"Pick a RR-Type...": "Scegli un tipo di RR...", "Pick a RR-Type...": "Scegli un tipo di RR...",
"Pick Accepted Status Codes...": "Scegli i codici di Stato Accettati...", "Pick Accepted Status Codes...": "Scegli i codici di Stato Accettati...",
Default: "Predefinito", Default: "Predefinito",
"HTTP Options": "Opzioni HTTP", "HTTP Options": "Opzioni HTTP",
"Create Incident": "Crea Incident", "Create Incident": "Segnala incidente",
Title: "Titolo", Title: "Titolo",
Content: "Contenuto", Content: "Contenuto",
Style: "Stile", Style: "Stile",
@ -331,24 +332,34 @@ export default {
Created: "Creato", Created: "Creato",
"Last Updated": "Ultima modifica", "Last Updated": "Ultima modifica",
Unpin: "Rimuovi", Unpin: "Rimuovi",
"Switch to Light Theme": "Utilizza tema chiaro", "Switch to Light Theme": "Utilizza il tema chiaro",
"Switch to Dark Theme": "Utilizza tema scuro", "Switch to Dark Theme": "Utilizza il tema scuro",
"Show Tags": "Mostra etichette", "Show Tags": "Mostra etichette",
"Hide Tags": "Nascondi etichette", "Hide Tags": "Nascondi etichette",
Description: "Descrizione", Description: "Descrizione",
"No monitors available.": "Nessun oggetto monitorato disponibile.", "No monitors available.": "Nessun monitor disponibile.",
"Add one": "Aggiungi", "Add one": "Aggiungine uno!",
"No Monitors": "Nessun oggetto monitorato inserito", "No Monitors": "Nessun monitor presente.",
"Untitled Group": "Gruppo senza titolo", "Untitled Group": "Gruppo senza titolo",
Services: "Servizi", Services: "Servizi",
Discard: "Scarta", Discard: "Scarta modifiche",
Cancel: "Annulla", Cancel: "Annulla",
"Powered by": "Servito da", "Powered by": "Powered by",
shrinkDatabaseDescription: "Lancia il comando VACUUM sul database SQLite. Se il database è stato creato dopo la versione 1.10.0, AUTO_VACUUM è già abilitato e questa azione non è necessaria.", shrinkDatabaseDescription: "Lancia il comando \"VACUUM\" sul database SQLite. Se il database è stato creato dopo la versione 1.10.0, la funzione \"AUTO_VACUUM\" è già abilitata di default e quindi questa azione non è necessaria.",
serwersms: "SerwerSMS.pl", serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "Nome utente API (incl. prefisso webapi_)", serwersmsAPIUser: "Nome utente API (incl. prefisso webapi_)",
serwersmsAPIPassword: "Password API", serwersmsAPIPassword: "Password API",
serwersmsPhoneNumber: "Numero di Telefono", serwersmsPhoneNumber: "Numero di Telefono",
serwersmsSenderName: "Nome del mittente SMS (registrato via portale cliente)", serwersmsSenderName: "Nome del mittente SMS (registrato via portale cliente)",
stackfield: "Stackfield", stackfield: "Stackfield",
smtpDkimSettings: "Impostazioni DKIM",
smtpDkimDesc: "Fare riferimento a Nodemailer DKIM {0} per l'utilizzo.",
documentation: "documentazione",
smtpDkimDomain: "Dominio",
smtpDkimKeySelector: "Selettore Chiave",
smtpDkimPrivateKey: "Chiave Privata",
smtpDkimHashAlgo: "Algoritmo di hashing (opzionale)",
smtpDkimheaderFieldNames: "Campi Intestazione da firmare (opzionale)",
smtpDkimskipFields: "Campi Intestazione da non firmare (opzionale)",
GoogleChat: "Google Chat (solo per Google Workspace)",
}; };

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

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

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

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

@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "Wstrzymane", pauseDashboardHome: "Wstrzymane",
deleteMonitorMsg: "Czy na pewno chcesz usunąć ten monitor?", deleteMonitorMsg: "Czy na pewno chcesz usunąć ten monitor?",
deleteNotificationMsg: "Czy na pewno chcesz usunąć to powiadomienie dla wszystkich monitorów?", deleteNotificationMsg: "Czy na pewno chcesz usunąć to powiadomienie dla wszystkich monitorów?",
resoverserverDescription: "Cloudflare jest domyślnym serwerem, możesz zmienić serwer resolver w każdej chwili.", resolverserverDescription: "Cloudflare jest domyślnym serwerem, możesz zmienić serwer resolver w każdej chwili.",
rrtypeDescription: "Wybierz rodzaj rekordu, który chcesz monitorować.", rrtypeDescription: "Wybierz rodzaj rekordu, który chcesz monitorować.",
pauseMonitorMsg: "Czy na pewno chcesz wstrzymać monitorowanie?", pauseMonitorMsg: "Czy na pewno chcesz wstrzymać monitorowanie?",
enableDefaultNotificationDescription: "Dla każdego nowego monitora to powiadomienie będzie domyślnie włączone. Nadal możesz wyłączyć powiadomienia osobno dla każdego monitora.", enableDefaultNotificationDescription: "Dla każdego nowego monitora to powiadomienie będzie domyślnie włączone. Nadal możesz wyłączyć powiadomienia osobno dla każdego monitora.",
@ -22,8 +22,8 @@ export default {
confirmClearStatisticsMsg: "Jesteś pewien, że chcesz usunąć WSZYSTKIE statystyki?", confirmClearStatisticsMsg: "Jesteś pewien, że chcesz usunąć WSZYSTKIE statystyki?",
importHandleDescription: "Wybierz 'Pomiń istniejące', jeśli chcesz pominąć każdy monitor lub powiadomienie o tej samej nazwie. 'Nadpisz' spowoduje usunięcie każdego istniejącego monitora i powiadomienia.", importHandleDescription: "Wybierz 'Pomiń istniejące', jeśli chcesz pominąć każdy monitor lub powiadomienie o tej samej nazwie. 'Nadpisz' spowoduje usunięcie każdego istniejącego monitora i powiadomienia.",
confirmImportMsg: "Czy na pewno chcesz zaimportować kopię zapasową? Upewnij się, że wybrałeś właściwą opcję importu.", confirmImportMsg: "Czy na pewno chcesz zaimportować kopię zapasową? Upewnij się, że wybrałeś właściwą opcję importu.",
twoFAVerifyLabel: "Proszę podaj swój token 2FA, aby sprawdzić czy 2FA działa.", twoFAVerifyLabel: "Proszę, podaj swój token 2FA, aby sprawdzić, czy 2FA działa.",
tokenValidSettingsMsg: "Token jest prawdiłowy! Teraz możesz zapisać ustawienia 2FA.", tokenValidSettingsMsg: "Token jest prawidłowy! Teraz możesz zapisać ustawienia 2FA.",
confirmEnableTwoFAMsg: "Jesteś pewien, że chcesz włączyć 2FA?", confirmEnableTwoFAMsg: "Jesteś pewien, że chcesz włączyć 2FA?",
confirmDisableTwoFAMsg: "Jesteś pewien, że chcesz wyłączyć 2FA?", confirmDisableTwoFAMsg: "Jesteś pewien, że chcesz wyłączyć 2FA?",
Settings: "Ustawienia", Settings: "Ustawienia",
@ -68,7 +68,7 @@ export default {
URL: "URL", URL: "URL",
Hostname: "Hostname", Hostname: "Hostname",
Port: "Port", Port: "Port",
"Heartbeat Interval": "Czętotliwość bicia serca", "Heartbeat Interval": "Częstotliwość bicia serca",
Retries: "Prób", Retries: "Prób",
"Heartbeat Retry Interval": "Częstotliwość ponawiania bicia serca", "Heartbeat Retry Interval": "Częstotliwość ponawiania bicia serca",
Advanced: "Zaawansowane", Advanced: "Zaawansowane",
@ -110,7 +110,7 @@ export default {
"No Monitors, please": "Brak monitorów, proszę", "No Monitors, please": "Brak monitorów, proszę",
"add one": "dodać jeden", "add one": "dodać jeden",
"Notification Type": "Rodzaj powiadomienia", "Notification Type": "Rodzaj powiadomienia",
Email: "Email", Email: "E-mail",
Test: "Test", Test: "Test",
"Certificate Info": "Informacje o certyfikacie", "Certificate Info": "Informacje o certyfikacie",
"Resolver Server": "Serwer rozwiązywania nazw", "Resolver Server": "Serwer rozwiązywania nazw",
@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Edytuj stronę statusu", "Edit Status Page": "Edytuj stronę statusu",
"Go to Dashboard": "Idź do panelu", "Go to Dashboard": "Idź do panelu",
"Status Page": "Strona statusu", "Status Page": "Strona statusu",
"Status Pages": "Strona statusu",
defaultNotificationName: "Moje powiadomienie {notification} ({number})", defaultNotificationName: "Moje powiadomienie {notification} ({number})",
here: "tutaj", here: "tutaj",
Required: "Wymagane", Required: "Wymagane",
@ -188,13 +189,13 @@ export default {
"Chat ID": "Identyfikator czatu", "Chat ID": "Identyfikator czatu",
supportTelegramChatID: "Czat wsparcia technicznego / Bezpośrednia rozmowa / Czat grupowy", supportTelegramChatID: "Czat wsparcia technicznego / Bezpośrednia rozmowa / Czat grupowy",
wayToGetTelegramChatID: "Możesz uzyskać swój identyfikator czatu, wysyłając wiadomość do bota i przechodząc pod ten adres URL, aby wyświetlić identyfikator czatu:", wayToGetTelegramChatID: "Możesz uzyskać swój identyfikator czatu, wysyłając wiadomość do bota i przechodząc pod ten adres URL, aby wyświetlić identyfikator czatu:",
"YOUR BOT TOKEN HERE": "TWOJ TOKEN BOTA", "YOUR BOT TOKEN HERE": "TWÓJ TOKEN BOTA",
chatIDNotFound: "Identyfikator czatu nie znaleziony, najpierw napisz do bota", chatIDNotFound: "Identyfikator czatu nie znaleziony, najpierw napisz do bota",
webhook: "Webhook", webhook: "Webhook",
"Post URL": "Adres URL", "Post URL": "Adres URL",
"Content Type": "Rodzaj danych", "Content Type": "Rodzaj danych",
webhookJsonDesc: "{0} jest dobry w przypadku serwerów HTTP, takich jak express.js", webhookJsonDesc: "{0} jest dobry w przypadku serwerów HTTP, takich jak express.js",
webhookFormDataDesc: "{multipart} jest dobry dla PHP, musisz jedynie przetowrzyć dane przez {decodeFunction}", webhookFormDataDesc: "{multipart} jest dobry dla PHP, musisz jedynie przetworzyć dane przez {decodeFunction}",
smtp: "Email (SMTP)", smtp: "Email (SMTP)",
secureOptionNone: "Brak / STARTTLS (25, 587)", secureOptionNone: "Brak / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)", secureOptionTLS: "TLS (465)",
@ -205,7 +206,7 @@ export default {
smtpBCC: "UDW", smtpBCC: "UDW",
discord: "Discord", discord: "Discord",
"Discord Webhook URL": "URL webhook Discorda", "Discord Webhook URL": "URL webhook Discorda",
wayToGetDiscordURL: "Możesz go uzyskać przechodząc do Ustawienia serwera -> Integracje -> Tworzenie webhooka", wayToGetDiscordURL: "Możesz go uzyskać, przechodząc do Ustawienia serwera -> Integracje -> Tworzenie webhooka",
"Bot Display Name": "Wyświetlana nazwa bota", "Bot Display Name": "Wyświetlana nazwa bota",
"Prefix Custom Message": "Własny początek wiadomości", "Prefix Custom Message": "Własny początek wiadomości",
"Hello @everyone is...": "Hej {'@'}everyone ...", "Hello @everyone is...": "Hej {'@'}everyone ...",
@ -237,6 +238,7 @@ export default {
promosms: "PromoSMS", promosms: "PromoSMS",
lunasea: "LunaSea", lunasea: "LunaSea",
apprise: "Apprise (obsługuje 50+ usług powiadomień)", apprise: "Apprise (obsługuje 50+ usług powiadomień)",
GoogleChat: "Google Chat (wyłącznie Google Workspace)",
pushbullet: "Pushbullet", pushbullet: "Pushbullet",
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
@ -259,7 +261,7 @@ export default {
"Read more:": "Czytaj dalej: {0}", "Read more:": "Czytaj dalej: {0}",
"Status:": "Status: {0}", "Status:": "Status: {0}",
"Read more": "Czytaj dalej", "Read more": "Czytaj dalej",
appriseInstalled: "Apprise jest zostało zainstalowane.", appriseInstalled: "Apprise jest zainstalowane.",
appriseNotInstalled: "Apprise nie zostało zainstalowane. {0}", appriseNotInstalled: "Apprise nie zostało zainstalowane. {0}",
"Access Token": "Token dostępu", "Access Token": "Token dostępu",
"Channel access token": "Token dostępu kanału", "Channel access token": "Token dostępu kanału",
@ -289,7 +291,7 @@ export default {
"Feishu WebHookUrl": "Feishu WebHookURL", "Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "Adres URL serwera domowego (z http(s):// i opcjonalnie port)", matrixHomeserverURL: "Adres URL serwera domowego (z http(s):// i opcjonalnie port)",
"Internal Room Id": "Wewnętrzne ID pokoju", "Internal Room Id": "Wewnętrzne ID pokoju",
matrixDesc1: "Możesz znaleźć wewnętrzne ID pokoju patrząc w zaawansowanej sekcji ustawień pokoju w twoim kliencie Matrix. Powinien on wyglądać jak !QMdRCpUIfLwsfjxye6:home.server.", matrixDesc1: "Możesz znaleźć wewnętrzne ID pokoju, patrząc w zaawansowanej sekcji ustawień pokoju w twoim kliencie Matrix. Powinien on wyglądać jak !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "Jest wysoce zalecane, abyś stworzył nowego użytkownika i nie używał tokena dostępu swojego użytkownika Matrix, ponieważ pozwoli on na pełny dostęp do twojego konta i wszystkich pokoi, do których dołączyłeś. Zamiast tego, utwórz nowego użytkownika i zaproś go tylko do pokoju, w którym chcesz otrzymywać powiadomienia. Możesz uzyskać token dostępu przez uruchomienie {0}", matrixDesc2: "Jest wysoce zalecane, abyś stworzył nowego użytkownika i nie używał tokena dostępu swojego użytkownika Matrix, ponieważ pozwoli on na pełny dostęp do twojego konta i wszystkich pokoi, do których dołączyłeś. Zamiast tego, utwórz nowego użytkownika i zaproś go tylko do pokoju, w którym chcesz otrzymywać powiadomienia. Możesz uzyskać token dostępu przez uruchomienie {0}",
Method: "Metoda", Method: "Metoda",
Body: "Treść", Body: "Treść",
@ -305,6 +307,44 @@ export default {
steamApiKeyDescription: "Do monitorowania serwera gier Steam potrzebny jest klucz Steam Web-API. Możesz zarejestrować swój klucz API tutaj: ", steamApiKeyDescription: "Do monitorowania serwera gier Steam potrzebny jest klucz Steam Web-API. Możesz zarejestrować swój klucz API tutaj: ",
"Current User": "Aktualny użytkownik", "Current User": "Aktualny użytkownik",
recent: "Ostatnie", recent: "Ostatnie",
Done: "Zrobione",
Info: "Info",
Security: "Bezpieczeństwo",
"Steam API Key": "Klucz Steam API",
"Shrink Database": "Zmniejsz bazę danych",
"Pick a RR-Type...": "Wybierz typ RR...",
"Pick Accepted Status Codes...": "Wybierz akceptowalne kody statusu...",
Default: "Domyślnie",
"HTTP Options": "Opcje HTTP",
"Create Incident": "Stwórz incydent",
Title: "Tytuł",
Content: "Treść",
Style: "Styl",
info: "info",
warning: "ostrzeżenie",
danger: "niebezpieczeństwo",
primary: "podstawowy",
light: "jasny",
dark: "ciemny",
Post: "Wyślij",
"Please input title and content": "Podaj tytuł i treść",
Created: "Stworzony",
"Last Updated": "Ostatnio zaktualizowany",
Unpin: "Odepnij",
"Switch to Light Theme": "Przełącz na jasny motyw",
"Switch to Dark Theme": "Przełącz na ciemny motyw",
"Show Tags": "Pokaż tagi",
"Hide Tags": "Ukryj tagi",
Description: "Opis",
"No monitors available.": "Brak dostępnych monitorów.",
"Add one": "Dodaj jeden",
"No Monitors": "Brak monitorów",
"Untitled Group": "Nienazwana grupa",
Services: "Usługi",
Discard: "Odrzuć",
Cancel: "Anuluj",
"Powered by": "Napędzane przez",
shrinkDatabaseDescription: "Uruchom VACUUM na bazie SQLite. Jeżeli twoja baza została stworzona po wersji 1.10.0, to posiada już włączoną opcję AUTO_VACUUM i stosowanie ręcznego oczyszczania nie jest potrzebne.",
clicksendsms: "ClickSend SMS", clicksendsms: "ClickSend SMS",
apiCredentials: "Poświadczenia API", apiCredentials: "Poświadczenia API",
serwersms: "SerwerSMS.pl", serwersms: "SerwerSMS.pl",
@ -312,4 +352,14 @@ export default {
serwersmsAPIPassword: "Hasło API", serwersmsAPIPassword: "Hasło API",
serwersmsPhoneNumber: "Numer telefonu", serwersmsPhoneNumber: "Numer telefonu",
serwersmsSenderName: "Nazwa nadawcy (zatwierdzona w panelu klienta)", serwersmsSenderName: "Nazwa nadawcy (zatwierdzona w panelu klienta)",
"stackfield": "Stackfield",
smtpDkimSettings: "Ustawienia DKIM",
smtpDkimDesc: "Zapoznaj się z Nodemailer DKIM {0}, aby dowiedzieć się więcej",
documentation: "dokumentacja",
smtpDkimDomain: "Nazwa domeny",
smtpDkimKeySelector: "Selektor klucza",
smtpDkimPrivateKey: "Klucz prywatny",
smtpDkimHashAlgo: "Algorytm Hashowania (opcjonalne)",
smtpDkimheaderFieldNames: "Klucze nagłówka do podpisu (opcjonalne)",
smtpDkimskipFields: "Klucze nagłówka do pominięcia (opcjonalne)",
}; };

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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save