Merge remote-tracking branch 'origin/master' into radius-check

# Conflicts:
#	server/database.js
#	server/model/monitor.js
#	server/server.js
#	server/util-server.js
#	src/pages/EditMonitor.vue
pull/1212/head^2
Louis Lam 2 years ago
commit 3b3763351b

@ -1,3 +1,5 @@
👉 Delete this line if you have read and agree our pull request rules and guidelines: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma
# Description
Fixes #(issue)

@ -8,6 +8,7 @@
"declaration-empty-line-before": null,
"alpha-value-notation": "number",
"color-function-notation": "legacy",
"shorthand-property-no-redundant-values": null
"shorthand-property-no-redundant-values": null,
"color-hex-length": null,
}
}

@ -27,17 +27,30 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
## Can I create a pull request for Uptime Kuma?
(Updated 2022-04-24) Since I don't want to waste your time, be sure to create empty draft pull request, so we can discuss first.
Yes, you can. However, since I don't want to waste your time, be sure to **create empty draft pull request, so we can discuss first** if it is a large pull request or you don't know it will be merged or not.
Also, please don't rush or ask for ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
✅ Accept:
- Bug/Security fix
- Translations
- Adding notification providers
⚠️ Discuss First
⚠️ Discussion First
- Large pull requests
- New features
❌ Won't Merge
- Do not pass auto test
- Any breaking changes
- Duplicated pull request
- Buggy
- Existing logic is completely modified or deleted for no reason
- A function that is completely out of scope
### Recommended Pull Request Guideline
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
@ -53,22 +66,15 @@ Before deep into coding, discussion first is preferred. Creating an empty pull r
1. Click "Change to draft"
1. Discussion
#### ❌ Won't Merge
- Any breaking changes
- Duplicated pull request
- Buggy
- Existing logic is completely modified or deleted
- A function that is completely out of scope
## Project Styles
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
- Settings should be configurable in the frontend. Env var is not encouraged.
- Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`.
- Easy to use
- The web UI styling should be consistent and nice.
## Coding Styles
@ -80,8 +86,8 @@ I personally do not like something need to learn so much and need to config so m
## Name convention
- Javascript/Typescript: camelCaseType
- SQLite: underscore_type
- CSS/SCSS: dash-type
- SQLite: snake_case (Underscore)
- CSS/SCSS: kebab-case (Dash)
## Tools

@ -23,7 +23,7 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec
## ⭐ Features
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server.
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers.
* Fancy, Reactive, Fast UI/UX.
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
* 20 second intervals.
@ -151,9 +151,9 @@ You can discuss or ask for help in [issues](https://github.com/louislam/uptime-k
### Subreddit
My Reddit account: louislamlam
My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam).
You can mention me if you ask a question on Reddit.
https://www.reddit.com/r/UptimeKuma/
[r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
## Contribute

@ -8,9 +8,6 @@ Do not use the issue tracker or discuss it in the public as it will cause more d
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
### Uptime Kuma Versions
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.

@ -1,18 +1,35 @@
import legacy from "@vitejs/plugin-legacy";
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import visualizer from "rollup-plugin-visualizer";
import viteCompression from "vite-plugin-compression";
const postCssScss = require("postcss-scss");
const postcssRTLCSS = require("postcss-rtlcss");
const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i;
// https://vitejs.dev/config/
export default defineConfig({
define: {
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
},
plugins: [
vue(),
legacy({
targets: [ "ie > 11" ],
additionalLegacyPolyfills: [ "regenerator-runtime/runtime" ]
})
targets: [ "since 2015" ],
}),
visualizer({
filename: "tmp/dist-stats.html"
}),
viteCompression({
algorithm: "gzip",
filter: viteCompressionFilter,
}),
viteCompression({
algorithm: "brotliCompress",
filter: viteCompressionFilter,
}),
],
css: {
postcss: {
@ -21,4 +38,13 @@ export default defineConfig({
"plugins": [ postcssRTLCSS ]
}
},
build: {
rollupOptions: {
output: {
manualChunks(id, { getModuleInfo, getModuleIds }) {
}
}
},
}
});

@ -0,0 +1,5 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor_group
ADD send_url BOOLEAN DEFAULT 0 NOT NULL;
COMMIT;

@ -0,0 +1,18 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
CREATE TABLE docker_host (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
user_id INT NOT NULL,
docker_daemon VARCHAR(255),
docker_type VARCHAR(255),
name VARCHAR(255)
);
ALTER TABLE monitor
ADD docker_host INTEGER REFERENCES docker_host(id);
ALTER TABLE monitor
ADD docker_container VARCHAR(255);
COMMIT;

@ -0,0 +1,18 @@
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD auth_method VARCHAR(250);
ALTER TABLE monitor
ADD auth_domain TEXT;
ALTER TABLE monitor
ADD auth_workstation TEXT;
COMMIT;
BEGIN TRANSACTION;
UPDATE monitor
SET auth_method = 'basic'
WHERE basic_auth_user is not null;
COMMIT;

@ -0,0 +1,10 @@
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD database_connection_string VARCHAR(2000);
ALTER TABLE monitor
ADD database_query TEXT;
COMMIT

@ -4,5 +4,5 @@ WORKDIR /app
# 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 && \
pip3 --no-cache-dir install apprise==0.9.8.3 && \
pip3 --no-cache-dir install apprise==0.9.9 && \
rm -rf /root/.cache

@ -11,8 +11,9 @@ WORKDIR /app
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 \
sqlite3 iputils-ping util-linux dumb-init && \
pip3 --no-cache-dir install apprise==0.9.8.3 && \
rm -rf /var/lib/apt/lists/*
pip3 --no-cache-dir install apprise==0.9.9 && \
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove
# Install cloudflared
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
@ -22,5 +23,6 @@ RUN node ./extra/download-cloudflared.js $TARGETPLATFORM && \
apt update && \
apt --yes --no-install-recommends install ./cloudflared.deb && \
rm -rf /var/lib/apt/lists/* && \
rm -f cloudflared.deb
rm -f cloudflared.deb && \
apt --yes autoremove

@ -1,4 +1,4 @@
# Simple docker-composer.yml
# Simple docker-compose.yml
# You can change your port or volume location
version: '3.3'

@ -41,7 +41,7 @@ function updateWiki(newVersion) {
function safeDelete(dir) {
if (fs.existsSync(dir)) {
fs.rmdirSync(dir, {
fs.rm(dir, {
recursive: true,
});
}

13722
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.16.0-beta.0",
"version": "1.17.1",
"license": "MIT",
"repository": {
"type": "git",
@ -39,7 +39,7 @@
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.15.1 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.17.1 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@ -57,32 +57,29 @@
"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"
"git-remove-tag": "git tag -d",
"build-dist-and-restart": "npm run build && npm run start-server-dev"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "~1.2.36",
"@fortawesome/free-regular-svg-icons": "~5.15.4",
"@fortawesome/free-solid-svg-icons": "~5.15.4",
"@fortawesome/vue-fontawesome": "~3.0.0-5",
"@louislam/sqlite3": "~15.0.6",
"@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0",
"axios": "~0.26.1",
"axios-ntlm": "^1.3.0",
"badge-maker": "^3.3.1",
"bcryptjs": "~2.4.3",
"bootstrap": "5.1.3",
"bree": "~7.1.5",
"cacheable-lookup": "~6.0.4",
"chardet": "^1.3.0",
"chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0",
"check-password-strength": "^2.0.5",
"cheerio": "^1.0.0-rc.10",
"chroma-js": "^2.1.2",
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
"dayjs": "~1.10.8",
"compression": "^1.7.4",
"dayjs": "^1.11.0",
"express": "~4.17.3",
"express-basic-auth": "~1.2.1",
"favico.js": "^0.3.10",
"express-static-gzip": "^2.1.7",
"form-data": "~4.0.0",
"http-graceful-shutdown": "~3.1.7",
"http-proxy-agent": "^5.0.0",
@ -92,65 +89,78 @@
"jwt-decode": "^3.1.2",
"limiter": "^2.1.0",
"mqtt": "^4.2.8",
"mssql": "^8.1.0",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "^1.0.0",
"nodemailer": "~6.6.5",
"notp": "~2.0.3",
"password-hash": "~1.2.2",
"postcss-rtlcss": "~3.4.1",
"postcss-scss": "~4.0.3",
"prismjs": "^1.27.0",
"pg": "^8.7.3",
"pg-connection-string": "^2.5.0",
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1",
"qrcode": "~1.5.0",
"redbean-node": "0.1.3",
"redbean-node": "0.1.4",
"socket.io": "~4.4.1",
"socket.io-client": "~4.4.1",
"socks-proxy-agent": "^6.1.1",
"socks-proxy-agent": "6.1.1",
"tar": "^6.1.11",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2",
"timezones-list": "~3.0.1",
"v-pagination-3": "~0.1.7",
"vue": "next",
"vue-chart-3": "3.0.9",
"vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "~3.0.4",
"vue-i18n": "~9.1.9",
"vue-image-crop-upload": "~3.0.3",
"vue-multiselect": "~3.0.0-alpha.2",
"vue-prism-editor": "^2.0.0-alpha.2",
"vue-qrcode": "~1.0.0",
"vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5",
"vuedraggable": "~4.1.0"
"thirty-two": "~1.0.2"
},
"devDependencies": {
"@actions/github": "~5.0.1",
"@babel/eslint-parser": "~7.17.0",
"@babel/preset-env": "^7.15.8",
"@fortawesome/fontawesome-svg-core": "~1.2.36",
"@fortawesome/free-regular-svg-icons": "~5.15.4",
"@fortawesome/free-solid-svg-icons": "~5.15.4",
"@fortawesome/vue-fontawesome": "~3.0.0-5",
"@popperjs/core": "~2.10.2",
"@types/bootstrap": "~5.1.9",
"@vitejs/plugin-legacy": "~1.6.4",
"@vitejs/plugin-vue": "~1.9.4",
"@vue/compiler-sfc": "~3.2.31",
"@vitejs/plugin-legacy": "~1.8.2",
"@vitejs/plugin-vue": "~2.3.3",
"@vue/compiler-sfc": "~3.2.36",
"aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0",
"bootstrap": "5.1.3",
"chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0",
"concurrently": "^7.1.0",
"core-js": "~3.18.3",
"cross-env": "~7.0.3",
"dns2": "~2.0.1",
"eslint": "~8.14.0",
"eslint-plugin-vue": "~8.7.1",
"favico.js": "^0.3.10",
"jest": "~27.2.5",
"jest-puppeteer": "~6.0.3",
"npm-check-updates": "^12.5.9",
"postcss-html": "^1.3.1",
"postcss-rtlcss": "~3.4.1",
"postcss-scss": "~4.0.3",
"prismjs": "^1.27.0",
"puppeteer": "~13.1.3",
"qrcode": "~1.5.0",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "~1.42.1",
"stylelint": "~14.7.1",
"stylelint-config-standard": "~25.0.0",
"timezones-list": "~3.0.1",
"typescript": "~4.4.4",
"vite": "~2.6.14",
"v-pagination-3": "~0.1.7",
"vite": "~2.9.9",
"vite-plugin-compression": "^0.5.1",
"vue": "next",
"vue-chart-3": "3.0.9",
"vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "~3.0.4",
"vue-i18n": "~9.1.9",
"vue-image-crop-upload": "~3.0.3",
"vue-multiselect": "~3.0.0-alpha.2",
"vue-prism-editor": "^2.0.0-alpha.2",
"vue-qrcode": "~1.0.0",
"vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5",
"vuedraggable": "~4.1.0",
"wait-on": "^6.0.1"
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 893 B

@ -0,0 +1,54 @@
const https = require("https");
const http = require("http");
const CacheableLookup = require("cacheable-lookup");
class CacheableDnsHttpAgent {
static cacheable = new CacheableLookup();
static httpAgentList = {};
static httpsAgentList = {};
/**
* Register cacheable to global agents
*/
static registerGlobalAgent() {
this.cacheable.install(http.globalAgent);
this.cacheable.install(https.globalAgent);
}
static install(agent) {
this.cacheable.install(agent);
}
/**
* @var {https.AgentOptions} agentOptions
* @return {https.Agent}
*/
static getHttpsAgent(agentOptions) {
let key = JSON.stringify(agentOptions);
if (!(key in this.httpsAgentList)) {
this.httpsAgentList[key] = new https.Agent(agentOptions);
this.cacheable.install(this.httpsAgentList[key]);
}
return this.httpsAgentList[key];
}
/**
* @var {http.AgentOptions} agentOptions
* @return {https.Agents}
*/
static getHttpAgent(agentOptions) {
let key = JSON.stringify(agentOptions);
if (!(key in this.httpAgentList)) {
this.httpAgentList[key] = new http.Agent(agentOptions);
this.cacheable.install(this.httpAgentList[key]);
}
return this.httpAgentList[key];
}
}
module.exports = {
CacheableDnsHttpAgent,
};

@ -22,7 +22,10 @@ async function sendNotificationList(socket) {
]);
for (let bean of list) {
result.push(bean.export());
let notificationObject = bean.export();
notificationObject.isDefault = (notificationObject.isDefault === 1);
notificationObject.active = (notificationObject.active === 1);
result.push(notificationObject);
}
io.to(socket.userID).emit("notificationList", result);
@ -122,10 +125,35 @@ async function sendInfo(socket) {
});
}
/**
* Send list of docker hosts to client
* @param {Socket} socket Socket.io socket instance
* @returns {Promise<Bean[]>}
*/
async function sendDockerHostList(socket) {
const timeLogger = new TimeLogger();
let result = [];
let list = await R.find("docker_host", " user_id = ? ", [
socket.userID,
]);
for (let bean of list) {
result.push(bean.toJSON());
}
io.to(socket.userID).emit("dockerHostList", result);
timeLogger.print("Send Docker Host List");
return list;
}
module.exports = {
sendNotificationList,
sendImportantHeartbeatList,
sendHeartbeatList,
sendProxyList,
sendInfo,
sendDockerHostList
};

@ -53,11 +53,15 @@ class Database {
"patch-2fa-invalidate-used-token.sql": true,
"patch-notification_sent_history.sql": true,
"patch-monitor-basic-auth.sql": true,
"patch-add-docker-columns.sql": true,
"patch-status-page.sql": true,
"patch-proxy.sql": true,
"patch-monitor-expiry-notification.sql": true,
"patch-status-page-footer-css.sql": true,
"patch-added-mqtt-monitor.sql": true,
"patch-add-clickable-status-page-link.sql": true,
"patch-add-sqlserver-monitor.sql": true,
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
"patch-add-radius-monitor.sql": true,
};
@ -145,6 +149,9 @@ class Database {
await R.exec("PRAGMA cache_size = -12000");
await R.exec("PRAGMA auto_vacuum = FULL");
// Avoid error "SQLITE_BUSY: database is locked" by allowing SQLITE to wait up to 5 seconds to do a write
await R.exec("PRAGMA busy_timeout = 5000");
// 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
@ -176,7 +183,13 @@ class Database {
} else {
log.info("db", "Database patch is needed");
this.backup(version);
try {
this.backup(version);
} catch (e) {
log.error("db", e);
log.error("db", "Unable to create a backup before patching the database. Please make sure you have enough space and permission.");
process.exit(1);
}
// Try catch anything here, if gone wrong, restore the backup
try {
@ -444,6 +457,23 @@ class Database {
this.backupWalPath = walPath + ".bak" + version;
fs.copyFileSync(walPath, this.backupWalPath);
}
// Double confirm if all files actually backup
if (!fs.existsSync(this.backupPath)) {
throw new Error("Backup failed! " + this.backupPath);
}
if (fs.existsSync(shmPath)) {
if (!fs.existsSync(this.backupShmPath)) {
throw new Error("Backup failed! " + this.backupShmPath);
}
}
if (fs.existsSync(walPath)) {
if (!fs.existsSync(this.backupWalPath)) {
throw new Error("Backup failed! " + this.backupWalPath);
}
}
}
}

@ -0,0 +1,106 @@
const axios = require("axios");
const { R } = require("redbean-node");
const version = require("../package.json").version;
const https = require("https");
class DockerHost {
/**
* Save a docker host
* @param {Object} dockerHost Docker host to save
* @param {?number} dockerHostID ID of the docker host to update
* @param {number} userID ID of the user who adds the docker host
* @returns {Promise<Bean>}
*/
static async save(dockerHost, dockerHostID, userID) {
let bean;
if (dockerHostID) {
bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
if (!bean) {
throw new Error("docker host not found");
}
} else {
bean = R.dispense("docker_host");
}
bean.user_id = userID;
bean.docker_daemon = dockerHost.dockerDaemon;
bean.docker_type = dockerHost.dockerType;
bean.name = dockerHost.name;
await R.store(bean);
return bean;
}
/**
* Delete a Docker host
* @param {number} dockerHostID ID of the Docker host to delete
* @param {number} userID ID of the user who created the Docker host
* @returns {Promise<void>}
*/
static async delete(dockerHostID, userID) {
let bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
if (!bean) {
throw new Error("docker host not found");
}
// Delete removed proxy from monitors if exists
await R.exec("UPDATE monitor SET docker_host = null WHERE docker_host = ?", [ dockerHostID ]);
await R.trash(bean);
}
/**
* Fetches the amount of containers on the Docker host
* @param {Object} dockerHost Docker host to check for
* @returns {number} Total amount of containers on the host
*/
static async testDockerHost(dockerHost) {
const options = {
url: "/containers/json?all=true",
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version
},
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: false,
}),
};
if (dockerHost.dockerType === "socket") {
options.socketPath = dockerHost.dockerDaemon;
} else if (dockerHost.dockerType === "tcp") {
options.baseURL = dockerHost.dockerDaemon;
}
let res = await axios.request(options);
if (Array.isArray(res.data)) {
if (res.data.length > 1) {
if ("ImageID" in res.data[0]) {
return res.data.length;
} else {
throw new Error("Invalid Docker response, is it Docker really a daemon?");
}
} else {
return res.data.length;
}
} else {
throw new Error("Invalid Docker response, is it Docker really a daemon?");
}
}
}
module.exports = {
DockerHost,
};

@ -0,0 +1,19 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
class DockerHost extends BeanModel {
/**
* Returns an object that ready to parse to JSON
* @returns {Object}
*/
toJSON() {
return {
id: this.id,
userID: this.user_id,
dockerDaemon: this.docker_daemon,
dockerType: this.docker_type,
name: this.name,
};
}
}
module.exports = DockerHost;

@ -31,7 +31,7 @@ class Group extends BeanModel {
*/
async getMonitorList() {
return R.convertToBeans("monitor", await R.getAll(`
SELECT monitor.* FROM monitor, monitor_group
SELECT monitor.*, monitor_group.send_url FROM monitor, monitor_group
WHERE monitor.id = monitor_group.monitor_id
AND group_id = ?
ORDER BY monitor_group.weight

@ -7,7 +7,7 @@ dayjs.extend(timezone);
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, radius, setting, mqttAsync } = require("../util-server");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification");
@ -16,6 +16,7 @@ const { demoMode } = require("../config");
const version = require("../../package.json").version;
const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
/**
* status:
@ -34,7 +35,13 @@ class Monitor extends BeanModel {
let obj = {
id: this.id,
name: this.name,
sendUrl: this.sendUrl,
};
if (this.sendUrl) {
obj.url = this.url;
}
if (showTags) {
obj.tags = await this.getTags();
}
@ -81,6 +88,9 @@ class Monitor extends BeanModel {
dns_resolve_type: this.dns_resolve_type,
dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result,
pushToken: this.pushToken,
docker_container: this.docker_container,
docker_host: this.docker_host,
proxyId: this.proxy_id,
notificationIDList,
tags: tags,
@ -88,6 +98,11 @@ class Monitor extends BeanModel {
mqttPassword: this.mqttPassword,
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage,
databaseConnectionString: this.databaseConnectionString,
databaseQuery: this.databaseQuery,
authMethod: this.authMethod,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
radiusUsername: this.radiusUsername,
radiusPassword: this.radiusPassword,
radiusCalledStationId: this.radiusCalledStationId,
@ -197,7 +212,7 @@ class Monitor extends BeanModel {
let bean = R.dispense("heartbeat");
bean.monitor_id = this.id;
bean.time = R.isoDateTime(dayjs.utc());
bean.time = R.isoDateTimeMillis(dayjs.utc());
bean.status = DOWN;
if (this.isUpsideDown()) {
@ -218,7 +233,7 @@ class Monitor extends BeanModel {
// HTTP basic auth
let basicAuthHeader = {};
if (this.basic_auth_user) {
if (this.auth_method === "basic") {
basicAuthHeader = {
"Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
};
@ -269,7 +284,21 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
log.debug("monitor", `[${this.name}] Axios Request`);
let res = await axios.request(options);
let res;
if (this.auth_method === "ntlm") {
options.httpsAgent.keepAlive = true;
res = await httpNtlm(options, {
username: this.basic_auth_user,
password: this.basic_auth_pass,
domain: this.authDomain,
workstation: this.authWorkstation ? this.authWorkstation : undefined
});
} else {
res = await axios.request(options);
}
bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime;
@ -317,7 +346,11 @@ class Monitor extends BeanModel {
bean.msg += ", keyword is found";
bean.status = UP;
} else {
throw new Error(bean.msg + ", but keyword is not found");
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
if (data.length > 50) {
data = data.substring(0, 47) + "...";
}
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
}
}
@ -335,7 +368,7 @@ class Monitor extends BeanModel {
let startTime = dayjs().valueOf();
let dnsMessage = "";
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type);
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.port, this.dns_resolve_type);
bean.ping = dayjs().valueOf() - startTime;
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") {
@ -372,22 +405,33 @@ class Monitor extends BeanModel {
bean.msg = dnsMessage;
bean.status = UP;
} else if (this.type === "push") { // Type: Push
const time = R.isoDateTime(dayjs.utc().subtract(this.interval, "second"));
log.debug("monitor", `[${this.name}] Checking monitor at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
const bufferTime = 1000; // 1s buffer to accommodate clock differences
let heartbeatCount = await R.count("heartbeat", " monitor_id = ? AND time > ? ", [
this.id,
time
]);
if (previousBeat) {
const msSinceLastBeat = dayjs.utc().valueOf() - dayjs.utc(previousBeat.time).valueOf();
log.debug("monitor", "heartbeatCount" + heartbeatCount + " " + time);
log.debug("monitor", `[${this.name}] msSinceLastBeat = ${msSinceLastBeat}`);
if (heartbeatCount <= 0) {
throw new Error("No heartbeat in the time window");
// If the previous beat was down or pending we use the regular
// beatInterval/retryInterval in the setTimeout further below
if (previousBeat.status !== (this.isUpsideDown() ? DOWN : UP) || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
throw new Error("No heartbeat in the time window");
} else {
let timeout = beatInterval * 1000 - msSinceLastBeat;
if (timeout < 0) {
timeout = bufferTime;
} else {
timeout += bufferTime;
}
// No need to insert successful heartbeat for push type, so end here
retries = 0;
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
this.heartbeatInterval = setTimeout(beat, timeout);
return;
}
} else {
// No need to insert successful heartbeat for push type, so end here
retries = 0;
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
return;
throw new Error("No heartbeat in the time window");
}
} else if (this.type === "steam") {
@ -405,10 +449,13 @@ class Monitor extends BeanModel {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
},
httpsAgent: new https.Agent({
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
}),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
maxCachedSessions: 0,
}),
maxRedirects: this.maxredirects,
validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes());
@ -429,6 +476,35 @@ class Monitor extends BeanModel {
} else {
throw new Error("Server not found on Steam");
}
} else if (this.type === "docker") {
log.debug(`[${this.name}] Prepare Options for Axios`);
const dockerHost = await R.load("docker_host", this.docker_host);
const options = {
url: `/containers/${this.docker_container}/json`,
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
},
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: ! this.getIgnoreTls(),
}),
};
if (dockerHost._dockerType === "socket") {
options.socketPath = dockerHost._dockerDaemon;
} else if (dockerHost._dockerType === "tcp") {
options.baseURL = dockerHost._dockerDaemon;
}
log.debug(`[${this.name}] Axios Request`);
let res = await axios.request(options);
if (res.data.State.Running) {
bean.status = UP;
bean.msg = "";
}
} else if (this.type === "mqtt") {
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
port: this.port,
@ -437,6 +513,22 @@ class Monitor extends BeanModel {
interval: this.interval,
});
bean.status = UP;
} else if (this.type === "sqlserver") {
let startTime = dayjs().valueOf();
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "postgres") {
let startTime = dayjs().valueOf();
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "radius") {
let startTime = dayjs().valueOf();
try {
@ -511,7 +603,7 @@ class Monitor extends BeanModel {
}
if (bean.status === UP) {
log.info("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
log.debug("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
} else if (bean.status === PENDING) {
if (this.retryInterval > 0) {
beatInterval = this.retryInterval;
@ -855,10 +947,19 @@ class Monitor extends BeanModel {
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
const notificationList = await Monitor.getNotificationList(this);
log.debug("monitor", "call sendCertNotificationByTargetDays");
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList);
let notifyDays = await setting("tlsExpiryNotifyDays");
if (notifyDays == null || !Array.isArray(notifyDays)) {
// Reset Default
setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
notifyDays = [ 7, 14, 21 ];
}
if (notifyDays != null && Array.isArray(notifyDays)) {
for (const day of notifyDays) {
log.debug("monitor", "call sendCertNotificationByTargetDays", day);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, day, notificationList);
}
}
}
}

@ -1,10 +1,109 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
const cheerio = require("cheerio");
const { UptimeKumaServer } = require("../uptime-kuma-server");
class StatusPage extends BeanModel {
/**
* Like this: { "test-uptime.kuma.pet": "default" }
* @type {{}}
*/
static domainMappingList = { };
/**
*
* @param {Response} response
* @param {string} indexHTML
* @param {string} slug
*/
static async handleStatusPageResponse(response, indexHTML, slug) {
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (statusPage) {
response.send(await StatusPage.renderHTML(indexHTML, statusPage));
} else {
response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
}
}
/**
* SSR for status pages
* @param {string} indexHTML
* @param {StatusPage} statusPage
*/
static async renderHTML(indexHTML, statusPage) {
const $ = cheerio.load(indexHTML);
const description155 = statusPage.description?.substring(0, 155);
$("title").text(statusPage.title);
$("meta[name=description]").attr("content", description155);
if (statusPage.icon) {
$("link[rel=icon]")
.attr("href", statusPage.icon)
.removeAttr("type");
$("link[rel=apple-touch-icon]").remove();
}
const head = $("head");
// OG Meta Tags
head.append(`<meta property="og:title" content="${statusPage.title}" />`);
head.append(`<meta property="og:description" content="${description155}" />`);
// Preload data
const json = JSON.stringify(await StatusPage.getStatusPageData(statusPage));
head.append(`
<script>
window.preloadData = ${json}
</script>
`);
// manifest.json
$("link[rel=manifest]").attr("href", `/api/status-page/${statusPage.slug}/manifest.json`);
return $.root().html();
}
/**
* Get all status page data in one call
* @param {StatusPage} statusPage
*/
static async getStatusPageData(statusPage) {
// Incident
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
statusPage.id,
]);
if (incident) {
incident = incident.toPublicJSON();
}
// Public Group List
const publicGroupList = [];
const showTags = !!statusPage.show_tags;
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags);
publicGroupList.push(monitorGroup);
}
// Response
return {
config: await statusPage.toPublicJSON(),
incident,
publicGroupList
};
}
/**
* Loads domain mapping from DB
* Return object like this: { "test-uptime.kuma.pet": "default" }

@ -0,0 +1,50 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setting } = require("../util-server");
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
class AlertNow extends NotificationProvider {
name = "AlertNow";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let textMsg = "";
let status = "open";
let eventType = "ERROR";
let eventId = new Date().toISOString().slice(0, 10).replace(/-/g, "");
if (heartbeatJSON && heartbeatJSON.status === UP) {
textMsg = `[${heartbeatJSON.name}] ✅ Application is back online`;
status = "close";
eventType = "INFO";
eventId += `_${heartbeatJSON.name.replace(/\s/g, "")}`;
} else if (heartbeatJSON && heartbeatJSON.status === DOWN) {
textMsg = `[${heartbeatJSON.name}] 🔴 Application went down`;
}
textMsg += ` - ${msg}`;
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorJSON) {
textMsg += ` >> ${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
}
const data = {
"summary": textMsg,
"status": status,
"event_type": eventType,
"event_id": eventId,
};
await axios.post(notification.alertNowWebhookURL, data);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = AlertNow;

@ -12,9 +12,7 @@ const { default: axios } = require("axios");
// bark is an APN bridge that sends notifications to Apple devices.
const barkNotificationGroup = "UptimeKuma";
const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
const barkNotificationSound = "telegraph";
const successMessage = "Successes!";
class Bark extends NotificationProvider {
@ -50,13 +48,23 @@ class Bark extends NotificationProvider {
* @param {string} postUrl URL to append parameters to
* @returns {string}
*/
appendAdditionalParameters(postUrl) {
// grouping all our notifications
postUrl += "?group=" + barkNotificationGroup;
appendAdditionalParameters(notification, postUrl) {
// set icon to uptime kuma icon, 11kb should be fine
postUrl += "&icon=" + barkNotificationAvatar;
// grouping all our notifications
if (notification.barkGroup != null) {
postUrl += "&group=" + notification.barkGroup;
} else {
// default name
postUrl += "&group=" + "UptimeKuma";
}
// picked a sound, this should follow system's mute status when arrival
postUrl += "&sound=" + barkNotificationSound;
if (notification.barkSound != null) {
postUrl += "&sound=" + notification.barkSound;
} else {
// default sound
postUrl += "&sound=" + "telegraph";
}
return postUrl;
}

@ -55,8 +55,8 @@ class Discord extends NotificationProvider {
value: monitorJSON["name"],
},
{
name: "Service URL / Address",
value: address,
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
},
{
name: "Time (UTC)",
@ -90,8 +90,8 @@ class Discord extends NotificationProvider {
value: monitorJSON["name"],
},
{
name: "Service URL",
value: address.startsWith("http") ? "[Visit Service](" + address + ")" : address,
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: monitorJSON["type"] === "push" ? "Heartbeat" : address.startsWith("http") ? "[Visit Service](" + address + ")" : address,
},
{
name: "Time (UTC)",
@ -99,7 +99,7 @@ class Discord extends NotificationProvider {
},
{
name: "Ping",
value: heartbeatJSON["ping"] + "ms",
value: heartbeatJSON["ping"] == null ? "N/A" : heartbeatJSON["ping"] + " ms",
},
],
}],

@ -0,0 +1,38 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const defaultNotificationService = "notify";
class HomeAssistant extends NotificationProvider {
name = "HomeAssistant";
async send(notification, message, monitor = null, heartbeat = null) {
const notificationService = notification?.notificationService || defaultNotificationService;
try {
await axios.post(
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
{
title: "Uptime Kuma",
message,
...(notificationService !== "persistent_notification" && { data: {
name: monitor?.name,
status: heartbeat?.status,
} }),
},
{
headers: {
Authorization: `Bearer ${notification.longLivedAccessToken}`,
"Content-Type": "application/json",
},
}
);
return "Sent Successfully.";
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = HomeAssistant;

@ -0,0 +1,43 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const qs = require("qs");
const { DOWN, UP } = require("../../src/util");
class LineNotify extends NotificationProvider {
name = "LineNotify";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let lineAPIUrl = "https://notify-api.line.me/api/notify";
let config = {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Bearer " + notification.lineNotifyAccessToken
}
};
if (heartbeatJSON == null) {
let testMessage = {
"message": msg,
};
await axios.post(lineAPIUrl, qs.stringify(testMessage), config);
} else if (heartbeatJSON["status"] === DOWN) {
let downMessage = {
"message": "\n[🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
};
await axios.post(lineAPIUrl, qs.stringify(downMessage), config);
} else if (heartbeatJSON["status"] === UP) {
let upMessage = {
"message": "\n[✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
};
await axios.post(lineAPIUrl, qs.stringify(upMessage), config);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = LineNotify;

@ -14,7 +14,7 @@ class LunaSea extends NotificationProvider {
if (heartbeatJSON == null) {
let testdata = {
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
"body": msg,
};
await axios.post(lunaseadevice, testdata);
return okMsg;

@ -0,0 +1,26 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Ntfy extends NotificationProvider {
name = "ntfy";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
await axios.post(`${notification.ntfyserverurl}`, {
"topic": notification.ntfytopic,
"message": msg,
"priority": notification.ntfyPriority || 4,
"title": "Uptime-Kuma",
});
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Ntfy;

@ -0,0 +1,113 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
let successMessage = "Sent Successfully.";
class PagerDuty extends NotificationProvider {
name = "PagerDuty";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
try {
if (heartbeatJSON == null) {
const title = "Uptime Kuma Alert";
const monitor = {
type: "ping",
url: "Uptime Kuma Test Button",
};
return this.postNotification(notification, title, msg, monitor);
}
if (heartbeatJSON.status === UP) {
const title = "Uptime Kuma Monitor ✅ Up";
const eventAction = notification.pagerdutyAutoResolve || null;
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, eventAction);
}
if (heartbeatJSON.status === DOWN) {
const title = "Uptime Kuma Monitor 🔴 Down";
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "trigger");
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
/**
* Check if result is successful, result code should be in range 2xx
* @param {Object} result Axios response object
* @throws {Error} The status code is not in range 2xx
*/
checkResult(result) {
if (result.status == null) {
throw new Error("PagerDuty notification failed with invalid response!");
}
if (result.status < 200 || result.status >= 300) {
throw new Error("PagerDuty notification failed with status code " + result.status);
}
}
/**
* Send the message
* @param {BeanModel} notification Message title
* @param {string} title Message title
* @param {string} body Message
* @param {Object} monitorInfo Monitor details (For Up/Down only)
* @param {?string} eventAction Action event for PagerDuty (trigger, acknowledge, resolve)
* @returns {string}
*/
async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") {
if (eventAction == null) {
return "No action required";
}
let monitorUrl;
if (monitorInfo.type === "port") {
monitorUrl = monitorInfo.hostname;
if (monitorInfo.port) {
monitorUrl += ":" + monitorInfo.port;
}
} else if (monitorInfo.hostname != null) {
monitorUrl = monitorInfo.hostname;
} else {
monitorUrl = monitorInfo.url;
}
const options = {
method: "POST",
url: notification.pagerdutyIntegrationUrl,
headers: { "Content-Type": "application/json" },
data: {
payload: {
summary: `[${title}] [${monitorInfo.name}] ${body}`,
severity: notification.pagerdutyPriority || "warning",
source: monitorUrl,
},
routing_key: notification.pagerdutyIntegrationKey,
event_action: eventAction,
dedup_key: "Uptime Kuma/" + monitorInfo.id,
}
};
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorInfo) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
}
let result = await axios.request(options);
this.checkResult(result);
if (result.statusText != null) {
return "PagerDuty notification succeed: " + result.statusText;
}
return successMessage;
}
}
module.exports = PagerDuty;

@ -1,38 +1,43 @@
const { R } = require("redbean-node");
const { log } = require("../src/util");
const Alerta = require("./notification-providers/alerta");
const AlertNow = require("./notification-providers/alertnow");
const AliyunSms = require("./notification-providers/aliyun-sms");
const Apprise = require("./notification-providers/apprise");
const Bark = require("./notification-providers/bark");
const ClickSendSMS = require("./notification-providers/clicksendsms");
const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
const Feishu = require("./notification-providers/feishu");
const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
const Gotify = require("./notification-providers/gotify");
const HomeAssistant = require("./notification-providers/home-assistant");
const Line = require("./notification-providers/line");
const LineNotify = require("./notification-providers/linenotify");
const LunaSea = require("./notification-providers/lunasea");
const Mattermost = require("./notification-providers/mattermost");
const Matrix = require("./notification-providers/matrix");
const Mattermost = require("./notification-providers/mattermost");
const Ntfy = require("./notification-providers/ntfy");
const Octopush = require("./notification-providers/octopush");
const OneBot = require("./notification-providers/onebot");
const PagerDuty = require("./notification-providers/pagerduty");
const PromoSMS = require("./notification-providers/promosms");
const ClickSendSMS = require("./notification-providers/clicksendsms");
const Pushbullet = require("./notification-providers/pushbullet");
const PushDeer = require("./notification-providers/pushdeer");
const Pushover = require("./notification-providers/pushover");
const Pushy = require("./notification-providers/pushy");
const TechulusPush = require("./notification-providers/techulus-push");
const RocketChat = require("./notification-providers/rocket-chat");
const SerwerSMS = require("./notification-providers/serwersms");
const Signal = require("./notification-providers/signal");
const Slack = require("./notification-providers/slack");
const SMTP = require("./notification-providers/smtp");
const Stackfield = require("./notification-providers/stackfield");
const Teams = require("./notification-providers/teams");
const TechulusPush = require("./notification-providers/techulus-push");
const Telegram = require("./notification-providers/telegram");
const Webhook = require("./notification-providers/webhook");
const Feishu = require("./notification-providers/feishu");
const AliyunSms = require("./notification-providers/aliyun-sms");
const DingDing = require("./notification-providers/dingding");
const Bark = require("./notification-providers/bark");
const { log } = require("../src/util");
const SerwerSMS = require("./notification-providers/serwersms");
const Stackfield = require("./notification-providers/stackfield");
const WeCom = require("./notification-providers/wecom");
const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
const Alerta = require("./notification-providers/alerta");
const OneBot = require("./notification-providers/onebot");
const PushDeer = require("./notification-providers/pushdeer");
class Notification {
@ -45,39 +50,44 @@ class Notification {
this.providerList = {};
const list = [
new Apprise(),
new Alerta(),
new AlertNow(),
new AliyunSms(),
new Apprise(),
new Bark(),
new ClickSendSMS(),
new DingDing(),
new Discord(),
new Teams(),
new Feishu(),
new GoogleChat(),
new Gorush(),
new Gotify(),
new HomeAssistant(),
new Line(),
new LineNotify(),
new LunaSea(),
new Feishu(),
new Mattermost(),
new Matrix(),
new Mattermost(),
new Ntfy(),
new Octopush(),
new OneBot(),
new PagerDuty(),
new PromoSMS(),
new ClickSendSMS(),
new Pushbullet(),
new PushDeer(),
new Pushover(),
new Pushy(),
new TechulusPush(),
new RocketChat(),
new SerwerSMS(),
new Signal(),
new Slack(),
new SMTP(),
new Stackfield(),
new Teams(),
new TechulusPush(),
new Telegram(),
new Webhook(),
new Bark(),
new SerwerSMS(),
new Stackfield(),
new WeCom(),
new GoogleChat(),
new Gorush(),
new Alerta(),
new OneBot(),
new PushDeer(),
];
for (let item of list) {

@ -1,5 +1,5 @@
let express = require("express");
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin } = require("../util-server");
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, send403 } = require("../util-server");
const { R } = require("redbean-node");
const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
@ -59,7 +59,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
let duration = 0;
let bean = R.dispense("heartbeat");
bean.time = R.isoDateTime(dayjs.utc());
bean.time = R.isoDateTimeMillis(dayjs.utc());
if (previousHeartbeat) {
isFirstBeat = false;
@ -67,6 +67,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
}
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
log.debug("router", "PreviousStatus: " + previousStatus);
log.debug("router", "Current Status: " + status);
@ -91,115 +92,13 @@ router.get("/api/push/:pushToken", async (request, response) => {
}
} catch (e) {
response.json({
response.status(404).json({
ok: false,
msg: e.message
});
}
});
// Status page config, incident, monitor list
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
// Get Status Page
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
response.statusCode = 404;
response.json({
msg: "Not Found"
});
return;
}
try {
// Incident
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
statusPage.id,
]);
if (incident) {
incident = incident.toPublicJSON();
}
// Public Group List
const publicGroupList = [];
const showTags = !!statusPage.show_tags;
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags);
publicGroupList.push(monitorGroup);
}
// Response
response.json({
config: await statusPage.toPublicJSON(),
incident,
publicGroupList
});
} catch (error) {
send403(response, error.message);
}
});
// Status Page Polling Data
// Can fetch only if published
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
allowDevAllOrigin(response);
try {
let heartbeatList = {};
let uptimeList = {};
let slug = request.params.slug;
let statusPageID = await StatusPage.slugToID(slug);
let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND public = 1
AND \`group\`.status_page_id = ?
`, [
statusPageID
]);
for (let monitorID of monitorIDList) {
let list = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT 50
`, [
monitorID,
]);
list = R.convertToBeans("heartbeat", list);
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
const type = 24;
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
}
response.json({
heartbeatList,
uptimeList
});
} catch (error) {
send403(response, error.message);
}
});
router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => {
allowAllOrigin(response);
@ -376,16 +275,4 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
}
});
/**
* Send a 403 response
* @param {Object} res Express response object
* @param {string} [msg=""] Message to send
*/
function send403(res, msg = "") {
res.status(403).json({
"status": "fail",
"msg": msg,
});
}
module.exports = router;

@ -0,0 +1,148 @@
let express = require("express");
const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const StatusPage = require("../model/status_page");
const { allowDevAllOrigin, send403 } = require("../util-server");
const { R } = require("redbean-node");
const Monitor = require("../model/monitor");
let router = express.Router();
let cache = apicache.middleware;
const server = UptimeKumaServer.getInstance();
router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
let slug = request.params.slug;
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
router.get("/status", cache("5 minutes"), async (request, response) => {
let slug = "default";
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
router.get("/status-page", cache("5 minutes"), async (request, response) => {
let slug = "default";
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
// Status page config, incident, monitor list
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
try {
// Get Status Page
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
return null;
}
let statusPageData = await StatusPage.getStatusPageData(statusPage);
if (!statusPageData) {
response.statusCode = 404;
response.json({
msg: "Not Found"
});
return;
}
// Response
response.json(statusPageData);
} catch (error) {
send403(response, error.message);
}
});
// Status Page Polling Data
// Can fetch only if published
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
allowDevAllOrigin(response);
try {
let heartbeatList = {};
let uptimeList = {};
let slug = request.params.slug;
let statusPageID = await StatusPage.slugToID(slug);
let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND public = 1
AND \`group\`.status_page_id = ?
`, [
statusPageID
]);
for (let monitorID of monitorIDList) {
let list = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT 50
`, [
monitorID,
]);
list = R.convertToBeans("heartbeat", list);
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
const type = 24;
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
}
response.json({
heartbeatList,
uptimeList
});
} catch (error) {
send403(response, error.message);
}
});
// Status page's manifest.json
router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
try {
// Get Status Page
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
response.statusCode = 404;
response.json({
msg: "Not Found"
});
return;
}
// Response
response.json({
"name": statusPage.title,
"start_url": "/status/" + statusPage.slug,
"display": "standalone",
"icons": [
{
"src": statusPage.icon,
"sizes": "128x128",
"type": "image/png"
}
]
});
} catch (error) {
send403(response, error.message);
}
});
module.exports = router;

@ -16,7 +16,7 @@ if (nodeVersion < requiredVersion) {
}
const args = require("args-parser")(process.argv);
const { sleep, log, getRandomInt, genSecret, debug, isDev } = require("../src/util");
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
const config = require("./config");
log.info("server", "Welcome to Uptime Kuma");
@ -35,6 +35,7 @@ const fs = require("fs");
log.info("server", "Importing 3rd-party libraries");
log.debug("server", "Importing express");
const express = require("express");
const expressStaticGzip = require("express-static-gzip");
log.debug("server", "Importing redbean-node");
const { R } = require("redbean-node");
log.debug("server", "Importing jsonwebtoken");
@ -117,13 +118,14 @@ if (config.demoMode) {
}
// Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client");
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList } = require("./client");
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
const TwoFA = require("./2fa");
const StatusPage = require("./model/status_page");
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
app.use(express.json());
@ -148,22 +150,6 @@ let jwtSecret = null;
*/
let needSetup = false;
/**
* Cache Index HTML
* @type {string}
*/
let indexHTML = "";
try {
indexHTML = fs.readFileSync("./dist/index.html").toString();
} catch (e) {
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
process.exit(1);
}
}
(async () => {
Database.init(args);
await initDatabase(testMode);
@ -179,13 +165,25 @@ try {
// Entry Page
app.get("/", async (request, response) => {
debug(`Request Domain: ${request.hostname}`);
let hostname = request.hostname;
if (await setting("trustProxy")) {
const proxy = request.headers["x-forwarded-host"];
if (proxy) {
hostname = proxy;
}
}
log.debug("entry", `Request Domain: ${hostname}`);
if (hostname in StatusPage.domainMappingList) {
log.debug("entry", "This is a status page domain");
let slug = StatusPage.domainMappingList[hostname];
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
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 {
response.redirect("/dashboard");
}
@ -214,7 +212,9 @@ try {
// With Basic Auth using the first user's username/password
app.get("/metrics", basicAuth, prometheusAPIMetrics());
app.use("/", express.static("dist"));
app.use("/", expressStaticGzip("dist", {
enableBrotli: true,
}));
// ./data/upload
app.use("/upload", express.static(Database.uploadDir));
@ -227,12 +227,16 @@ try {
const apiRouter = require("./routers/api-router");
app.use(apiRouter);
// Status Page Router
const statusPageRouter = require("./routers/status-page-router");
app.use(statusPageRouter);
// Universal Route Handler, must be at the end of all express routes.
app.get("*", async (_request, response) => {
if (_request.originalUrl.startsWith("/upload/")) {
response.status(404).send("File not found.");
} else {
response.send(indexHTML);
response.send(server.indexHTML);
}
});
@ -251,7 +255,9 @@ try {
// ***************************
socket.on("loginByToken", async (token, callback) => {
log.info("auth", `Login by token. IP=${getClientIp(socket)}`);
const clientIP = await server.getClientIP(socket);
log.info("auth", `Login by token. IP=${clientIP}`);
try {
let decoded = jwt.verify(token, jwtSecret);
@ -267,14 +273,14 @@ try {
afterLogin(socket, user);
log.debug("auth", "afterLogin ok");
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${getClientIp(socket)}`);
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`);
callback({
ok: true,
});
} else {
log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${getClientIp(socket)}`);
log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${clientIP}`);
callback({
ok: false,
@ -283,7 +289,7 @@ try {
}
} catch (error) {
log.error("auth", `Invalid token. IP=${getClientIp(socket)}`);
log.error("auth", `Invalid token. IP=${clientIP}`);
callback({
ok: false,
@ -294,7 +300,9 @@ try {
});
socket.on("login", async (data, callback) => {
log.info("auth", `Login by username + password. IP=${getClientIp(socket)}`);
const clientIP = await server.getClientIP(socket);
log.info("auth", `Login by username + password. IP=${clientIP}`);
// Checking
if (typeof callback !== "function") {
@ -307,7 +315,7 @@ try {
// Login Rate Limit
if (! await loginRateLimiter.pass(callback)) {
log.info("auth", `Too many failed requests for user ${data.username}. IP=${getClientIp(socket)}`);
log.info("auth", `Too many failed requests for user ${data.username}. IP=${clientIP}`);
return;
}
@ -317,7 +325,7 @@ try {
if (user.twofa_status === 0) {
afterLogin(socket, user);
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
callback({
ok: true,
@ -329,7 +337,7 @@ try {
if (user.twofa_status === 1 && !data.token) {
log.info("auth", `2FA token required for user ${data.username}. IP=${getClientIp(socket)}`);
log.info("auth", `2FA token required for user ${data.username}. IP=${clientIP}`);
callback({
tokenRequired: true,
@ -347,7 +355,7 @@ try {
socket.userID,
]);
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
callback({
ok: true,
@ -357,7 +365,7 @@ try {
});
} else {
log.warn("auth", `Invalid token provided for user ${data.username}. IP=${getClientIp(socket)}`);
log.warn("auth", `Invalid token provided for user ${data.username}. IP=${clientIP}`);
callback({
ok: false,
@ -367,7 +375,7 @@ try {
}
} else {
log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${getClientIp(socket)}`);
log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${clientIP}`);
callback({
ok: false,
@ -439,6 +447,8 @@ try {
});
socket.on("save2FA", async (currentPassword, callback) => {
const clientIP = await server.getClientIP(socket);
try {
if (! await twoFaRateLimiter.pass(callback)) {
return;
@ -451,7 +461,7 @@ try {
socket.userID,
]);
log.info("auth", `Saved 2FA token. IP=${getClientIp(socket)}`);
log.info("auth", `Saved 2FA token. IP=${clientIP}`);
callback({
ok: true,
@ -459,7 +469,7 @@ try {
});
} catch (error) {
log.error("auth", `Error changing 2FA token. IP=${getClientIp(socket)}`);
log.error("auth", `Error changing 2FA token. IP=${clientIP}`);
callback({
ok: false,
@ -469,6 +479,8 @@ try {
});
socket.on("disable2FA", async (currentPassword, callback) => {
const clientIP = await server.getClientIP(socket);
try {
if (! await twoFaRateLimiter.pass(callback)) {
return;
@ -478,7 +490,7 @@ try {
await doubleCheckPassword(socket, currentPassword);
await TwoFA.disable2FA(socket.userID);
log.info("auth", `Disabled 2FA token. IP=${getClientIp(socket)}`);
log.info("auth", `Disabled 2FA token. IP=${clientIP}`);
callback({
ok: true,
@ -486,7 +498,7 @@ try {
});
} catch (error) {
log.error("auth", `Error disabling 2FA token. IP=${getClientIp(socket)}`);
log.error("auth", `Error disabling 2FA token. IP=${clientIP}`);
callback({
ok: false,
@ -659,7 +671,7 @@ try {
bean.retryInterval = monitor.retryInterval;
bean.hostname = monitor.hostname;
bean.maxretries = monitor.maxretries;
bean.port = monitor.port;
bean.port = parseInt(monitor.port);
bean.keyword = monitor.keyword;
bean.ignoreTls = monitor.ignoreTls;
bean.expiryNotification = monitor.expiryNotification;
@ -669,11 +681,18 @@ try {
bean.dns_resolve_type = monitor.dns_resolve_type;
bean.dns_resolve_server = monitor.dns_resolve_server;
bean.pushToken = monitor.pushToken;
bean.docker_container = monitor.docker_container;
bean.docker_host = monitor.docker_host;
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
bean.mqttUsername = monitor.mqttUsername;
bean.mqttPassword = monitor.mqttPassword;
bean.mqttTopic = monitor.mqttTopic;
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
bean.databaseConnectionString = monitor.databaseConnectionString;
bean.databaseQuery = monitor.databaseQuery;
bean.authMethod = monitor.authMethod;
bean.authWorkstation = monitor.authWorkstation;
bean.authDomain = monitor.authDomain;
bean.radiusUsername = monitor.radiusUsername;
bean.radiusPassword = monitor.radiusPassword;
bean.radiusCalledStationId = monitor.radiusCalledStationId;
@ -1252,8 +1271,11 @@ try {
method: monitorListData[i].method || "GET",
body: monitorListData[i].body,
headers: monitorListData[i].headers,
authMethod: monitorListData[i].authMethod,
basic_auth_user: monitorListData[i].basic_auth_user,
basic_auth_pass: monitorListData[i].basic_auth_pass,
authWorkstation: monitorListData[i].authWorkstation,
authDomain: monitorListData[i].authDomain,
interval: monitorListData[i].interval,
retryInterval: retryInterval,
hostname: monitorListData[i].hostname,
@ -1424,6 +1446,7 @@ try {
cloudflaredSocketHandler(socket);
databaseSocketHandler(socket);
proxySocketHandler(socket);
dockerSocketHandler(socket);
log.debug("server", "added all socket handlers");
@ -1524,6 +1547,7 @@ async function afterLogin(socket, user) {
let monitorList = await server.sendMonitorList(socket);
sendNotificationList(socket);
sendProxyList(socket);
sendDockerHostList(socket);
await sleep(500);
@ -1678,10 +1702,6 @@ async function shutdownFunction(signal) {
await cloudflaredStop();
}
function getClientIp(socket) {
return socket.client.conn.remoteAddress.replace(/^.*:/, "");
}
/** Final function called before application exits */
function finalFunction() {
log.info("server", "Graceful shutdown successful!");

@ -0,0 +1,165 @@
const { R } = require("redbean-node");
const { log } = require("../src/util");
class Settings {
/**
* Example:
* {
* key1: {
* value: "value2",
* timestamp: 12345678
* },
* key2: {
* value: 2,
* timestamp: 12345678
* },
* }
* @type {{}}
*/
static cacheList = {
};
static cacheCleaner = null;
/**
* Retrieve value of setting based on key
* @param {string} key Key of setting to retrieve
* @returns {Promise<any>} Value
*/
static async get(key) {
// Start cache clear if not started yet
if (!Settings.cacheCleaner) {
Settings.cacheCleaner = setInterval(() => {
log.debug("settings", "Cache Cleaner is just started.");
for (key in Settings.cacheList) {
if (Date.now() - Settings.cacheList[key].timestamp > 60 * 1000) {
log.debug("settings", "Cache Cleaner deleted: " + key);
delete Settings.cacheList[key];
}
}
}, 60 * 1000);
}
// Query from cache
if (key in Settings.cacheList) {
const v = Settings.cacheList[key].value;
log.debug("settings", `Get Setting (cache): ${key}: ${v}`);
return v;
}
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
key,
]);
try {
const v = JSON.parse(value);
log.debug("settings", `Get Setting: ${key}: ${v}`);
Settings.cacheList[key] = {
value: v,
timestamp: Date.now()
};
return v;
} catch (e) {
return value;
}
}
/**
* Sets the specified setting to specified value
* @param {string} key Key of setting to set
* @param {any} value Value to set to
* @param {?string} type Type of setting
* @returns {Promise<void>}
*/
static async set(key, value, type = null) {
let bean = await R.findOne("setting", " `key` = ? ", [
key,
]);
if (!bean) {
bean = R.dispense("setting");
bean.key = key;
}
bean.type = type;
bean.value = JSON.stringify(value);
await R.store(bean);
Settings.deleteCache([ key ]);
}
/**
* Get settings based on type
* @param {string} type The type of setting
* @returns {Promise<Bean>}
*/
static async getSettings(type) {
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
type,
]);
let result = {};
for (let row of list) {
try {
result[row.key] = JSON.parse(row.value);
} catch (e) {
result[row.key] = row.value;
}
}
return result;
}
/**
* Set settings based on type
* @param {string} type Type of settings to set
* @param {Object} data Values of settings
* @returns {Promise<void>}
*/
static async setSettings(type, data) {
let keyList = Object.keys(data);
let promiseList = [];
for (let key of keyList) {
let bean = await R.findOne("setting", " `key` = ? ", [
key
]);
if (bean == null) {
bean = R.dispense("setting");
bean.type = type;
bean.key = key;
}
if (bean.type === type) {
bean.value = JSON.stringify(data[key]);
promiseList.push(R.store(bean));
}
}
await Promise.all(promiseList);
Settings.deleteCache(keyList);
}
/**
*
* @param {string[]} keyList
*/
static deleteCache(keyList) {
for (let key of keyList) {
delete Settings.cacheList[key];
}
}
}
module.exports = {
Settings,
};

@ -63,7 +63,10 @@ module.exports.cloudflaredSocketHandler = (socket) => {
socket.on(prefix + "stop", async (currentPassword, callback) => {
try {
checkLogin(socket);
await doubleCheckPassword(socket, currentPassword);
const disabledAuth = await setting("disableAuth");
if (!disabledAuth) {
await doubleCheckPassword(socket, currentPassword);
}
cloudflared.stop();
} catch (error) {
callback({

@ -0,0 +1,79 @@
const { sendDockerHostList } = require("../client");
const { checkLogin } = require("../util-server");
const { DockerHost } = require("../docker");
const { log } = require("../../src/util");
/**
* Handlers for docker hosts
* @param {Socket} socket Socket.io instance
*/
module.exports.dockerSocketHandler = (socket) => {
socket.on("addDockerHost", async (dockerHost, dockerHostID, callback) => {
try {
checkLogin(socket);
let dockerHostBean = await DockerHost.save(dockerHost, dockerHostID, socket.userID);
await sendDockerHostList(socket);
callback({
ok: true,
msg: "Saved",
id: dockerHostBean.id,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("deleteDockerHost", async (dockerHostID, callback) => {
try {
checkLogin(socket);
await DockerHost.delete(dockerHostID, socket.userID);
await sendDockerHostList(socket);
callback({
ok: true,
msg: "Deleted",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("testDockerHost", async (dockerHost, callback) => {
try {
checkLogin(socket);
let amount = await DockerHost.testDockerHost(dockerHost);
let msg;
if (amount > 1) {
msg = "Connected Successfully. Amount of containers: " + amount;
} else {
msg = "Connected Successfully, but there are no containers?";
}
callback({
ok: true,
msg,
});
} catch (e) {
log.error("docker", e);
callback({
ok: false,
msg: e.message,
});
}
});
};

@ -202,6 +202,11 @@ module.exports.statusPageSocketHandler = (socket) => {
relationBean.weight = monitorOrder++;
relationBean.group_id = groupBean.id;
relationBean.monitor_id = monitor.id;
if (monitor.sendUrl !== undefined) {
relationBean.send_url = monitor.sendUrl;
}
await R.store(relationBean);
}

@ -7,6 +7,8 @@ const { R } = require("redbean-node");
const { log } = require("../src/util");
const Database = require("./database");
const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings");
/**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
@ -29,6 +31,12 @@ class UptimeKumaServer {
httpServer = undefined;
io = undefined;
/**
* Cache Index HTML
* @type {string}
*/
indexHTML = "";
static getInstance(args) {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
@ -43,7 +51,6 @@ class UptimeKumaServer {
log.info("server", "Creating express and socket.io instance");
this.app = express();
if (sslKey && sslCert) {
log.info("server", "Server Type: HTTPS");
this.httpServer = https.createServer({
@ -55,6 +62,18 @@ class UptimeKumaServer {
this.httpServer = http.createServer(this.app);
}
try {
this.indexHTML = fs.readFileSync("./dist/index.html").toString();
} catch (e) {
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
process.exit(1);
}
}
CacheableDnsHttpAgent.registerGlobalAgent();
this.io = new Server(this.httpServer);
}
@ -110,6 +129,22 @@ class UptimeKumaServer {
errorLogStream.end();
}
async getClientIP(socket) {
let clientIP = socket.client.conn.remoteAddress;
if (clientIP === undefined) {
clientIP = "";
}
if (await Settings.get("trustProxy")) {
return socket.client.conn.request.headers["x-forwarded-for"]
|| socket.client.conn.request.headers["x-real-ip"]
|| clientIP.replace(/^.*:/, "");
} else {
return clientIP.replace(/^.*:/, "");
}
}
}
module.exports = {

@ -10,6 +10,11 @@ const chardet = require("chardet");
const mqtt = require("mqtt");
const chroma = require("chroma-js");
const { badgeConstants } = require("./config");
const mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
const { NtlmClient } = require("axios-ntlm");
const { Settings } = require("./settings");
const radiusClient = require("node-radius-client");
const {
dictionaries: {
@ -178,16 +183,40 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
});
};
/**
* Use NTLM Auth for a http request.
* @param {Object} options The http request options
* @param {Object} ntlmOptions The auth options
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.httpNtlm = function (options, ntlmOptions) {
return new Promise((resolve, reject) => {
let client = NtlmClient(ntlmOptions);
client(options)
.then((resp) => {
resolve(resp);
})
.catch((err) => {
reject(err);
});
});
};
/**
* Resolves a given record using the specified DNS server
* @param {string} hostname The hostname of the record to lookup
* @param {string} resolverServer The DNS server to use
* @param {string} resolverPort Port the DNS server is listening on
* @param {string} rrtype The type of record to request
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.dnsResolve = function (hostname, resolverServer, rrtype) {
exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
const resolver = new Resolver();
resolver.setServers([ resolverServer ]);
// Remove brackets from IPv6 addresses so we can re-add them to
// prevent issues with ::1:5300 (::1 port 5300)
resolverServer = resolverServer.replace("[", "").replace("]", "");
resolver.setServers([ `[${resolverServer}]:${resolverPort}` ]);
return new Promise((resolve, reject) => {
if (rrtype === "PTR") {
resolver.reverse(hostname, (err, records) => {
@ -209,6 +238,59 @@ exports.dnsResolve = function (hostname, resolverServer, rrtype) {
});
};
/**
* Run a query on SQL Server
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.mssqlQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
mssql.connect(connectionString).then(pool => {
return pool.request()
.query(query);
}).then(result => {
resolve(result);
}).catch(err => {
reject(err);
}).finally(() => {
mssql.close();
});
});
};
/**
* Run a query on Postgres
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.postgresQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
const config = postgresConParse(connectionString);
if (config.password === "") {
// See https://github.com/brianc/node-postgres/issues/1927
return reject(new Error("Password is undefined."));
}
const client = new Client({ connectionString });
client.connect();
return client.query(query)
.then(res => {
resolve(res);
})
.catch(err => {
reject(err);
})
.finally(() => {
client.end();
});
});
};
exports.radius = function (
hostname,
username,
@ -237,19 +319,10 @@ exports.radius = function (
* Retrieve value of setting based on key
* @param {string} key Key of setting to retrieve
* @returns {Promise<any>} Value
* @deprecated Use await Settings.get(key)
*/
exports.setting = async function (key) {
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
key,
]);
try {
const v = JSON.parse(value);
log.debug("util", `Get Setting: ${key}: ${v}`);
return v;
} catch (e) {
return value;
}
return await Settings.get(key);
};
/**
@ -260,70 +333,26 @@ exports.setting = async function (key) {
* @returns {Promise<void>}
*/
exports.setSetting = async function (key, value, type = null) {
let bean = await R.findOne("setting", " `key` = ? ", [
key,
]);
if (!bean) {
bean = R.dispense("setting");
bean.key = key;
}
bean.type = type;
bean.value = JSON.stringify(value);
await R.store(bean);
await Settings.set(key, value, type);
};
/**
* Get settings based on type
* @param {?string} type The type of setting
* @param {string} type The type of setting
* @returns {Promise<Bean>}
*/
exports.getSettings = async function (type) {
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
type,
]);
let result = {};
for (let row of list) {
try {
result[row.key] = JSON.parse(row.value);
} catch (e) {
result[row.key] = row.value;
}
}
return result;
return await Settings.getSettings(type);
};
/**
* Set settings based on type
* @param {?string} type Type of settings to set
* @param {string} type Type of settings to set
* @param {Object} data Values of settings
* @returns {Promise<void>}
*/
exports.setSettings = async function (type, data) {
let keyList = Object.keys(data);
let promiseList = [];
for (let key of keyList) {
let bean = await R.findOne("setting", " `key` = ? ", [
key
]);
if (bean == null) {
bean = R.dispense("setting");
bean.type = type;
bean.key = key;
}
if (bean.type === type) {
bean.value = JSON.stringify(data[key]);
promiseList.push(R.store(bean));
}
}
await Promise.all(promiseList);
await Settings.setSettings(type, data);
};
// ssl-checker by @dyaa
@ -416,7 +445,7 @@ exports.checkCertificate = function (res) {
/**
* Check if the provided status code is within the accepted ranges
* @param {string} status The status code to check
* @param {number} status The status code to check
* @param {string[]} acceptedCodes An array of accepted status codes
* @returns {boolean} True if status code within range, false otherwise
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string
@ -584,3 +613,15 @@ exports.percentageToColor = (percentage, maxHue = 90, minHue = 10) => {
exports.filterAndJoin = (parts, connector = "") => {
return parts.filter((part) => !!part && part !== "").join(connector);
};
/**
* Send a 403 response
* @param {Object} res Express response object
* @param {string} [msg=""] Message to send
*/
module.exports.send403 = (res, msg = "") => {
res.status(403).json({
"status": "fail",
"msg": msg,
});
};

@ -34,6 +34,25 @@ textarea.form-control {
}
}
// optgroup
optgroup {
color: #b1b1b1;
option {
color: #212529;
}
}
.dark {
optgroup {
color: #535864;
option {
color: $dark-font-color;
}
}
}
// Scrollbar
::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 20px;
@ -363,6 +382,12 @@ textarea.form-control {
overflow-y: auto;
height: calc(100% - 65px);
}
@media (max-width: 770px) {
&.scrollbar {
height: calc(100% - 40px);
}
}
.item {
display: block;
@ -473,6 +498,14 @@ textarea.form-control {
outline: none !important;
}
h5.settings-subheading::after {
content: "";
display: block;
width: 50%;
padding-top: 8px;
border-bottom: 1px solid $dark-border-color;
}
// Localization
@import "localization.scss";

@ -0,0 +1,86 @@
<template>
<div class="input-group mb-3">
<input
ref="input"
v-model="model"
class="form-control"
:type="type"
:placeholder="placeholder"
:disabled="!enabled"
>
<a class="btn btn-outline-primary" @click="action()">
<font-awesome-icon :icon="icon" />
</a>
</div>
</template>
<script>
/**
* Generic input field with a customizable action on the right.
* Action is passed in as a function.
*/
export default {
props: {
/**
* The value of the input field.
*/
modelValue: {
type: String,
default: ""
},
/**
* Whether the input field is enabled / disabled.
*/
enabled: {
type: Boolean,
default: true
},
/**
* Placeholder text for the input field.
*/
placeholder: {
type: String,
default: ""
},
/**
* The icon displayed in the right button of the input field.
* Accepts a Font Awesome icon string identifier.
* @example "plus"
*/
icon: {
type: String,
required: true,
},
/**
* The input type of the input field.
* @example "email"
*/
type: {
type: String,
default: "text",
},
/**
* The action to be performed when the button is clicked.
* Action is passed in as a function.
*/
action: {
type: Function,
default: () => {},
}
},
emits: [ "update:modelValue" ],
computed: {
/**
* Send value update to parent on change.
*/
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
}
},
};
</script>

@ -25,10 +25,12 @@ export default {
CertificateInfoRow,
},
props: {
/** Object representing certificate */
certInfo: {
type: Object,
required: true,
},
/** Is the TLS certificate valid? */
valid: {
type: Boolean,
required: true,

@ -56,12 +56,19 @@ export default {
Datetime,
},
props: {
/** Object representing certificate */
cert: {
type: Object,
required: true,
},
},
methods: {
/**
* Format the subject of the certificate
* @param {Object} subject Object representing the certificates
* subject
* @returns {string}
*/
formatSubject(subject) {
if (subject.O && subject.CN && subject.C) {
return `${subject.CN} - ${subject.O} (${subject.C})`;

@ -29,14 +29,17 @@ import { Modal } from "bootstrap";
export default {
props: {
/** Style of button */
btnStyle: {
type: String,
default: "btn-primary",
},
/** Text to use as yes */
yesText: {
type: String,
default: "Yes", // TODO: No idea what to translate this
},
/** Text to use as no */
noText: {
type: String,
default: "No",
@ -50,9 +53,13 @@ export default {
this.modal = new Modal(this.$refs.modal);
},
methods: {
/** Show the confirm dialog */
show() {
this.modal.show();
},
/**
* @emits string "yes" Notify the parent when Yes is pressed
*/
yes() {
this.$emit("yes");
},

@ -25,33 +25,41 @@ let timeout;
export default {
props: {
/** ID of this input */
id: {
type: String,
default: ""
},
/** Type of input */
type: {
type: String,
default: "text"
},
/** The value of the input */
modelValue: {
type: String,
default: ""
},
/** A placeholder to use */
placeholder: {
type: String,
default: ""
},
/** Should the field auto complete */
autocomplete: {
type: String,
default: undefined,
},
/** Is the input required? */
required: {
type: Boolean
},
/** Should the input be read only? */
readonly: {
type: String,
default: undefined,
},
/** Is the input disabled? */
disabled: {
type: String,
default: undefined,
@ -79,14 +87,21 @@ export default {
},
methods: {
/** Show the input */
showInput() {
this.visibility = "text";
},
/** Hide the input */
hideInput() {
this.visibility = "password";
},
/**
* Copy the provided text to the users clipboard
* @param {string} textToCopy
* @returns {Promise<void>}
*/
copyToClipboard(textToCopy) {
this.icon = "check";

@ -10,6 +10,7 @@ import { sleep } from "../util.ts";
export default {
props: {
/** Value to count */
value: {
type: [ String, Number ],
default: 0,
@ -18,6 +19,7 @@ export default {
type: Number,
default: 0.3,
},
/** Unit of the value */
unit: {
type: String,
default: "ms",
@ -43,9 +45,7 @@ export default {
let frames = 12;
let step = Math.floor(diff / frames);
if (isNaN(step) || ! this.isNum || (diff > 0 && step < 1) || (diff < 0 && step > 1) || diff === 0) {
// Lazy to NOT this condition, hahaha.
} else {
if (! (isNaN(step) || ! this.isNum || (diff > 0 && step < 1) || (diff < 0 && step > 1) || diff === 0)) {
for (let i = 1; i < frames; i++) {
this.output += step;
await sleep(15);

@ -13,10 +13,12 @@ dayjs.extend(relativeTime);
export default {
props: {
/** Value of date time */
value: {
type: String,
default: null,
},
/** Should only the date be displayed? */
dateOnly: {
type: Boolean,
default: false,

@ -0,0 +1,177 @@
<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 Docker Host") }}
</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="docker-name" class="form-label">{{ $t("Friendly Name") }}</label>
<input id="docker-name" v-model="dockerHost.name" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="docker-type" class="form-label">{{ $t("Connection Type") }}</label>
<select id="docker-type" v-model="dockerHost.dockerType" class="form-select">
<option v-for="type in connectionTypes" :key="type" :value="type">{{ $t(type) }}</option>
</select>
</div>
<div class="mb-3">
<label for="docker-daemon" class="form-label">{{ $t("Docker Daemon") }}</label>
<input id="docker-daemon" v-model="dockerHost.dockerDaemon" type="text" class="form-control" required>
<div class="form-text">
{{ $t("Examples") }}:
<ul>
<li>/var/run/docker.sock</li>
<li>tcp://localhost:2375</li>
</ul>
</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="button" class="btn btn-warning" :disabled="processing" @click="test">
{{ $t("Test") }}
</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="deleteDockerHost">
{{ $t("deleteDockerHostMsg") }}
</Confirm>
</template>
<script lang="ts">
import { Modal } from "bootstrap";
import Confirm from "./Confirm.vue";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
Confirm,
},
props: {},
emits: [ "added" ],
data() {
return {
model: null,
processing: false,
id: null,
connectionTypes: [ "socket", "tcp" ],
dockerHost: {
name: "",
dockerDaemon: "",
dockerType: "",
// Do not set default value here, please scroll to show()
}
};
},
mounted() {
this.modal = new Modal(this.$refs.modal);
},
methods: {
deleteConfirm() {
this.modal.hide();
this.$refs.confirmDelete.show();
},
show(dockerHostID) {
if (dockerHostID) {
let found = false;
this.id = dockerHostID;
for (let n of this.$root.dockerHostList) {
if (n.id === dockerHostID) {
this.dockerHost = n;
found = true;
break;
}
}
if (!found) {
toast.error("Docker Host not found!");
}
} else {
this.id = null;
this.dockerHost = {
name: "",
dockerType: "socket",
dockerDaemon: "/var/run/docker.sock",
};
}
this.modal.show();
},
submit() {
this.processing = true;
this.$root.getSocket().emit("addDockerHost", this.dockerHost, 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);
}
}
});
},
test() {
this.processing = true;
this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => {
this.$root.toastRes(res);
this.processing = false;
});
},
deleteDockerHost() {
this.processing = true;
this.$root.getSocket().emit("deleteDockerHost", 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>

@ -17,14 +17,17 @@
export default {
props: {
/** Size of the heartbeat bar */
size: {
type: String,
default: "big",
},
/** ID of the monitor */
monitorId: {
type: Number,
required: true,
},
/** Array of the monitors heartbeats */
heartbeatList: {
type: Array,
default: null,
@ -160,12 +163,19 @@ export default {
this.resize();
},
methods: {
/** Resize the heartbeat bar */
resize() {
if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
}
},
/**
* Get the title of the beat.
* Used as the hover tooltip on the heartbeat bar.
* @param {Object} beat Beat to get title from
* @returns {string}
*/
getBeatTitle(beat) {
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
},

@ -24,25 +24,31 @@
<script>
export default {
props: {
/** The value of the input */
modelValue: {
type: String,
default: ""
},
/** A placeholder to use */
placeholder: {
type: String,
default: ""
},
/** Maximum length of the input */
maxlength: {
type: Number,
default: 255
},
/** Should the field auto complete */
autocomplete: {
type: String,
default: undefined,
},
/** Is the input required? */
required: {
type: Boolean
},
/** Should the input be read only? */
readonly: {
type: String,
default: undefined,
@ -68,9 +74,11 @@ export default {
},
methods: {
/** Show users input in plain text */
showInput() {
this.visibility = "text";
},
/** Censor users input */
hideInput() {
this.visibility = "password";
},

@ -55,6 +55,7 @@ export default {
};
},
methods: {
/** Submit the user details and attempt to log in */
submit() {
this.processing = true;

@ -58,6 +58,7 @@ export default {
Tag,
},
props: {
/** Should the scrollbar be shown */
scrollbar: {
type: Boolean,
},
@ -69,10 +70,22 @@ export default {
};
},
computed: {
/**
* Improve the sticky appearance of the list by increasing its
* height as user scrolls down.
* Not used on mobile.
*/
boxStyle() {
return {
height: `calc(100vh - 160px + ${this.windowTop}px)`,
};
if (window.innerWidth > 550) {
return {
height: `calc(100vh - 160px + ${this.windowTop}px)`,
};
} else {
return {
height: "calc(100vh - 160px)",
};
}
},
sortedMonitorList() {
@ -124,6 +137,7 @@ export default {
window.removeEventListener("scroll", this.onScroll);
},
methods: {
/** Handle user scroll */
onScroll() {
if (window.top.scrollY <= 133) {
this.windowTop = window.top.scrollY;
@ -131,9 +145,15 @@ export default {
this.windowTop = 133;
}
},
/**
* Get URL of monitor
* @param {number} id ID of monitor
* @returns {string} Relative URL of monitor
*/
monitorURL(id) {
return getMonitorRelativeURL(id);
},
/** Clear the search bar */
clearSearchText() {
this.searchText = "";
}

@ -125,11 +125,16 @@ export default {
},
methods: {
/** Show dialog to confirm deletion */
deleteConfirm() {
this.modal.hide();
this.$refs.confirmDelete.show();
},
/**
* Show settings for specified notification
* @param {number} notificationID ID of notification to show
*/
show(notificationID) {
if (notificationID) {
this.id = notificationID;
@ -152,6 +157,7 @@ export default {
this.modal.show();
},
/** Submit the form to the server */
submit() {
this.processing = true;
this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => {
@ -170,6 +176,7 @@ export default {
});
},
/** Test the notification endpoint */
test() {
this.processing = true;
this.$root.getSocket().emit("testNotification", this.notification, (res) => {
@ -178,6 +185,7 @@ export default {
});
},
/** Delete the notification endpoint */
deleteNotification() {
this.processing = true;
this.$root.getSocket().emit("deleteNotification", this.id, (res) => {
@ -190,6 +198,7 @@ export default {
});
},
/**
* Get a unique default name for the notification
* @param {keyof NotificationFormList} notificationKey
* @return {string}
*/

@ -35,6 +35,7 @@ Chart.register(LineController, BarController, LineElement, PointElement, TimeSca
export default {
components: { LineChart },
props: {
/** ID of monitor */
monitorId: {
type: Number,
required: true,

@ -130,11 +130,16 @@ export default {
},
methods: {
/** Show dialog to confirm deletion */
deleteConfirm() {
this.modal.hide();
this.$refs.confirmDelete.show();
},
/**
* Show settings for specified proxy
* @param {number} proxyID ID of proxy to show
*/
show(proxyID) {
if (proxyID) {
this.id = proxyID;
@ -163,6 +168,7 @@ export default {
this.modal.show();
},
/** Submit form data for saving */
submit() {
this.processing = true;
this.$root.getSocket().emit("addProxy", this.proxy, this.id, (res) => {
@ -180,6 +186,7 @@ export default {
});
},
/** Delete this proxy */
deleteProxy() {
this.processing = true;
this.$root.getSocket().emit("deleteProxy", this.id, (res) => {

@ -39,9 +39,29 @@
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
<Uptime :monitor="monitor.element" type="24" :pill="true" />
{{ monitor.element.name }}
<a
v-if="showLink(monitor)"
:href="monitor.element.url"
class="item-name"
target="_blank"
>
{{ monitor.element.name }}
</a>
<p v-else class="item-name"> {{ monitor.element.name }} </p>
<span
v-if="showLink(monitor, true)"
title="Toggle Clickable Link"
>
<font-awesome-icon
v-if="editMode"
:class="{'link-active': monitor.element.sendUrl, 'btn-link': true}"
icon="link" class="action me-3"
@click="toggleLink(group.index, monitor.index)"
/>
</span>
</div>
<div v-if="showTag" class="tags">
<div v-if="showTags" class="tags">
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
@ -72,10 +92,12 @@ export default {
Tag,
},
props: {
/** Are we in edit mode? */
editMode: {
type: Boolean,
required: true,
},
/** Should tags be shown? */
showTags: {
type: Boolean,
}
@ -94,13 +116,50 @@ export default {
},
methods: {
/**
* Remove the specified group
* @param {number} index Index of group to remove
*/
removeGroup(index) {
this.$root.publicGroupList.splice(index, 1);
},
/**
* Remove a monitor from a group
* @param {number} groupIndex Index of group to remove monitor
* from
* @param {number} index Index of monitor to remove
*/
removeMonitor(groupIndex, index) {
this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
},
/**
* Toggle the value of sendUrl
* @param {number} groupIndex Index of group monitor is member of
* @param {number} index Index of monitor within group
*/
toggleLink(groupIndex, index) {
this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl = !this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl;
},
/**
* Should a link to the monitor be shown?
* Attempts to guess if a link should be shown based upon if
* sendUrl is set and if the URL is default or not.
* @param {Object} monitor Monitor to check
* @param {boolean} [ignoreSendUrl=false] Should the presence of the sendUrl
* property be ignored. This will only work in edit mode.
* @returns {boolean}
*/
showLink(monitor, ignoreSendUrl = false) {
// We must check if there are any elements in monitorList to
// prevent undefined errors if it hasn't been loaded yet
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword";
}
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
},
}
};
</script>
@ -119,6 +178,22 @@ export default {
min-height: 46px;
}
.item-name {
padding-left: 5px;
padding-right: 5px;
margin: 0;
display: inline-block;
}
.btn-link {
color: #bbbbbb;
margin-left: 5px;
}
.link-active {
color: $primary;
}
.flip-list-move {
transition: transform 0.5s;
}

@ -5,6 +5,7 @@
<script>
export default {
props: {
/** Current status of monitor */
status: {
type: Number,
default: 0,

@ -20,14 +20,20 @@
<script>
export default {
props: {
/** Object representing tag */
item: {
type: Object,
required: true,
},
/** Function to remove tag */
remove: {
type: Function,
default: null,
},
/**
* Size of tag
* @values normal, small
*/
size: {
type: String,
default: "normal",

@ -139,6 +139,7 @@ export default {
VueMultiselect,
},
props: {
/** Array of tags to be pre-selected */
preSelectedTags: {
type: Array,
default: () => [],
@ -241,9 +242,11 @@ export default {
this.getExistingTags();
},
methods: {
/** Show the add tag dialog */
showAddDialog() {
this.modal.show();
},
/** Get all existing tags */
getExistingTags() {
this.$root.getSocket().emit("getTags", (res) => {
if (res.ok) {
@ -253,6 +256,10 @@ export default {
}
});
},
/**
* Delete the specified tag
* @param {Object} tag Object representing tag to delete
*/
deleteTag(item) {
if (item.new) {
// Undo Adding a new Tag
@ -262,6 +269,13 @@ export default {
this.deleteTags.push(item);
}
},
/**
* Get colour of text inside the tag
* @param {Object} option The tag that needs to be displayed.
* Defaults to "white" unless the tag has no color, which will
* then return the body color (based on application theme)
* @returns string
*/
textColor(option) {
if (option.color) {
return "white";
@ -269,6 +283,7 @@ export default {
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
}
},
/** Add a draft tag */
addDraftTag() {
console.log("Adding Draft Tag: ", this.newDraftTag);
if (this.newDraftTag.select != null) {
@ -296,6 +311,7 @@ export default {
}
this.clearDraftTag();
},
/** Remove a draft tag */
clearDraftTag() {
this.newDraftTag = {
name: null,
@ -307,26 +323,51 @@ export default {
};
this.modal.hide();
},
/**
* Add a tag asynchronously
* @param {Object} newTag Object representing new tag to add
* @returns {Promise<void>}
*/
addTagAsync(newTag) {
return new Promise((resolve) => {
this.$root.getSocket().emit("addTag", newTag, resolve);
});
},
/**
* Add a tag to a monitor asynchronously
* @param {number} tagId ID of tag to add
* @param {number} monitorId ID of monitor to add tag to
* @param {string} value Value of tag
* @returns {Promise<void>}
*/
addMonitorTagAsync(tagId, monitorId, value) {
return new Promise((resolve) => {
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
});
},
/**
* Delete a tag from a monitor asynchronously
* @param {number} tagId ID of tag to remove
* @param {number} monitorId ID of monitor to remove tag from
* @param {string} value Value of tag
* @returns {Promise<void>}
*/
deleteMonitorTagAsync(tagId, monitorId, value) {
return new Promise((resolve) => {
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
});
},
/** Handle pressing Enter key when inside the modal */
onEnter() {
if (!this.validateDraftTag.invalid) {
this.addDraftTag();
}
},
/**
* Submit the form data
* @param {number} monitorId ID of monitor this change affects
* @returns {void}
*/
async submit(monitorId) {
console.log(`Submitting tag changes for monitor ${monitorId}...`);
this.processing = true;

@ -29,10 +29,12 @@
<script>
export default {
props: {
/** Heading of the section */
heading: {
type: String,
default: "",
},
/** Should the section be open by default? */
defaultOpen: {
type: Boolean,
default: false,

@ -100,18 +100,22 @@ export default {
this.getStatus();
},
methods: {
/** Show the dialog */
show() {
this.modal.show();
},
/** Show dialog to confirm enabling 2FA */
confirmEnableTwoFA() {
this.$refs.confirmEnableTwoFA.show();
},
/** Show dialog to confirm disabling 2FA */
confirmDisableTwoFA() {
this.$refs.confirmDisableTwoFA.show();
},
/** Prepare 2FA configuration */
prepare2FA() {
this.processing = true;
@ -126,6 +130,7 @@ export default {
});
},
/** Save the current 2FA configuration */
save2FA() {
this.processing = true;
@ -143,6 +148,7 @@ export default {
});
},
/** Disable 2FA for this user */
disable2FA() {
this.processing = true;
@ -160,6 +166,7 @@ export default {
});
},
/** Verify the token generated by the user */
verifyToken() {
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
if (res.ok) {
@ -170,6 +177,7 @@ export default {
});
},
/** Get current status of 2FA */
getStatus() {
this.$root.getSocket().emit("twoFAStatus", (res) => {
if (res.ok) {

@ -5,14 +5,17 @@
<script>
export default {
props: {
/** Monitor this represents */
monitor: {
type: Object,
default: null,
},
/** Type of monitor */
type: {
type: String,
default: null,
},
/** Is this a pill? */
pill: {
type: Boolean,
default: false,

@ -0,0 +1,13 @@
<template>
<div class="mb-3">
<label for="alertnow-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="alertnow-webhook-url" v-model="$parent.notification.alertNowWebhookURL" 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://service.opsnow.com/docs/alertnow/en/user-guide-alertnow-en.html#standard" target="_blank">{{ $t("here") }}</a>
</i18n-t>
</div>
</div>
</template>

@ -2,9 +2,6 @@
<div class="mb-3">
<label for="Bark Endpoint" class="form-label">{{ $t("Bark Endpoint") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="Bark Endpoint" v-model="$parent.notification.barkEndpoint" 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="div" keypath="wayToGetTeamsURL" class="form-text">
<a
href="https://github.com/Finb/Bark"
@ -12,4 +9,45 @@
>{{ $t("here") }}</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="Bark Group" class="form-label">{{ $t("Bark Group") }}</label>
<input id="Bark Group" v-model="$parent.notification.barkGroup" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="Bark Sound" class="form-label">{{ $t("Bark Sound") }}</label>
<select id="Bark Sound" v-model="$parent.notification.barkSound" class="form-select" required>
<option value="alarm">alarm</option>
<option value="anticipate">anticipate</option>
<option value="bell">bell</option>
<option value="birdsong">birdsong</option>
<option value="bloom">bloom</option>
<option value="calypso">calypso</option>
<option value="chime">chime</option>
<option value="choo">choo</option>
<option value="descent">descent</option>
<option value="electronic">electronic</option>
<option value="fanfare">fanfare</option>
<option value="glass">glass</option>
<option value="gotosleep">gotosleep</option>
<option value="healthnotification">healthnotification</option>
<option value="horn">horn</option>
<option value="ladder">ladder</option>
<option value="mailsent">mailsent</option>
<option value="minuet">minuet</option>
<option value="multiwayinvitation">multiwayinvitation</option>
<option value="newmail">newmail</option>
<option value="newsflash">newsflash</option>
<option value="noir">noir</option>
<option value="paymentsuccess">paymentsuccess</option>
<option value="shake">shake</option>
<option value="sherwoodforest">sherwoodforest</option>
<option value="silence">silence</option>
<option value="spell">spell</option>
<option value="suspense">suspense</option>
<option value="telegraph">telegraph</option>
<option value="tiptoes">tiptoes</option>
<option value="typewriters">typewriters</option>
<option value="update">update</option>
</select>
</div>
</template>

@ -0,0 +1,40 @@
<template>
<div class="mb-3">
<label for="homeAssistantUrl" class="form-label">{{ $t("Home Assistant URL") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="homeAssistantUrl" v-model="$parent.notification.homeAssistantUrl" type="url" class="form-control" required>
</div>
<div class="mb-3">
<label for="longLivedAccessToken" class="form-label">{{ $t("Long-Lived Access Token") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="longLivedAccessToken" v-model="$parent.notification.longLivedAccessToken" type="text" class="form-control" required>
<div class="form-text">
<p>{{ $t("Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ") }}</p>
</div>
</div>
<div class="mb-3">
<label for="notificationService" class="form-label">{{ $t("Notification Service") }}</label>
<input id="notificationService" v-model="$parent.notification.notificationService" type="text" :placeholder="$t('default: notify all devices')" class="form-control">
<div class="form-text">
<p>{{ $t("A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.") }}</p>
<p>{{ $t("Automations can optionally be triggered in Home Assistant:") }}</p>
<p>
{{ $t("Trigger type:") }} <code>Event</code><br />
{{ $t("Event type:") }} <code>call_service</code><br />
{{ $t("Event data:") }}
</p>
<pre>domain: notify
service: mobile_app_my_phone # change to your device name
service_data:
title: Uptime Kuma
data:
status: 0 # 0=down 1=up
# name: Optional Uptime Kuma Monitor Name to filter by</pre>
<p>
{{ $t("Then choose an action, for example switch the scene to where an RGB light is red.") }}
</p>
</div>
</div>
</template>

@ -0,0 +1,9 @@
<template>
<div class="mb-3">
<label for="line-notify-access-token" class="form-label">{{ $t("Access Token") }}</label>
<input id="line-notify-access-token" v-model="$parent.notification.lineNotifyAccessToken" type="text" class="form-control" :required="true">
</div>
<i18n-t tag="div" keypath="wayToGetLineNotifyToken" class="form-text" style="margin-top: 8px;">
<a href="https://notify-bot.line.me/" target="_blank">https://notify-bot.line.me/</a>
</i18n-t>
</template>

@ -0,0 +1,30 @@
<template>
<div class="mb-3">
<label for="ntfy-ntfytopic" class="form-label">{{ $t("ntfy Topic") }}</label>
<div class="input-group mb-3">
<input id="ntfy-ntfytopic" v-model="$parent.notification.ntfytopic" type="text" class="form-control" required>
</div>
</div>
<div class="mb-3">
<label for="ntfy-server-url" class="form-label">{{ $t("Server URL") }}</label>
<div class="input-group mb-3">
<input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required>
</div>
</div>
<div class="mb-3">
<label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
</div>
</template>
<script>
export default {
mounted() {
if (typeof this.$parent.notification.ntfyPriority === "undefined") {
this.$parent.notification.ntfyserverurl = "https://ntfy.sh";
this.$parent.notification.ntfyPriority = 5;
}
},
};
</script>

@ -0,0 +1,45 @@
<template>
<div class="mb-3">
<label for="pagerduty-integration-key" class="form-label">{{ $t("Integration Key") }}</label>
<HiddenInput id="pagerduty-integration-key" v-model="$parent.notification.pagerdutyIntegrationKey" :required="true" autocomplete="false"></HiddenInput>
<i18n-t tag="div" keypath="wayToGetPagerDutyKey" class="form-text">
<a href="https://support.pagerduty.com/docs/services-and-integrations" target="_blank">{{ $t("here") }}</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="pagerduty-integration-url" class="form-label">{{ $t("Integration URL") }}</label>
<input id="pagerduty-integration-url" v-model="$parent.notification.pagerdutyIntegrationUrl" type="text" class="form-control" autocomplete="false">
</div>
<div class="mb-3">
<label for="pagerduty-priority" class="form-label">{{ $t("Priority") }}</label>
<select id="pagerduty-priority" v-model="$parent.notification.pagerdutyPriority" class="form-select">
<option value="info">{{ $t("info") }}</option>
<option value="warning" selected="selected">{{ $t("warning") }}</option>
<option value="error">{{ $t("error") }}</option>
<option value="critical">{{ $t("critical") }}</option>
</select>
</div>
<div class="mb-3">
<label for="pagerduty-resolve" class="form-label">{{ $t("Auto resolve or acknowledged") }}</label>
<select id="pagerduty-resolve" v-model="$parent.notification.pagerdutyAutoResolve" class="form-select">
<option value="0" selected="selected">{{ $t("do nothing") }}</option>
<option value="acknowledge">{{ $t("auto acknowledged") }}</option>
<option value="resolve">{{ $t("auto resolve") }}</option>
</select>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
mounted() {
if (typeof this.$parent.notification.pagerdutyIntegrationUrl === "undefined") {
this.$parent.notification.pagerdutyIntegrationUrl = "https://events.pagerduty.com/v2/enqueue";
}
}
};
</script>

@ -1,8 +1,8 @@
<template>
<div class="mb-3">
<label for="promosms-login" class="form-label">{{ $("promosmsLogin") }}</label>
<label for="promosms-login" class="form-label">{{ $t("promosmsLogin") }}</label>
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
<label for="promosms-key" class="form-label">{{ $("promosmsPassword") }}</label>
<label for="promosms-key" class="form-label">{{ $t("promosmsPassword") }}</label>
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="mb-3">

@ -1,36 +1,41 @@
import STMP from "./SMTP.vue";
import Telegram from "./Telegram.vue";
import Alerta from "./Alerta.vue";
import AlertNow from "./AlertNow.vue";
import AliyunSMS from "./AliyunSms.vue";
import Apprise from "./Apprise.vue";
import Bark from "./Bark.vue";
import ClickSendSMS from "./ClickSendSMS.vue";
import DingDing from "./DingDing.vue";
import Discord from "./Discord.vue";
import Webhook from "./Webhook.vue";
import Signal from "./Signal.vue";
import Feishu from "./Feishu.vue";
import GoogleChat from "./GoogleChat.vue";
import Gorush from "./Gorush.vue";
import Gotify from "./Gotify.vue";
import Slack from "./Slack.vue";
import RocketChat from "./RocketChat.vue";
import Teams from "./Teams.vue";
import Pushover from "./Pushover.vue";
import Pushy from "./Pushy.vue";
import TechulusPush from "./TechulusPush.vue";
import HomeAssistant from "./HomeAssistant.vue";
import Line from "./Line.vue";
import LineNotify from "./LineNotify.vue";
import LunaSea from "./LunaSea.vue";
import Matrix from "./Matrix.vue";
import Mattermost from "./Mattermost.vue";
import Ntfy from "./Ntfy.vue";
import Octopush from "./Octopush.vue";
import OneBot from "./OneBot.vue";
import PagerDuty from "./PagerDuty.vue";
import PromoSMS from "./PromoSMS.vue";
import ClickSendSMS from "./ClickSendSMS.vue";
import LunaSea from "./LunaSea.vue";
import Feishu from "./Feishu.vue";
import Apprise from "./Apprise.vue";
import Pushbullet from "./Pushbullet.vue";
import Line from "./Line.vue";
import Mattermost from "./Mattermost.vue";
import Matrix from "./Matrix.vue";
import AliyunSMS from "./AliyunSms.vue";
import DingDing from "./DingDing.vue";
import Bark from "./Bark.vue";
import PushDeer from "./PushDeer.vue";
import Pushover from "./Pushover.vue";
import Pushy from "./Pushy.vue";
import RocketChat from "./RocketChat.vue";
import SerwerSMS from "./SerwerSMS.vue";
import Signal from "./Signal.vue";
import Slack from "./Slack.vue";
import Stackfield from "./Stackfield.vue";
import STMP from "./SMTP.vue";
import Teams from "./Teams.vue";
import TechulusPush from "./TechulusPush.vue";
import Telegram from "./Telegram.vue";
import Webhook from "./Webhook.vue";
import WeCom from "./WeCom.vue";
import GoogleChat from "./GoogleChat.vue";
import Gorush from "./Gorush.vue";
import Alerta from "./Alerta.vue";
import OneBot from "./OneBot.vue";
import PushDeer from "./PushDeer.vue";
/**
* Manage all notification form.
@ -38,39 +43,44 @@ import PushDeer from "./PushDeer.vue";
* @type { Record<string, any> }
*/
const NotificationFormList = {
"telegram": Telegram,
"webhook": Webhook,
"smtp": STMP,
"alerta": Alerta,
"AlertNow": AlertNow,
"AliyunSMS": AliyunSMS,
"apprise": Apprise,
"Bark": Bark,
"clicksendsms": ClickSendSMS,
"DingDing": DingDing,
"discord": Discord,
"teams": Teams,
"signal": Signal,
"Feishu": Feishu,
"GoogleChat": GoogleChat,
"gorush": Gorush,
"gotify": Gotify,
"slack": Slack,
"rocket.chat": RocketChat,
"pushover": Pushover,
"pushy": Pushy,
"PushByTechulus": TechulusPush,
"HomeAssistant": HomeAssistant,
"line": Line,
"LineNotify": LineNotify,
"lunasea": LunaSea,
"matrix": Matrix,
"mattermost": Mattermost,
"ntfy": Ntfy,
"octopush": Octopush,
"OneBot": OneBot,
"PagerDuty": PagerDuty,
"promosms": PromoSMS,
"clicksendsms": ClickSendSMS,
"lunasea": LunaSea,
"Feishu": Feishu,
"AliyunSMS": AliyunSMS,
"apprise": Apprise,
"pushbullet": Pushbullet,
"line": Line,
"mattermost": Mattermost,
"matrix": Matrix,
"DingDing": DingDing,
"Bark": Bark,
"PushByTechulus": TechulusPush,
"PushDeer": PushDeer,
"pushover": Pushover,
"pushy": Pushy,
"rocket.chat": RocketChat,
"serwersms": SerwerSMS,
"signal": Signal,
"slack": Slack,
"smtp": STMP,
"stackfield": Stackfield,
"teams": Teams,
"telegram": Telegram,
"webhook": Webhook,
"WeCom": WeCom,
"GoogleChat": GoogleChat,
"gorush": Gorush,
"alerta": Alerta,
"OneBot": OneBot,
"PushDeer": PushDeer,
};
export default NotificationFormList;

@ -4,6 +4,11 @@
<object class="my-4" width="200" height="200" data="/icon.svg" />
<div class="fs-4 fw-bold">Uptime Kuma</div>
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
<div class="frontend-version">{{ $t("Frontend Version") }}: {{ $root.frontendVersion }}</div>
<div v-if="!$root.isFrontendBackendVersionMatched" class="alert alert-warning mt-4" role="alert">
{{ $t("Frontend Version do not match backend version!") }}
</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>
@ -46,6 +51,16 @@ export default {
}
.update-link {
font-size: 0.8em;
}
.frontend-version {
font-size: 0.9em;
color: #cccccc;
.dark & {
color: #333333;
}
}
</style>

@ -133,10 +133,15 @@ export default {
},
methods: {
/**
* Show the confimation dialog confirming the configuration
* be imported
*/
confirmImport() {
this.$refs.confirmImport.show();
},
/** Download a backup of the configuration */
downloadBackup() {
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
let fileName = `Uptime_Kuma_Backup_${time}.json`;
@ -157,6 +162,10 @@ export default {
downloadItem.click();
},
/**
* Import the specified backup file
* @returns {?string}
*/
importBackup() {
this.processing = true;
let uploadItem = document.getElementById("import-backend").files;

@ -0,0 +1,48 @@
<template>
<div>
<div class="dockerHost-list my-4">
<p v-if="$root.dockerHostList.length === 0">
{{ $t("Not available, please setup.") }}
</p>
<ul class="list-group mb-3" style="border-radius: 1rem;">
<li v-for="(dockerHost, index) in $root.dockerHostList" :key="index" class="list-group-item">
{{ dockerHost.name }}<br>
<a href="#" @click="$refs.dockerHostDialog.show(dockerHost.id)">{{ $t("Edit") }}</a>
</li>
</ul>
<button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
{{ $t("Setup Docker Host") }}
</button>
</div>
<DockerHostDialog ref="dockerHostDialog" />
</div>
</template>
<script>
import DockerHostDialog from "../../components/DockerHostDialog.vue";
export default {
components: {
DockerHostDialog,
},
data() {
return {};
},
computed: {
settings() {
return this.$parent.$parent.$parent.settings;
},
saveSettings() {
return this.$parent.$parent.$parent.saveSettings;
},
settingsLoaded() {
return this.$parent.$parent.$parent.settingsLoaded;
},
}
};
</script>

@ -178,10 +178,12 @@ export default {
},
methods: {
/** Save the settings */
saveGeneral() {
localStorage.timezone = this.$root.userTimezone;
this.saveSettings();
},
/** Get the base URL of the application */
autoGetPrimaryBaseURL() {
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
},

@ -90,6 +90,7 @@ export default {
},
methods: {
/** Get the current size of the database */
loadDatabaseSize() {
log.debug("monitorhistory", "load database size");
this.$root.getSocket().emit("getDatabaseSize", (res) => {
@ -102,6 +103,7 @@ export default {
});
},
/** Request that the database is shrunk */
shrinkDatabase() {
this.$root.getSocket().emit("shrinkDatabase", (res) => {
if (res.ok) {
@ -113,10 +115,12 @@ export default {
});
},
/** Show the dialog to confirm clearing stats */
confirmClearStatistics() {
this.$refs.confirmClearStatistics.show();
},
/** Send the request to clear stats */
clearStatistics() {
this.$root.clearStatistics((res) => {
if (res.ok) {

@ -20,16 +20,92 @@
</button>
</div>
<div class="my-4 pt-4">
<h5 class="my-4 settings-subheading">{{ $t("settingsCertificateExpiry") }}</h5>
<p>{{ $t("certificationExpiryDescription") }}</p>
<p>{{ $t("notificationDescription") }}</p>
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
<span>{{ day }} {{ $tc("day", day) }}</span>
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)">
<font-awesome-icon class="" icon="times" />
</button>
</div>
</div>
<div class="col-12 col-xl-6">
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" />
</div>
<div>
<button class="btn btn-primary" type="button" @click="saveSettings()">
{{ $t("Save") }}
</button>
</div>
</div>
<NotificationDialog ref="notificationDialog" />
</div>
</template>
<script>
import NotificationDialog from "../../components/NotificationDialog.vue";
import ActionInput from "../ActionInput.vue";
export default {
components: {
NotificationDialog
NotificationDialog,
ActionInput,
},
data() {
return {
/**
* Variable to store the input for new certificate expiry day.
*/
expiryNotifInput: null,
};
},
computed: {
settings() {
return this.$parent.$parent.$parent.settings;
},
saveSettings() {
return this.$parent.$parent.$parent.saveSettings;
},
settingsLoaded() {
return this.$parent.$parent.$parent.settingsLoaded;
},
},
methods: {
/**
* Remove a day from expiry notification days.
* @param {number} day The day to remove.
*/
removeExpiryNotifDay(day) {
this.settings.tlsExpiryNotifyDays = this.settings.tlsExpiryNotifyDays.filter(d => d !== day);
},
/**
* Add a new expiry notification day.
* Will verify:
* - day is not null or empty string.
* - day is a number.
* - day is > 0.
* - The day is not already in the list.
* @param {number} day The day number to add.
*/
addExpiryNotifDay(day) {
if (day != null && day !== "") {
const parsedDay = parseInt(day);
if (parsedDay != null && !isNaN(parsedDay) && parsedDay > 0) {
if (!this.settings.tlsExpiryNotifyDays.includes(parsedDay)) {
this.settings.tlsExpiryNotifyDays.push(parseInt(day));
this.settings.tlsExpiryNotifyDays.sort((a, b) => a - b);
this.expiryNotifInput = null;
}
}
}
},
},
};
</script>
@ -37,10 +113,27 @@ export default {
<style lang="scss" scoped>
@import "../../assets/vars.scss";
.btn-rm-expiry {
padding-left: 11px;
padding-right: 11px;
}
.dark {
.list-group-item {
background-color: $dark-bg2;
color: $dark-font-color;
}
}
.cert-exp-days .cert-exp-day-row {
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
.dark & {
border-bottom: 1px solid $dark-border-color;
}
}
.cert-exp-days .cert-exp-day-row:last-child {
border: none;
}
</style>

@ -68,7 +68,9 @@
<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">
<p class="mt-2">{{ $t("disableCloudflaredNoAuthMsg") }}</p>
<div v-if="!settings.disableAuth" class="mt-3">
<label for="current-password2" class="form-label">
{{ $t("Current Password") }}
</label>
@ -89,6 +91,51 @@
{{ $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>
<h4 class="my-4">{{ $t("HTTP Headers") }}</h4>
<div class="my-3">
<label class="form-label">
{{ $t("Trust Proxy") }}
</label>
<div class="form-check">
<input
id="trustProxyYes"
v-model="settings.trustProxy"
class="form-check-input"
type="radio"
name="trustProxyYes"
:value="true"
required
/>
<label class="form-check-label" for="trustProxyYes">
{{ $t("Yes") }}
</label>
</div>
<div class="form-check">
<input
id="trustProxyNo"
v-model="settings.trustProxy"
class="form-check-input"
type="radio"
name="flexRadioDefault"
:value="false"
required
/>
<label class="form-check-label" for="trustProxyNo">
{{ $t("No") }}
</label>
</div>
<div class="form-text">
{{ $t("trustProxyDescription") }}
</div>
</div>
<div>
<button class="btn btn-primary" type="submit" @click="saveSettings()">
{{ $t("Save") }}
</button>
</div>
</div>
</template>
@ -108,7 +155,15 @@ export default {
return this.$root.cloudflared;
},
computed: {
settings() {
return this.$parent.$parent.$parent.settings;
},
saveSettings() {
return this.$parent.$parent.$parent.saveSettings;
},
settingsLoaded() {
return this.$parent.$parent.$parent.settingsLoaded;
},
},
watch: {
@ -120,14 +175,17 @@ export default {
this.$root.getSocket().emit(prefix + "leave");
},
methods: {
/** Start the Cloudflare tunnel */
start() {
this.$root.getSocket().emit(prefix + "start", this.cloudflareTunnelToken);
},
/** Stop the Cloudflare tunnel */
stop() {
this.$root.getSocket().emit(prefix + "stop", this.currentPassword, (res) => {
this.$root.toastRes(res);
});
},
/** Remove the token for the Cloudflare tunnel */
removeToken() {
this.$root.getSocket().emit(prefix + "removeToken");
this.cloudflareTunnelToken = "";

@ -8,7 +8,7 @@
<button v-if="! settings.disableAuth" id="logout-btn" class="btn btn-danger ms-4 me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button>
</p>
<h5 class="my-4">{{ $t("Change Password") }}</h5>
<h5 class="my-4 settings-subheading">{{ $t("Change Password") }}</h5>
<form class="mb-3" @submit.prevent="savePassword">
<div class="mb-3">
<label for="current-password" class="form-label">
@ -62,7 +62,7 @@
</template>
<div v-if="! settings.disableAuth" class="mt-5 mb-3">
<h5 class="my-4">
<h5 class="my-4 settings-subheading">
{{ $t("Two Factor Authentication") }}
</h5>
<div class="mb-4">
@ -78,7 +78,7 @@
<div class="my-4">
<!-- Advanced -->
<h5 class="my-4">{{ $t("Advanced") }}</h5>
<h5 class="my-4 settings-subheading">{{ $t("Advanced") }}</h5>
<div class="mb-4">
<button v-if="settings.disableAuth" id="enableAuth-btn" class="btn btn-outline-primary me-2 mb-2" @click="enableAuth">{{ $t("Enable Auth") }}</button>
@ -90,156 +90,11 @@
<TwoFADialog ref="TwoFADialog" />
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
<template v-if="$i18n.locale === 'es-ES' ">
<p>Seguro que deseas <strong>deshabilitar la autenticación</strong>?</p>
<p>Es para <strong>quien implementa autenticación de terceros</strong> ante Uptime Kuma como por ejemplo Cloudflare Access.</p>
<p>Por favor usar con cuidado.</p>
</template>
<template v-else-if="$i18n.locale === 'pt-BR' ">
<p>Você tem certeza que deseja <strong>desativar a autenticação</strong>?</p>
<p>Isso é para <strong>alguém que tem autenticação de terceiros</strong> na frente do 'UpTime Kuma' como o Cloudflare Access.</p>
<p>Por favor, utilize isso com cautela.</p>
</template>
<template v-else-if="$i18n.locale === 'zh-HK' ">
<p>你是否確認<strong>取消登入認証</strong></p>
<p>這個功能是設計給已有<strong>第三方認証</strong>的用家例如 Cloudflare Access</p>
<p>請小心使用</p>
</template>
<template v-else-if="$i18n.locale === 'zh-CN' ">
<p>是否确定 <strong>取消登录验证</strong></p>
<p>这是为 <strong>有第三方认证</strong> 的用户提供的功能 Cloudflare Access</p>
<p>请谨慎使用</p>
</template>
<template v-else-if="$i18n.locale === 'zh-TW' ">
<p>你是否要<strong>取消登入驗證</strong></p>
<p>此功能是設計給已有<strong>第三方認證</strong>的使用者例如 Cloudflare Access</p>
<p>請謹慎使用</p>
</template>
<template v-else-if="$i18n.locale === 'de-DE' ">
<p>Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?</p>
<p>Es ist für <strong>jemanden der eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access.</p>
<p>Bitte mit Vorsicht nutzen.</p>
</template>
<template v-else-if="$i18n.locale === 'sl-SI' ">
<p>Ali ste prepričani, da želite onemogočiti <strong>avtentikacijo</strong>?</p>
<p>Namenjen je <strong>nekomu, ki ima pred programom Uptime Kuma vklopljeno zunanje preverjanje pristnosti</strong>, na primer Cloudflare Access.</p>
<p>Uporabljajte previdno.</p>
</template>
<template v-else-if="$i18n.locale === 'sr' ">
<p>Да ли сте сигурни да желите да <strong>искључите аутентификацију</strong>?</p>
<p>То је за <strong>оне који имају додату аутентификацију</strong> испред Uptime Kuma као на пример Cloudflare Access.</p>
<p>Молим Вас користите ово са пажњом.</p>
</template>
<template v-else-if="$i18n.locale === 'sr-latn' ">
<p>Da li ste sigurni da želite da <strong>isključite autentifikaciju</strong>?</p>
<p>To je za <strong>one koji imaju dodatu autentifikaciju</strong> ispred Uptime Kuma kao na primer Cloudflare Access.</p>
<p>Molim Vas koristite ovo sa pažnjom.</p>
</template>
<template v-if="$i18n.locale === 'hr-HR' ">
<p>Jeste li sigurni da želite <strong>isključiti autentikaciju</strong>?</p>
<p>To je za <strong>korisnike koji imaju vanjsku autentikaciju stranice</strong> ispred Uptime Kume, poput usluge Cloudflare Access.</p>
<p>Pažljivo koristite ovu opciju.</p>
</template>
<template v-else-if="$i18n.locale === 'tr-TR' ">
<p><strong>Şifreli girişi devre dışı bırakmak istediğinizden</strong>emin misiniz?</p>
<p>Bu, Uptime Kuma'nın önünde Cloudflare Access gibi <strong>üçüncü taraf yetkilendirmesi olan</strong> kişiler içindir.</p>
<p>Lütfen dikkatli kullanın.</p>
</template>
<template v-else-if="$i18n.locale === 'ko-KR' ">
<p>정말로 <strong>인증 기능을 끌까요</strong>?</p>
<p> 기능은 <strong>Cloudflare Access와 같은 서드파티 인증</strong> Uptime Kuma 앞에 사용자를 위한 기능이에요.</p>
<p>신중하게 사용하세요.</p>
</template>
<template v-else-if="$i18n.locale === 'pl' ">
<p>Czy na pewno chcesz <strong>wyłączyć autoryzację</strong>?</p>
<p>Jest przeznaczony dla <strong>kogoś, kto ma autoryzację zewnętrzną</strong> przed Uptime Kuma, taką jak Cloudflare Access.</p>
<p>Proszę używać ostrożnie.</p>
</template>
<template v-else-if="$i18n.locale === 'et-EE' ">
<p>Kas soovid <strong>lülitada autentimise välja</strong>?</p>
<p>Kastuamiseks <strong>välise autentimispakkujaga</strong>, näiteks Cloudflare Access.</p>
<p>Palun kasuta vastutustundlikult.</p>
</template>
<template v-else-if="$i18n.locale === 'it-IT' ">
<p><strong>Disabilitare l'autenticazione?</strong></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>
</template>
<template v-else-if="$i18n.locale === 'id-ID' ">
<p>Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>?</p>
<p>Ini untuk <strong>mereka yang memiliki autentikasi pihak ketiga</strong> diletakkan di depan Uptime Kuma, misalnya akses Cloudflare.</p>
<p>Gunakan dengan hati-hati.</p>
</template>
<template v-else-if="$i18n.locale === 'ru-RU' ">
<p>Вы уверены, что хотите <strong>отключить авторизацию</strong>?</p>
<p>Это подходит для <strong>тех, у кого стоит другая авторизация</strong> перед открытием Uptime Kuma, например Cloudflare Access.</p>
<p>Пожалуйста, используйте с осторожностью.</p>
</template>
<template v-else-if="$i18n.locale === 'uk-UA' ">
<p>Ви впевнені, що бажаєте <strong>вимкнути авторизацію</strong>?</p>
<p>Це підходить для <strong>тих, у кого встановлена інша авторизація</strong> пееред відкриттям Uptime Kuma, наприклад Cloudflare Access.</p>
<p>Будь ласка, використовуйте з обережністю.</p>
</template>
<template v-else-if="$i18n.locale === 'fa' ">
<p>آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?</p>
<p>این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کردهاند</strong>، مانند Cloudflare Access.</p>
<p>لطفا از این امکان با دقت استفاده کنید.</p>
</template>
<template v-else-if="$i18n.locale === 'bg-BG' ">
<p>Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?</p>
<p>Използва се в случаите, когато <strong>има настроен алтернативен метод за удостоверяване</strong> преди Uptime Kuma, например Cloudflare Access.</p>
<p>Моля, използвайте с повишено внимание.</p>
</template>
<template v-else-if="$i18n.locale === 'hu' ">
<p>Biztos benne, hogy <strong>kikapcsolja a hitelesítést</strong>?</p>
<p>Akkor érdemes, ha <strong>van 3rd-party hitelesítés</strong> az Uptime Kuma-t megelőzően mint a Cloudflare Access.</p>
<p>Használja megfontoltan!</p>
</template>
<template v-else-if="$i18n.locale === 'nb-NO' ">
<p>Er du sikker at du vil <strong>deaktiver autentisering</strong>?</p>
<p>Dette er for <strong>de som har tredjepartsautorisering</strong> foran Uptime Kuma, for eksempel Cloudflare Access.</p>
<p>Vennligst vær forsiktig.</p>
</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) -->
<template v-else>
<p>Are you sure want to <strong>disable authentication</strong>?</p>
<p>It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.</p>
<p>Please use this option carefully!</p>
</template>
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="$t('disableauth.message1')"></p>
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="$t('disableauth.message2')"></p>
<p>{{ $t("Please use this option carefully!") }}</p>
<div class="mb-3">
<label for="current-password2" class="form-label">
@ -297,6 +152,7 @@ export default {
},
methods: {
/** Check new passwords match before saving them */
savePassword() {
if (this.password.newPassword !== this.password.repeatNewPassword) {
this.invalidPassword = true;
@ -314,6 +170,7 @@ export default {
}
},
/** Disable authentication for web app access */
disableAuth() {
this.settings.disableAuth = true;
@ -326,6 +183,7 @@ export default {
}, this.password.currentPassword);
},
/** Enable authentication for web app access */
enableAuth() {
this.settings.disableAuth = false;
this.saveSettings();
@ -340,15 +198,3 @@ export default {
},
};
</script>
<style lang="scss" scoped>
@import "../../assets/vars.scss";
h5::after {
content: "";
display: block;
width: 50%;
padding-top: 8px;
border-bottom: 1px solid $dark-border-color;
}
</style>

@ -9,7 +9,9 @@ const languageList = {
"nl-NL": "Nederlands",
"nb-NO": "Norsk",
"es-ES": "Español",
"eu": "Euskara",
"fa": "Farsi",
"pt-PT": "Português (Portugal)",
"pt-BR": "Português (Brasileiro)",
"fr-FR": "Français (France)",
"hu": "Magyar",
@ -31,6 +33,7 @@ const languageList = {
"vi-VN": "Tiếng Việt",
"zh-TW": "繁體中文 (台灣)",
"uk-UA": "Український",
"th-TH": "ไทย",
};
let messages = {

@ -81,6 +81,7 @@ library.add(
faUndo,
faPlusCircle,
faAngleDown,
faLink,
);
export { FontAwesomeIcon };

@ -4,8 +4,7 @@
2. Create a language file (e.g. `zh-TW.js`). The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm
3. Run `npm run update-language-files`. You can also use this command to check if there are new strings to translate for your language.
4. Your language file should be filled in. You can translate now.
5. Translate `src/components/settings/Security.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
6. Add it into `languageList` constant.
7. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
5. Add it into `languageList` constant.
6. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
If you do not have programming skills, let me know in [the issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏

@ -55,8 +55,7 @@ export default {
Current: "Текущ",
Uptime: "Достъпност",
"Cert Exp.": "Вал. сертификат",
days: "дни",
day: "ден",
day: "ден | дни",
"-day": "-дни",
hour: "час",
"-hour": "-часa",
@ -90,13 +89,16 @@ export default {
"Search Engine Visibility": "Видимост за търсачки",
"Allow indexing": "Разреши индексиране",
"Discourage search engines from indexing site": "Не позволявай на търсачките да индексират този сайт",
"Change Password": "Промени парола",
"Change Password": "Промяна на парола",
"Current Password": "Текуща парола",
"New Password": "Нова парола",
"Repeat New Password": "Повторете новата парола",
"Update Password": "Актуализирай парола",
"Update Password": "Актуализирай паролата",
"Disable Auth": "Изключи удостоверяване",
"Enable Auth": "Включи удостоверяване",
"disableauth.message1": "Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?",
"disableauth.message2": "Използва се в случаите, когато <strong>има настроен алтернативен метод за удостоверяване</strong> преди Uptime Kuma, например Cloudflare Access, Authelia или друг механизъм за удостоверяване.",
"Please use this option carefully!": "Моля, използвайте с повишено внимание.",
Logout: "Изход от профила",
Leave: "Отказ",
"I understand, please disable": "Разбирам. Моля, изключи",
@ -145,7 +147,7 @@ export default {
"Setup 2FA": "Настройка 2FA",
"Enable 2FA": "Включи 2FA",
"Disable 2FA": "Изключи 2FA",
"2FA Settings": "Настройки 2FA",
"2FA Settings": "Настройка за 2FA",
"Two Factor Authentication": "Двуфакторно удостоверяване",
Active: "Активно",
Inactive: "Неактивно",
@ -299,7 +301,7 @@ export default {
HeadersInvalidFormat: "Заявените хедъри не са валидни JSON: ",
BodyInvalidFormat: "Заявеното съобщение не е валиден JSON: ",
"Monitor History": "История на мониторите",
clearDataOlderThan: "Ще се съхранява {0} дни.",
clearDataOlderThan: "Ще се съхранява за {0} дни.",
records: "записа",
"One record": "Един запис",
steamApiKeyDescription: "За да мониторирате Steam Gameserver се нуждаете от Steam Web-API ключ. Може да регистрирате Вашия API ключ тук: ",
@ -308,12 +310,12 @@ export default {
PasswordsDoNotMatch: "Паролите не съвпадат.",
"Current User": "Текущ потребител",
recent: "Скорошни",
shrinkDatabaseDescription: "Инициира \"VACUUM\" за \"SQLite\" база данни. Ако Вашата база данни е създадена след версия 1.10.0, \"AUTO_VACUUM\" функцията е активна и това действие не нужно.",
shrinkDatabaseDescription: "Инициира \"VACUUM\" за \"SQLite\" база данни. Ако Вашата база данни е създадена след версия 1.10.0, \"AUTO_VACUUM\" функцията е активна и това действие не е нужно.",
Done: "Готово",
Info: "Информация",
Security: "Сигурност",
"Steam API Key": "Steam API ключ",
"Shrink Database": "Редуциране база данни",
"Shrink Database": "Редуцирай базата данни",
"Pick a RR-Type...": "Изберете вида на ресурсния запис за мониторитане...",
"Pick Accepted Status Codes...": "Изберете статус кодове, които да се считат за успешен отговор...",
Default: "По подразбиране",
@ -422,6 +424,7 @@ export default {
Next: "Следващ",
"The slug is already taken. Please choose another slug.": "Този слъг вече се използва. Моля изберете друг.",
"No Proxy": "Без прокси",
Authentication: "Удостоверяване",
"HTTP Basic Auth": "HTTP основно удостоверяване",
"New Status Page": "Нова статус страница",
"Page Not Found": "Страницата не е открита",
@ -464,4 +467,74 @@ export default {
"Domain Names": "Домейни",
signedInDisp: "Вписан като {0}",
signedInDispDisabled: "Удостоверяването е изключено.",
"Certificate Expiry Notification": "Известие за изтичане валидността на сертификата",
"API Username": "API Потребител",
"API Key": "API Ключ",
"Recipient Number": "Номер на получателя",
"From Name/Number": "От Име/Номер",
"Leave blank to use a shared sender number.": "Оставете празно, за да използвате споделен номер на подател.",
"Octopush API Version": "Octopush API версия",
"Legacy Octopush-DM": "Octopush-DM старa версия",
endpoint: "крайна точка",
octopushAPIKey: "\"API ключ\" от HTTP API удостоверяване в контролния панел",
octopushLogin: "\"Вписване\" от HTTP API удостоверяване в контролния панел",
promosmsLogin: "API Потребителско име",
promosmsPassword: "API Парола",
"pushoversounds pushover": "Pushover (по подразбиране)",
"pushoversounds bike": "Велосипед",
"pushoversounds bugle": "Тромпет",
"pushoversounds cashregister": "Касов апарат",
"pushoversounds classical": "Класическа музика",
"pushoversounds cosmic": "Космически",
"pushoversounds falling": "Падащ",
"pushoversounds gamelan": "Игра в мрежа",
"pushoversounds incoming": "Входящ",
"pushoversounds intermission": "Прекъсване",
"pushoversounds magic": "Магия",
"pushoversounds mechanical": "Механичен",
"pushoversounds pianobar": "Пиано бар",
"pushoversounds siren": "Сирена",
"pushoversounds spacealarm": "Космическа аларма",
"pushoversounds tugboat": "Буксир",
"pushoversounds alien": "Извънземна аларма (дълъг)",
"pushoversounds climb": "Изкачване (дълъг)",
"pushoversounds persistent": "Постоянен (дълъг)",
"pushoversounds echo": "Pushover ехо (дълъг)",
"pushoversounds updown": "Горе долу (дълъг)",
"pushoversounds vibrate": "Само вибрация",
"pushoversounds none": "Без (тих)",
pushyAPIKey: "Таен API ключ",
pushyToken: "Токен на устройство",
"Show update if available": "Покажи актуализация, ако е налична",
"Also check beta release": "Проверявай и за бета версии",
"Using a Reverse Proxy?": "Използвате ревърс прокси?",
"Check how to config it for WebSocket": "Проверете как да го конфигурирате за WebSocket",
"Steam Game Server": "Steam Game сървър",
"Most likely causes:": "Най-вероятни причини:",
"The resource is no longer available.": "Ресурсът вече не е наличен.",
"There might be a typing error in the address.": "Възможно е да е допусната грешка при изписването на адреса.",
"What you can try:": "Може да опитате:",
"Retype the address.": "Повторно въвеждане на адреса.",
"Go back to the previous page.": "Да се върнете към предишната страница.",
"Coming Soon": "Очаквайте скоро",
wayToGetClickSendSMSToken: "Може да получите API потребителско име и API ключ от {0} .",
dnsPortDescription: "DNS порт на сървъра. По подразбиране е 53, но може да бъде променен по всяко време.",
error: "грешка",
critical: "критично",
wayToGetPagerDutyKey: "Може да го получите като посетите Service -> Service Directory -> (Select a service) -> Integrations -> Add integration. Тук трябва да потърсите \"Events API V2\". Повече информация {0}",
"Integration Key": "Ключ за интегриране",
"Integration URL": "URL адрес за интеграция",
"Auto resolve or acknowledged": "Автоматично разрешаване или потвърждаване",
"do nothing": "не прави нищо",
"auto acknowledged": "автоматично потвърждаване",
"auto resolve": "автоматично разрешаване",
"Connection String": "Стринг за връзка",
Query: "Заявка",
settingsCertificateExpiry: "Изтичане валидността на TLS сертификата",
certificationExpiryDescription: "HTTPS мониторите ще задействат известие, ако е наличен изтичащ TLS сертификат, през следващите:",
"ntfy Topic": "ntfy Тема",
Domain: "Домейн",
Workstation: "Работна станция",
disableCloudflaredNoAuthMsg: "Тъй като сте в режим \"No Auth mode\", парола не се изисква.",
wayToGetLineNotifyToken: "Може да получите токен код за достъп от {0}",
};

@ -56,8 +56,7 @@ export default {
Current: "Aktuální",
Uptime: "Doba provozu",
"Cert Exp.": "Platnost certifikátu",
days: "dny/í",
day: "den",
day: "den | dny/í",
"-day": "-dní",
hour: "hodina",
"-hour": "-hodin",
@ -101,6 +100,9 @@ export default {
"Update Password": "Aktualizovat heslo",
"Disable Auth": "Deaktivovat ověřování",
"Enable Auth": "Povolit ověřování",
"disableauth.message1": "Opravdu chcete <strong>deaktivovat autentifikaci</strong>?",
"disableauth.message2": "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.",
"Please use this option carefully!": "Používejte ji prosím s rozmyslem.",
Logout: "Odhlášení",
Leave: "Odejít",
"I understand, please disable": "Rozumím, chci ji deaktivovat",

@ -30,8 +30,7 @@ export default {
Current: "Aktuelt",
Uptime: "Oppetid",
"Cert Exp.": "Certifikatets udløb",
days: "Dage",
day: "Dag",
day: "Dag | Dage",
"-day": "-Dage",
hour: "Timer",
"-hour": "-Timer",

@ -30,8 +30,7 @@ export default {
Current: "Aktuell",
Uptime: "Verfügbarkeit",
"Cert Exp.": "Zertifikatsablauf",
days: "Tage",
day: "Tag",
day: "Tag | Tage",
"-day": "-Tage",
hour: "Stunde",
"-hour": "-Stunden",
@ -78,6 +77,9 @@ export default {
"Update Password": "Passwort aktualisieren",
"Disable Auth": "Authentifizierung deaktivieren",
"Enable Auth": "Authentifizierung aktivieren",
"disableauth.message1": "Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?",
"disableauth.message2": "Es ist für <strong>jemanden der eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access.",
"Please use this option carefully!": "Bitte mit Vorsicht nutzen.",
Logout: "Ausloggen",
notificationDescription: "Benachrichtigungen müssen einem Monitor zugewiesen werden, damit diese funktionieren.",
Leave: "Verlassen",
@ -422,6 +424,7 @@ export default {
Next: "Weiter",
"The slug is already taken. Please choose another slug.": "Der Slug ist bereits in Verwendung. Bitte wähle einen anderen.",
"No Proxy": "Kein Proxy",
Authentication: "Authentifizierung",
"HTTP Basic Auth": "HTTP Basisauthentifizierung",
"New Status Page": "Neue Status-Seite",
"Page Not Found": "Seite nicht gefunden",

@ -13,6 +13,7 @@ export default {
pauseDashboardHome: "Pause",
deleteMonitorMsg: "Are you sure want to delete this monitor?",
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?",
dnsPortDescription: "DNS server port. Defaults to 53. You can change the port at any time.",
resolverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.",
rrtypeDescription: "Select the RR type you want to monitor",
pauseMonitorMsg: "Are you sure want to pause?",
@ -56,8 +57,7 @@ export default {
Current: "Current",
Uptime: "Uptime",
"Cert Exp.": "Cert Exp.",
days: "days",
day: "day",
day: "day | days",
"-day": "-day",
hour: "hour",
"-hour": "-hour",
@ -101,6 +101,9 @@ export default {
"Update Password": "Update Password",
"Disable Auth": "Disable Auth",
"Enable Auth": "Enable Auth",
"disableauth.message1": "Are you sure want to <strong>disable authentication</strong>?",
"disableauth.message2": "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.",
"Please use this option carefully!": "Please use this option carefully!",
Logout: "Logout",
Leave: "Leave",
"I understand, please disable": "I understand, please disable",
@ -330,6 +333,8 @@ export default {
info: "info",
warning: "warning",
danger: "danger",
error: "error",
critical: "critical",
primary: "primary",
light: "light",
dark: "dark",
@ -370,6 +375,13 @@ export default {
smtpDkimHashAlgo: "Hash Algorithm (Optional)",
smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
smtpDkimskipFields: "Header Keys not to sign (Optional)",
wayToGetPagerDutyKey: "You can get this by going to Service -> Service Directory -> (Select a service) -> Integrations -> Add integration. Here you can search for \"Events API V2\". More info {0}",
"Integration Key": "Integration Key",
"Integration URL": "Integration URL",
"Auto resolve or acknowledged": "Auto resolve or acknowledged",
"do nothing": "do nothing",
"auto acknowledged": "auto acknowledged",
"auto resolve": "auto resolve",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
@ -396,6 +408,8 @@ export default {
SignName: "SignName",
"Sms template must contain parameters: ": "Sms template must contain parameters: ",
"Bark Endpoint": "Bark Endpoint",
"Bark Group": "Bark Group",
"Bark Sound": "Bark Sound",
WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey",
"For safety, must use secret key": "For safety, must use secret key",
@ -429,6 +443,7 @@ export default {
Next: "Next",
"The slug is already taken. Please choose another slug.": "The slug is already taken. Please choose another slug.",
"No Proxy": "No Proxy",
Authentication: "Authentication",
"HTTP Basic Auth": "HTTP Basic Auth",
"New Status Page": "New Status Page",
"Page Not Found": "Page Not Found",
@ -440,6 +455,8 @@ export default {
"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.",
"HTTP Headers": "HTTP Headers",
"Trust Proxy": "Trust Proxy",
"Other Software": "Other Software",
"For example: nginx, Apache and Traefik.": "For example: nginx, Apache and Traefik.",
"Please read": "Please read",
@ -452,6 +469,7 @@ export default {
"Domain Name Expiry Notification": "Domain Name Expiry Notification",
Proxy: "Proxy",
"Date Created": "Date Created",
HomeAssistant: "Home Assistant",
onebotHttpAddress: "OneBot HTTP Address",
onebotMessageType: "OneBot Message Type",
onebotGroupMessage: "Group",
@ -478,7 +496,7 @@ export default {
"Leave blank to use a shared sender number.": "Leave blank to use a shared sender number.",
"Octopush API Version": "Octopush API Version",
"Legacy Octopush-DM": "Legacy Octopush-DM",
"endpoint": "endpoint",
endpoint: "endpoint",
octopushAPIKey: "\"API key\" from HTTP API credentials in control panel",
octopushLogin: "\"Login\" from HTTP API credentials in control panel",
promosmsLogin: "API Login Name",
@ -521,4 +539,24 @@ export default {
"Go back to the previous page.": "Go back to the previous page.",
"Coming Soon": "Coming Soon",
wayToGetClickSendSMSToken: "You can get API Username and API Key from {0} .",
"Connection String": "Connection String",
Query: "Query",
settingsCertificateExpiry: "TLS Certificate Expiry",
certificationExpiryDescription: "HTTPS Monitors trigger notification when TLS certificate expires in:",
"Setup Docker Host": "Setup Docker Host",
"Connection Type": "Connection Type",
"Docker Daemon": "Docker Daemon",
deleteDockerHostMsg: "Are you sure want to delete this docker host for all monitors?",
socket: "Socket",
tcp: "TCP / HTTP",
"Docker Container": "Docker Container",
"Container Name / ID": "Container Name / ID",
"Docker Host": "Docker Host",
"Docker Hosts": "Docker Hosts",
"ntfy Topic": "ntfy Topic",
"Domain": "Domain",
"Workstation": "Workstation",
disableCloudflaredNoAuthMsg: "You are in No Auth mode, password is not require.",
trustProxyDescription: "Trust 'X-Forwarded-*' headers. If you want to get the correct client IP and your Uptime Kuma is behind such as Nginx or Apache, you should enable this.",
wayToGetLineNotifyToken: "You can get an access token from {0}",
};

@ -7,8 +7,8 @@ export default {
maxRedirectDescription: "Número máximo de direcciones a seguir. Establecer a 0 para deshabilitar.",
acceptedStatusCodesDescription: "Seleccionar los códigos de estado que se consideran como respuesta exitosa.",
passwordNotMatchMsg: "La contraseña repetida no coincide.",
notificationDescription: "Por favor asigne una notificación a el/los monitor(es) para hacerlos funcional(es).",
keywordDescription: "Palabra clave en HTML plano o respuesta JSON y es sensible a mayúsculas",
notificationDescription: "Por favor asigna una notificación a el/los monitor(es) para hacerlos funcional(es).",
keywordDescription: "Palabra clave en HTML plano o respuesta JSON, es sensible a mayúsculas",
pauseDashboardHome: "Pausado",
deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?",
deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?",
@ -35,7 +35,7 @@ export default {
Pause: "Pausar",
Name: "Nombre",
Status: "Estado",
DateTime: "Fecha y Hora",
DateTime: "Fecha y hora",
Message: "Mensaje",
"No important events": "No hay eventos importantes",
Resume: "Reanudar",
@ -44,14 +44,13 @@ export default {
Current: "Actual",
Uptime: "Tiempo activo",
"Cert Exp.": "Caducidad cert.",
days: "días",
day: "día",
day: "día | días",
"-day": "-día",
hour: "hora",
"-hour": "-hora",
Response: "Respuesta",
Ping: "Ping",
"Monitor Type": "Tipo de Monitor",
"Monitor Type": "Tipo de monitor",
Keyword: "Palabra clave",
"Friendly Name": "Nombre sencillo",
URL: "URL",
@ -61,11 +60,11 @@ export default {
Retries: "Reintentos",
Advanced: "Avanzado",
"Upside Down Mode": "Modo invertido",
"Max. Redirects": "Redirecciones Máximas",
"Max. Redirects": "Redirecciones máximas",
"Accepted Status Codes": "Códigos de estado aceptados",
Save: "Guardar",
Notifications: "Notificaciones",
"Not available, please setup.": "No disponible, por favor configúrelo.",
"Not available, please setup.": "No disponible, por favor configúralo.",
"Setup Notification": "Configurar notificación",
Light: "Claro",
Dark: "Oscuro",
@ -83,8 +82,11 @@ export default {
"New Password": "Nueva contraseña",
"Repeat New Password": "Repetir nueva contraseña",
"Update Password": "Actualizar contraseña",
"Disable Auth": "Deshabilitar Autenticación",
"Enable Auth": "Habilitar Autenticación",
"Disable Auth": "Deshabilitar autenticación",
"Enable Auth": "Habilitar autenticación",
"disableauth.message1": "Seguro que deseas <strong>deshabilitar la autenticación</strong>?",
"disableauth.message2": "Es para <strong>quien implementa autenticación de terceros</strong> ante Uptime Kuma como por ejemplo Cloudflare Access.",
"Please use this option carefully!": "Por favor usar con cuidado.",
Logout: "Cerrar sesión",
Leave: "Salir",
"I understand, please disable": "Entiendo, por favor deshabilitar",
@ -102,32 +104,32 @@ export default {
Test: "Test",
"Certificate Info": "Información del certificado",
"Resolver Server": "Servidor de resolución",
"Resource Record Type": "Tipo de Registro",
"Resource Record Type": "Tipo de registro",
"Last Result": "Último resultado",
"Create your admin account": "Crea tu cuenta de administrador",
"Repeat Password": "Repetir contraseña",
respTime: "Tiempo de resp. (ms)",
notAvailableShort: "N/A",
Create: "Crear",
clearEventsMsg: "¿Está seguro de que desea eliminar todos los eventos de este monitor?",
clearHeartbeatsMsg: "¿Está seguro de que desea eliminar todos los latidos de este monitor?",
confirmClearStatisticsMsg: "¿Está seguro de que desea eliminar TODAS las estadísticas?",
"Clear Data": "Borrar Datos",
clearEventsMsg: "¿Estás seguro de que deseas eliminar todos los eventos de este monitor?",
clearHeartbeatsMsg: "¿Estás seguro de que deseas eliminar todos los latidos de este monitor?",
confirmClearStatisticsMsg: "¿Estás seguro de que deseas eliminar TODAS las estadísticas?",
"Clear Data": "Borrar datos",
Events: "Eventos",
Heartbeats: "Latidos",
"Auto Get": "Obtener automáticamente",
enableDefaultNotificationDescription: "Para cada nuevo monitor, esta notificación estará habilitada de forma predeterminada. Aún puede deshabilitar la notificación por separado para cada monitor.",
enableDefaultNotificationDescription: "Para cada nuevo monitor, esta notificación estará habilitada de forma predeterminada. Aún puedes deshabilitar la notificación por separado para cada monitor.",
"Default enabled": "Habilitado por defecto",
"Also apply to existing monitors": "También se aplica a monitores existentes",
Export: "Exportar",
Import: "Importar",
backupDescription: "Puede hacer una copia de seguridad de todos los monitores y todas las notificaciones en un archivo JSON.",
backupDescription: "Puedes hacer una copia de seguridad de todos los monitores y todas las notificaciones en un archivo JSON.",
backupDescription2: "PD: el historial y los datos de eventos no están incluidos.",
backupDescription3: "Los datos confidenciales, como los tokens de notificación, se incluyen en el archivo de exportación. Guárdelo con cuidado.",
alertNoFile: "Seleccione un archivo para importar.",
alertWrongFileType: "Seleccione un archivo JSON.",
twoFAVerifyLabel: "Ingrese su token para verificar que 2FA está funcionando",
tokenValidSettingsMsg: "¡El token es válido! Ahora puede guardar la configuración de 2FA.",
backupDescription3: "Los datos confidenciales, como los tokens de notificación, se incluyen en el archivo de exportación. Guárdalo con cuidado.",
alertNoFile: "Selecciona un archivo para importar.",
alertWrongFileType: "Selecciona un archivo JSON.",
twoFAVerifyLabel: "Ingresa tu token para verificar que 2FA está funcionando",
tokenValidSettingsMsg: "¡El token es válido! Ahora puedes guardar la configuración de 2FA.",
confirmEnableTwoFAMsg: "¿Estás seguro de que quieres habilitar 2FA?",
confirmDisableTwoFAMsg: "¿Estás seguro de que quieres desactivar 2FA?",
"Apply on all existing monitors": "Aplicar en todos los monitores existentes",
@ -143,19 +145,19 @@ export default {
"Show URI": "Mostrar URI",
"Clear all statistics": "Borrar todas las estadísticas",
retryCheckEverySecond: "Reintentar cada {0} segundo.",
importHandleDescription: "Elija 'Omitir existente' si desea omitir todos los monitores o notificaciones con el mismo nombre. 'Sobrescribir' eliminará todos los monitores y notificaciones existentes.",
confirmImportMsg: "¿Estás seguro de importar la copia de seguridad? Asegúrese de haber seleccionado la opción de importación correcta.",
importHandleDescription: "Elige 'Omitir existente' si deseas omitir todos los monitores o notificaciones con el mismo nombre. 'Sobrescribir' eliminará todos los monitores y notificaciones existentes.",
confirmImportMsg: "¿Estás seguro de importar la copia de seguridad? Asegúrate de haber seleccionado la opción de importación correcta.",
"Heartbeat Retry Interval": "Intervalo de reintento de latido",
"Import Backup": "Importar copia de seguridad",
"Export Backup": "Exportar copia de seguridad",
"Skip existing": "Omitir existente",
Overwrite: "Sobrescribir",
Options: "Opciones",
"Keep both": "Mantén ambos",
"Keep both": "Manténer ambos",
Tags: "Etiquetas",
"Add New below or Select...": "Agregar nuevo a continuación o Seleccionar...",
"Tag with this name already exist.": "La etiqueta con este nombre ya existe.",
"Tag with this value already exist.": "La etiqueta con este valor ya existe.",
"Add New below or Select...": "Agregar nuevo a continuación o seleccionar...",
"Tag with this name already exist.": "Una etiqueta con este nombre ya existe.",
"Tag with this value already exist.": "Una etiqueta con este valor ya existe.",
color: "color",
"value (optional)": "valor (opcional)",
Gray: "Gris",
@ -170,17 +172,17 @@ export default {
"Avg. Ping": "Ping promedio",
"Avg. Response": "Respuesta promedio",
"Entry Page": "Página de entrada",
statusPageNothing: "No hay nada aquí, agregue un grupo o un monitor.",
statusPageNothing: "No hay nada aquí, agrega un grupo o un monitor.",
"No Services": "Sin servicio",
"All Systems Operational": "Todos los sistemas están operativos",
"Partially Degraded Service": "Servicio parcialmente degradado",
"Degraded Service": "Servicio degradado",
"Add Group": "Agregar Grupo",
"Add Group": "Agregar grupo",
"Add a monitor": "Agregar un monitor",
"Edit Status Page": "Editar página de estado",
"Go to Dashboard": "Ir al panel de control",
"Status Page": "Página de estado",
"Status Pages": "Página de estado",
"Status Pages": "Páginas de estado",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",
@ -203,5 +205,5 @@ export default {
clearDataOlderThan: "Mantener los datos del historial del monitor durante {0} días.",
records: "registros",
"One record": "Un registro",
steamApiKeyDescription: "Para monitorear un servidor de juegos de Steam, necesita una clave Steam Web-API. Puede registrar su clave API aquí: ",
steamApiKeyDescription: "Para monitorear un servidor de juegos de Steam, necesitas una clave Steam Web-API. Puedes registrar tu clave API aquí: ",
};

@ -47,8 +47,7 @@ export default {
Current: "Hetkeseisund",
Uptime: "Eluiga",
"Cert Exp.": "Sert. aegumine",
days: "päeva",
day: "päev",
day: "päev | päeva",
"-day": "-päev",
hour: "tund",
"-hour": "-tund",
@ -88,6 +87,9 @@ export default {
"Update Password": "Uuenda salasõna",
"Disable Auth": "Lülita autentimine välja",
"Enable Auth": "Lülita autentimine sisse",
"disableauth.message1": "Kas soovid <strong>lülitada autentimise välja</strong>?",
"disableauth.message2": "Kastuamiseks <strong>välise autentimispakkujaga</strong>, näiteks Cloudflare Access.",
"Please use this option carefully!": "Palun kasuta vastutustundlikult.",
Logout: "Logi välja",
Leave: "Lahku",
"I understand, please disable": "Olen tutvunud riskidega, lülita välja",

@ -0,0 +1,539 @@
export default {
languageName: "Euskara",
checkEverySecond: "Egiaztatu {0} segunduro",
retryCheckEverySecond: "Errepikatu {0} segunduro",
retriesDescription: "Zerbitzua erorita markatu eta jakinarazpena bidali aurretik egindako saiakera kopuru maximoa",
ignoreTLSError: "Ezikusiarena egin TLS/SSL erroreei HTTPS webguneetan",
upsideDownModeDescription: "Alderantzizkatu erortze egoera. Zerbitzua martxan badago, ERORITA markatuko du.",
maxRedirectDescription: "Jarraitu beharreko berbideratze kopuru maximoa. Jarri 0 berbideratzeak desgaitzeko.",
acceptedStatusCodesDescription: "Hautatu erantzun ona kontsideratzen diren egoera kodeak.",
passwordNotMatchMsg: "Errepikatutako pasahitza ez dator bat.",
notificationDescription: "Jakinarazpenak monitorizazio funtzio bati asignatu behar zaizkio.",
keywordDescription: "Bilatu gako-hitza HTML edo JSON erantzunean. Bilaketan maiuskulak kontuan hartzen dira.",
pauseDashboardHome: "Gelditu",
deleteMonitorMsg: "Ziur zaude monitorizazio hau ezabatu nahi duzula?",
deleteNotificationMsg: "Ziur zaude jakinarazpen hau monitorizazio guztientzat ezabatu nahi duzula?",
dnsPortDescription: "DNS zerbitzari portua. Defektuz 53. Nahi duzunean aldatu dezakezu portua.",
resolverserverDescription: "Cloudflare zerbitzari lehenetsia da. Edozein unetan alda dezakezu ebazteko zerbitzaria.",
rrtypeDescription: "Hautatu kontrolatu nahi duzun RR mota",
enableDefaultNotificationDescription: "Jakinarazpen hau monitore berrientzat gaituko da defektuz. Baina monitorizazio bakoitzarentzat jakinarazpena desgaitu dezakezu.",
pauseMonitorMsg: "Ziur zaude gelditu egin nahi duzula?",
clearEventsMsg: "Ziur zaude monitorizazio honen gertaera guztiak ezabatu nahi dituzula?",
clearHeartbeatsMsg: "Ziur zaude monitorizazio honen pultsu guztiak ezabatu nahi dituzula?",
confirmClearStatisticsMsg: "Ziur zaude estatistika GUZTIAK ezabatu nahi dituzula?",
importHandleDescription: "Aukeratu 'existitzen bada', izen bereko monitore edo jakinarazpen bakoitza saltatu nahi baduzu. Lehendik dauden kontrol eta jakinarazpen guztiak ezabatuko ditu 'Gainidatzi' aukerak.",
confirmImportMsg: "Ziur zaude segurtasun-kopia inportatu nahi duzula? Egiaztatu inportatzeko aukera zuzena hautatu duzula.",
twoFAVerifyLabel: "Sartu zure tokena 2FA egiaztatzeko:",
tokenValidSettingsMsg: "Tokenak balio du! Orain 2FA konfigurazioa gorde dezakezu.",
confirmEnableTwoFAMsg: "Ziur zaude 2FA gaitu nahi duzula?",
confirmDisableTwoFAMsg: "Ziur zaude 2FA desgaitu nahi duzula?",
Settings: "Ezarpenak",
Dashboard: "Arbela",
"New Update": "Eguneraketa berria",
Language: "Hizkuntza",
Appearance: "Itxura",
Theme: "Gaia",
General: "Orokorra",
"Primary Base URL": "Oinarrizkoa URL",
Version: "Bertsioa",
"Check Update On GitHub": "Egiaztatu eguneraketa GitHuben",
List: "Zerrenda",
Add: "Gehitu",
"Add New Monitor": "Gehitu monitorizazio berria",
"Quick Stats": "Estatistika azkarrak",
Up: "Erabilgarri",
Down: "Erorita",
Pending: "Zain",
Unknown: "Ezezaguna",
Pause: "Gelditu",
Name: "Izena",
Status: "Egoera",
DateTime: "Data eta ordua",
Message: "Mezua",
"No important events": "Gertaera garrantzitsurik ez",
Resume: "Jarraitu",
Edit: "Editatu",
Delete: "Ezabatu",
Current: "Unekoa",
Uptime: "Martxan",
"Cert Exp.": "Ziurtagiri iraun.",
day: "egun | egun",
"-day": "-egun",
hour: "ordua",
"-hour": "-ordu",
Response: "Erantzuna",
Ping: "Ping",
"Monitor Type": "Monitorizazio mota",
Keyword: "Gakohitza",
"Friendly Name": "Izen xumea",
URL: "URLa",
Hostname: "Ostalari izena",
Port: "Portua",
"Heartbeat Interval": "Pultsu interbaloak",
Retries: "Errepikapenak",
"Heartbeat Retry Interval": "Pultsu errepikatze interbaloak",
Advanced: "Aurreratua",
"Upside Down Mode": "Alderantzizkako modua",
"Max. Redirects": "Berbideratze max.",
"Accepted Status Codes": "Onartutako egoera kodeak",
"Push URL": "Push URLa",
needPushEvery: "URL hau {0} segunduro deitu beharko zenuke.",
pushOptionalParams: "Hautazko parametroak: {0}",
Save: "Gorde",
Notifications: "Jakinarazpenak",
"Not available, please setup.": "Ez dago eskuragarri, ezarri mesedez.",
"Setup Notification": "Ezarri jakinarazpenak",
Light: "Argia",
Dark: "Iluna",
Auto: "Auto",
"Theme - Heartbeat Bar": "Gaia - Pultsu barra",
Normal: "Normala",
Bottom: "Behean",
None: "Bat ere ez",
Timezone: "Timezone",
"Search Engine Visibility": "Bilatzaile ikurgarritasuna",
"Allow indexing": "Onartu indexatzea",
"Discourage search engines from indexing site": "Discourage search engines from indexing site",
"Change Password": "Aldatu pasahitza",
"Current Password": "Uneko pasahitza",
"New Password": "Pasahitz berria",
"Repeat New Password": "Errepikatu pasahitz berria",
"Update Password": "Eguneratu pasahitza",
"Disable Auth": "Desgaitu Auth",
"Enable Auth": "Gaitu Auth",
"disableauth.message1": "Ziur zaude <strong>autentifikazioa desgaitu</strong> nahi duzula?",
"disableauth.message2": "Egoera jakin batzuetarako diseinatuta dago, Uptime Kumaren <strong>aurrean hirugarrengo autentifikazio batzuek jartzeko</strong> (Cloudflare Access, Authelia edo beste autentifikazio-mekanismo batzuk).",
"Please use this option carefully!": "Mesedez, kontuz erabili aukera hau!",
Logout: "Saioa amaitu",
Leave: "Utzi",
"I understand, please disable": "Ulertzen dut, mesedez desgaitu",
Confirm: "Baieztatu",
Yes: "Bai",
No: "Ez",
Username: "Erabiltzailea",
Password: "Pasahitza",
"Remember me": "Gogora nazazu",
Login: "Saioa hasi",
"No Monitors, please": "Monitorizaziorik ez, mesedez",
"add one": "gehitu bat",
"Notification Type": "Jakinarazpen mota",
Email: "Emaila",
Test: "Testa",
"Certificate Info": "Ziurtagiri informazioa",
"Resolver Server": "Ebazpen-zerbitzaria",
"Resource Record Type": "Baliabideen erregistro mota",
"Last Result": "Azken emaitza",
"Create your admin account": "Sortu zure admin kontua",
"Repeat Password": "Errepikatu pasahitza",
"Import Backup": "segurtasun-kopia inportatu",
"Export Backup": "segurtasun-kopia esportatu",
Export: "Esportatu",
Import: "Inportatu",
respTime: "Erantz. denbora (ms)",
notAvailableShort: "N/A",
"Default enabled": "Lehenetsia gaituta",
"Apply on all existing monitors": "Aplikatu existitzen diren monitorizazio guztietan",
Create: "Sortu",
"Clear Data": "Garbitu datuak",
Events: "Gertaerak",
Heartbeats: "Pultsuak",
"Auto Get": "Auto Get",
backupDescription: "Monitore eta jakinarazpen guztien segurtasun-kopiak egin ditzakezu JSON fitxategi batean.",
backupDescription2: "Oharra: ez dira historia eta gertaeren datuak sartzen.",
backupDescription3: "Datu sentikorrak, hala nola jakinarazpen tokenak, esportazio-fitxategian sartzen dira; mesedez, gorde esportazioa modu seguruan.",
alertNoFile: "Mesedez hautatu inportatzeko fitxategia.",
alertWrongFileType: "Mesedez hautatu JSON fitxategia.",
"Clear all statistics": "Garbitu estatistika guztiak",
"Skip existing": "Saltatu existitzen bada",
Overwrite: "Gainidatzi",
Options: "Aukerak",
"Keep both": "Biak mantendu",
"Verify Token": "Egiaztatu Tokena",
"Setup 2FA": "Ezarri 2FA",
"Enable 2FA": "Gaitu 2FA",
"Disable 2FA": "Desgaitu 2FA",
"2FA Settings": "2FA ezarpenak",
"Two Factor Authentication": "Bi aldetako autentifikazioa (2FA)",
Active: "Aktibo",
Inactive: "Inaktibo",
Token: "Tokena",
"Show URI": "Erakutsi URIa",
Tags: "Etiketak",
"Add New below or Select...": "Gehitu beste bat behean edo hautatu...",
"Tag with this name already exist.": "Izen hau duen etiketa dagoeneko badago.",
"Tag with this value already exist.": "Balio hau duen etiketa dagoeneko badago.",
color: "kolorea",
"value (optional)": "balioa (hautazkoa)",
Gray: "Grisa",
Red: "Gorria",
Orange: "Naranja",
Green: "Berdea",
Blue: "Urdina",
Indigo: "Indigo",
Purple: "Morea",
Pink: "Arrosa",
"Search...": "Bilatu...",
"Avg. Ping": "Batazbesteko Pinga",
"Avg. Response": "Batazbesteko erantzuna",
"Entry Page": "Sarrera orria",
statusPageNothing: "Ezer ere ez hemen, mesedez gehitu taldea edo monitorizazioa.",
"No Services": "Zerbitzurik ez",
"All Systems Operational": "Sistema guztiak martxan",
"Partially Degraded Service": "Zerbitzu partzialki degradatua",
"Degraded Service": "Zerbitzu degradatua",
"Add Group": "Gehitu taldea",
"Add a monitor": "Gehitu monitorizazioa",
"Edit Status Page": "Editatu egoera orria",
"Go to Dashboard": "Joan arbelera",
"Status Page": "Egoera orria",
"Status Pages": "Egoera orriak",
defaultNotificationName: "Nire {notification} Alerta ({number})",
here: "Hemen",
Required: "Beharrezkoa",
telegram: "Telegram",
"Bot Token": "Bot Tokena",
wayToGetTelegramToken: "You can get a token from {0}.",
"Chat ID": "Txat IDa",
supportTelegramChatID: "Support Direct Chat / Group / Channel's Chat ID",
wayToGetTelegramChatID: "You can get your chat ID by sending a message to the bot and going to this URL to view the chat_id:",
"YOUR BOT TOKEN HERE": "YOUR BOT TOKEN HERE",
chatIDNotFound: "Chat ID is not found; please send a message to this bot first",
webhook: "Webhook",
"Post URL": "Bidalketa URLa",
"Content Type": "Eduki mota",
webhookJsonDesc: "{0} is good for any modern HTTP servers such as Express.js",
webhookFormDataDesc: "{multipart} is good for PHP. The JSON will need to be parsed with {decodeFunction}",
smtp: "Emaila (SMTP)",
secureOptionNone: "Bat ere ez / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "Ignore TLS Error",
"From Email": "Email honetatik",
emailCustomSubject: "Pertsonalizatutako gaia",
"To Email": "Email honetara",
smtpCC: "CC",
smtpBCC: "BCC",
discord: "Discord",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "You can get this by going to Server Settings -> Integrations -> Create Webhook",
"Bot Display Name": "Bot Display Name",
"Prefix Custom Message": "Prefix Custom Message",
"Hello @everyone is...": "Hello {'@'}everyone is...",
teams: "Microsoft Teams",
"Webhook URL": "Webhook URL",
wayToGetTeamsURL: "You can learn how to create a webhook URL {0}.",
signal: "Signal",
Number: "Zenbakia",
Recipients: "Recipients",
needSignalAPI: "You need to have a signal client with REST API.",
wayToCheckSignalURL: "You can check this URL to view how to set one up:",
signalImportant: "IMPORTANT: You cannot mix groups and numbers in recipients!",
gotify: "Gotify",
"Application Token": "Aplikazio tokena",
"Server URL": "Zerbitzari URLa",
Priority: "Lehentasuna",
slack: "Slack",
"Icon Emoji": "Emoji ikonoa",
"Channel Name": "Kanalaren izena",
"Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "More info about Webhooks on: {0}",
aboutChannelName: "Enter the channel name on {0} Channel Name field if you want to bypass the Webhook channel. Ex: #other-channel",
aboutKumaURL: "If you leave the Uptime Kuma URL field blank, it will default to the Project GitHub page.",
emojiCheatSheet: "Emoji cheat sheet: {0}",
"rocket.chat": "Rocket.Chat",
pushover: "Pushover",
pushy: "Pushy",
PushByTechulus: "Push by Techulus",
octopush: "Octopush",
promosms: "PromoSMS",
clicksendsms: "ClickSend SMS",
lunasea: "LunaSea",
apprise: "Apprise (Support 50+ Notification services)",
GoogleChat: "Google Chat (Google Workspace only)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"User Key": "Erabiltzaile gakoa",
Device: "Gailua",
"Message Title": "Mezuaren izenburua",
"Notification Sound": "Jakinarazpen soinua",
"More info on:": "More info on: {0}",
pushoverDesc1: "Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.",
pushoverDesc2: "If you want to send notifications to different devices, fill out Device field.",
"SMS Type": "SMS mota",
octopushTypePremium: "Premium (Fast - recommended for alerting)",
octopushTypeLowCost: "Low Cost (Slow - sometimes blocked by operator)",
checkPrice: "Check {0} prices:",
apiCredentials: "API credentials",
octopushLegacyHint: "Do you use the legacy version of Octopush (2011-2020) or the new version?",
"Check octopush prices": "Check octopush prices {0}.",
octopushPhoneNumber: "Phone number (intl format, eg : +33612345678) ",
octopushSMSSender: "SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea Device ID",
"Apprise URL": "Apprise URL",
"Example:": "Adibidez: {0}",
"Read more:": "Irakurri gehiago: {0}",
"Status:": "Egoera: {0}",
"Read more": "Irakurri gehiago",
appriseInstalled: "Apprise instalatuta.",
appriseNotInstalled: "Apprise ez dago instalatuta. {0}",
"Access Token": "Access Token",
"Channel access token": "Channel access token",
"Line Developers Console": "Line Developers Console",
lineDevConsoleTo: "Line Developers Console - {0}",
"Basic Settings": "Oinarrizko ezarpenak",
"User ID": "Erabiltzaile ID",
"Messaging API": "Messaging 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.",
"Icon URL": "Ikono 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.",
aboutMattermostChannelName: "You can override the default channel that the Webhook posts to by entering the channel name into \"Channel Name\" field. This needs to be enabled in the Mattermost Webhook settings. Ex: #other-channel",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - cheap but slow and often overloaded. Limited only to Polish recipients.",
promosmsTypeFlash: "SMS FLASH - Message will automatically show on recipient device. Limited only to Polish recipients.",
promosmsTypeFull: "SMS FULL - Premium tier of SMS, You can use your Sender Name (You need to register name first). Reliable for alerts.",
promosmsTypeSpeed: "SMS SPEED - Highest priority in system. Very quick and reliable but costly (about twice of SMS FULL price).",
promosmsPhoneNumber: "Phone number (for Polish recipient You can skip area codes)",
promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "Hasiera zerbitzari URL (with http(s):// and optionally port)",
"Internal Room Id": "Internal Room ID",
matrixDesc1: "You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running {0}",
Method: "Metodoa",
Body: "Gorputza",
Headers: "Goiburuak",
PushUrl: "Push URL",
HeadersInvalidFormat: "The request headers are not valid JSON: ",
BodyInvalidFormat: "The request body is not valid JSON: ",
"Monitor History": "Monitorizazio Historia",
clearDataOlderThan: "Keep monitor history data for {0} days.",
PasswordsDoNotMatch: "Pasahitzak ez datoz bat.",
records: "records",
"One record": "One record",
steamApiKeyDescription: "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key here: ",
"Current User": "Uneko erabiltzailea",
topic: "Topic",
topicExplanation: "MQTT topic to monitor",
successMessage: "Arrakasta mezua",
successMessageExplanation: "MQTT message that will be considered as success",
recent: "Duela gutxikoa",
Done: "Egina",
Info: "Info",
Security: "Segurtasuna",
"Steam API Key": "Steam API Giltza",
"Shrink Database": "Shrink Datubasea",
"Pick a RR-Type...": "Pick a RR-Type...",
"Pick Accepted Status Codes...": "Hautatu onartutako egoera kodeak...",
Default: "Lehenetsia",
"HTTP Options": "HTTP Aukerak",
"Create Incident": "Sortu inzidentzia",
Title: "Titulua",
Content: "Edukia",
Style: "Estiloa",
info: "info",
warning: "kontuz",
danger: "arriskua",
error: "errorea",
critical: "kritikoa",
primary: "oinarrizkoa",
light: "argia",
dark: "iluna",
Post: "Post",
"Please input title and content": "Mesedez sartu titulua eta edukia",
Created: "Sortuta",
"Last Updated": "Azken eguneratzea",
Unpin: "Unpin",
"Switch to Light Theme": "Aldatu gai argira",
"Switch to Dark Theme": "Aldatu gai ilunera",
"Show Tags": "Erakutsi etiketak",
"Hide Tags": "Ezkutatu etiketak",
Description: "Deskribapena",
"No monitors available.": "Monitorizaziorik eskuragarri ez.",
"Add one": "Gehitu bat",
"No Monitors": "Monitorizaziorik ez",
"Untitled Group": "Titulurik gabeko taldea",
Services: "Zerbitzuak",
Discard: "Baztertu",
Cancel: "Ezeztatu",
"Powered by": "Honekin egina:",
shrinkDatabaseDescription: "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API erabiltzailea (webapi_ aurre-hizkia barne)",
serwersmsAPIPassword: "API pasahitza",
serwersmsPhoneNumber: "Telefono zenbakia",
serwersmsSenderName: "SMS bidaltzaile izena (registered via customer portal)",
stackfield: "Stackfield",
Customize: "Pertsonalizatu",
"Custom Footer": "Oin pertsonalizatua",
"Custom CSS": "CSS pertsonalizatua",
smtpDkimSettings: "DKIM ezarpenak",
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
documentation: "dokumentazioa",
smtpDkimDomain: "Domeinu izena",
smtpDkimKeySelector: "Gako hautatzailea",
smtpDkimPrivateKey: "Gako pribatua",
smtpDkimHashAlgo: "Hash algoritmoa (hautazkoa)",
smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
smtpDkimskipFields: "Header Keys not to sign (Optional)",
wayToGetPagerDutyKey: "You can get this by going to Service -> Service Directory -> (Select a service) -> Integrations -> Add integration. Here you can search for \"Events API V2\". More info {0}",
"Integration Key": "Integration Key",
"Integration URL": "Integrazio URLa",
"Auto resolve or acknowledged": "Auto resolve or acknowledged",
"do nothing": "ez egin ezer",
"auto acknowledged": "auto acknowledged",
"auto resolve": "auto resolve",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "Ingurunea",
alertaApiKey: "API Key",
alertaAlertState: "Alerta egoera",
alertaRecoverState: "Berreskuratze egoera",
deleteStatusPageMsg: "Ziur zaude egoera orri hau ezabatu nahi duzula?",
Proxies: "Proxiak",
default: "Lehenetsia",
enabled: "Gaituta",
setAsDefault: "Ezarri lehenetsitzat",
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: "Baliozkoa",
Invalid: "Baliogabea",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "TelefonoZenbakiak",
TemplateCode: "TemplateCode",
SignName: "SignName",
"Sms template must contain parameters: ": "Sms txantiloiak parametroak eduki behar ditu: ",
"Bark Endpoint": "Bark Endpoint",
WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey",
"For safety, must use secret key": "For safety, must use secret key",
"Device Token": "Gailu tokena",
Platform: "Plataforma",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "Altua",
Retry: "Errepikatu",
Topic: "Gaia",
"WeCom Bot Key": "WeCom Bot Key",
"Setup Proxy": "Ezarri Proxya",
"Proxy Protocol": "Proxy protokoloa",
"Proxy Server": "Proxy zerbitzaria",
"Proxy server has authentication": "Proxy zerbitzariak autentifikazioa dauka",
User: "Erabiltzailea",
Installed: "Instalatuta",
"Not installed": "Instalatu gabe",
Running: "Martxan",
"Not running": "Ez martxan",
"Remove Token": "Ezabatu Tokena",
Start: "Hasi",
Stop: "Gelditu",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "Gehitu egoera orri berria",
Slug: "Sluga",
"Accept characters:": "Onartu karaktereak:",
startOrEndWithOnly: "Start or end with {0} only",
"No consecutive dashes": "No consecutive dashes",
Next: "Hurrengoa",
"The slug is already taken. Please choose another slug.": "Sluga dagoeneko hartuta dago. Mesedez beste bat hautatu.",
"No Proxy": "Proxyrik ez",
Authentication: "Authentication",
"HTTP Basic Auth": "HTTP oinarrizko Auth",
"New Status Page": "Egoera orri berria",
"Page Not Found": "Orria ez da aurkitu",
"Reverse Proxy": "Alderantzizkako Proxya",
Backup: "Backup",
About: "Honi buruz",
wayToGetCloudflaredURL: "(Download cloudflared from {0})",
cloudflareWebsite: "Cloudflare webgunea",
"Message:": "Mezua:",
"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": "Beste softwarea",
"For example: nginx, Apache and Traefik.": "Adibidez: nginx, Apache and Traefik.",
"Please read": "Mesedez irakurri",
"Subject:": "Gaia:",
"Valid To:": "Balio-epea:",
"Days Remaining:": "Egun faltan:",
"Issuer:": "Issuer:",
"Fingerprint:": "Hatzmarka:",
"No status pages": "Egoera orririk ez",
"Domain Name Expiry Notification": "Domeinu izen iraungitze jakinarazpena",
Proxy: "Proxya",
"Date Created": "Data sortuta",
onebotHttpAddress: "OneBot HTTP helbidea",
onebotMessageType: "OneBot mezu mota",
onebotGroupMessage: "Taldea",
onebotPrivateMessage: "Pribatua",
onebotUserOrGroupId: "Talde/Erabiltzaile IDa",
onebotSafetyTips: "For safety, must set access token",
"PushDeer Key": "PushDeer Key",
"Footer Text": "Oineko testua",
"Show Powered By": "Erakutsi Honekin egina:",
"Domain Names": "Domeinu izenak",
signedInDisp: "Signed in as {0}",
signedInDispDisabled: "Auth desgaituta.",
"Certificate Expiry Notification": "Zertifikatu iraungitze jakinarazpena",
"API Username": "API Erabiltzailea",
"API Key": "API Gakoa",
"Recipient Number": "Recipient Number",
"From Name/Number": "From Name/Number",
"Leave blank to use a shared sender number.": "Leave blank to use a shared sender number.",
"Octopush API Version": "Octopush API Version",
"Legacy Octopush-DM": "Legacy Octopush-DM",
endpoint: "endpoint",
octopushAPIKey: "\"API key\" from HTTP API credentials in control panel",
octopushLogin: "\"Login\" from HTTP API credentials in control panel",
promosmsLogin: "API Saio haste izena",
promosmsPassword: "API Pasahitza",
"pushoversounds pushover": "Pushover (defektuz)",
"pushoversounds bike": "Bizikleta",
"pushoversounds bugle": "Bugle",
"pushoversounds cashregister": "Cash Register",
"pushoversounds classical": "Klasikoa",
"pushoversounds cosmic": "Kosmikoa",
"pushoversounds falling": "Erortzen",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Incoming",
"pushoversounds intermission": "Intermission",
"pushoversounds magic": "Magia",
"pushoversounds mechanical": "Mekanikoa",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Sirena",
"pushoversounds spacealarm": "Espazio Alarma",
"pushoversounds tugboat": "Tug Boat",
"pushoversounds alien": "Alien Alarm (long)",
"pushoversounds climb": "Climb (long)",
"pushoversounds persistent": "Persistent (long)",
"pushoversounds echo": "Pushover Echo (long)",
"pushoversounds updown": "Up Down (long)",
"pushoversounds vibrate": "Bibrazioa soilik",
"pushoversounds none": "Bat ere ez (isilik)",
pushyAPIKey: "Secret API giltza",
pushyToken: "Gailu tokena",
"Show update if available": "Erakutsi eguneratzea eskuragarri badago",
"Also check beta release": "Beta bertsioak ere egiaztatu",
"Using a Reverse Proxy?": "Proxy alderantzizkako zerbitzaria erabiltzen?",
"Check how to config it for WebSocket": "Check how to config it for WebSocket",
"Steam Game Server": "Steam joko zerbitzaria",
"Most likely causes:": "Arrazoi probableenak:",
"The resource is no longer available.": "Baliabidea ez dago erabilgarri.",
"There might be a typing error in the address.": "Idazketa-akats bat egon daiteke helbidean.",
"What you can try:": "Probatu dezakezuna:",
"Retype the address.": "Berridatzi helbidea.",
"Go back to the previous page.": "Itzuli aurreko orrialdera",
"Coming Soon": "Laster",
wayToGetClickSendSMSToken: "API erabiltzailea and API giltza hemendik lortu ditzakezu: {0} .",
"Connection String": "Konexio katea",
Query: "Kontsulta",
settingsCertificateExpiry: "TLS irungitze zertifikatua",
certificationExpiryDescription: "HTTPS Monitorizazio jakinarazpena martxan jarri TLS zertifikatua iraungitzeko hau falta denean:",
"ntfy Topic": "ntfy Topic",
Domain: "Domeinua",
Workstation: "Lan gunea",
disableCloudflaredNoAuthMsg: "Ez Auth moduan zaude, pasahitza ez da beharrezkoa.",
};

@ -55,7 +55,6 @@ export default {
Current: "فعلی",
Uptime: "آپتایم",
"Cert Exp.": "تاریخ انقضای SSL",
days: "روز",
day: "روز",
"-day": "-روز",
hour: "ساعت",
@ -97,6 +96,9 @@ export default {
"Update Password": "بروز رسانی رمز عبور",
"Disable Auth": "غیر فعال سازی تایید هویت",
"Enable Auth": "فعال سازی تایید هویت",
"disableauth.message1": "آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?",
"disableauth.message2": "این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کرده‌اند</strong>، مانند Cloudflare Access.",
"Please use this option carefully!": "لطفا از این امکان با دقت استفاده کنید.",
Logout: "خروج",
Leave: "منصرف شدم",
"I understand, please disable": "متوجه هستم، لطفا غیرفعال کنید!",

@ -55,8 +55,7 @@ export default {
Current: "Actuellement",
Uptime: "Uptime",
"Cert Exp.": "Expiration SSL",
days: "jours",
day: "jour",
day: "jour | jours",
"-day": "-jours",
hour: "-heure",
"-hour": "-heures",

@ -56,8 +56,7 @@ export default {
Current: "Trenutno",
Uptime: "Dostupnost",
"Cert Exp.": "Istek cert.",
days: "dana",
day: "dan",
day: "dan | dana",
"-day": "-dnevno",
hour: "sat",
"-hour": "-satno",
@ -101,6 +100,9 @@ export default {
"Update Password": "Spremi novu lozinku",
"Disable Auth": "Onemogući autentikaciju",
"Enable Auth": "Omogući autentikaciju",
"disableauth.message1": "Jeste li sigurni da želite <strong>isključiti autentikaciju</strong>?",
"disableauth.message2": "To je za <strong>korisnike koji imaju vanjsku autentikaciju stranice</strong> ispred Uptime Kume, poput usluge Cloudflare Access.",
"Please use this option carefully!": "Pažljivo koristite ovu opciju.",
Logout: "Odjava",
Leave: "Poništi",
"I understand, please disable": "Razumijem, svejedno onemogući",

@ -55,7 +55,6 @@ export default {
Current: "Aktuális",
Uptime: "Uptime",
"Cert Exp.": "SSL lejárat",
days: "nap",
day: "nap",
"-day": " nap",
hour: "óra",
@ -97,6 +96,9 @@ export default {
"Update Password": "Jelszó módosítása",
"Disable Auth": "Hitelesítés tiltása",
"Enable Auth": "Hitelesítés engedélyezése",
"disableauth.message1": "Biztos benne, hogy <strong>kikapcsolja a hitelesítést</strong>?",
"disableauth.message2": "Akkor érdemes, ha <strong>van 3rd-party hitelesítés</strong> az Uptime Kuma-t megelőzően mint a Cloudflare Access.",
"Please use this option carefully!": "Használja megfontoltan!",
Logout: "Kijelentkezés",
Leave: "Elhagy",
"I understand, please disable": "Megértettem, kérem tiltsa le",

@ -55,8 +55,7 @@ export default {
Current: "Saat ini",
Uptime: "Waktu aktif",
"Cert Exp.": "Cert Exp.",
days: "hari-hari",
day: "hari",
day: "hari | hari-hari",
"-day": "-hari",
hour: "Jam",
"-hour": "-Jam",
@ -97,6 +96,9 @@ export default {
"Update Password": "Perbarui Kata Sandi",
"Disable Auth": "Nonaktifkan Autentikasi",
"Enable Auth": "Aktifkan Autentikasi",
"disableauth.message1": "Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>?",
"disableauth.message2": "Ini untuk <strong>mereka yang memiliki autentikasi pihak ketiga</strong> diletakkan di depan Uptime Kuma, misalnya akses Cloudflare.",
"Please use this option carefully!": "Gunakan dengan hati-hati.",
Logout: "Keluar",
Leave: "Pergi",
"I understand, please disable": "Saya mengerti, silakan dinonaktifkan",

@ -56,8 +56,7 @@ export default {
Current: "Corrente",
Uptime: "Tempo di attività",
"Cert Exp.": "Scadenza certificato",
days: "giorni",
day: "giorno",
day: "giorno | giorni",
"-day": "-giorni",
hour: "ora",
"-hour": "-ore",
@ -101,6 +100,9 @@ export default {
"Update Password": "Modifica password",
"Disable Auth": "Disabilita autenticazione",
"Enable Auth": "Abilita autenticazione",
"disableauth.message1": "<strong>Disabilitare l'autenticazione?</strong>",
"disableauth.message2": "<strong>Questa opzione è per chi un sistema di autenticazione gestito da terze parti</strong> messo davanti ad Uptime Kuma, ad esempio Cloudflare Access.",
"Please use this option carefully!": "Utilizzare con attenzione!",
Logout: "Esci",
Leave: "Annulla",
"I understand, please disable": "Lo capisco, disabilitare l'autenticazione.",

@ -44,8 +44,7 @@ export default {
Current: "現在",
Uptime: "起動時間",
"Cert Exp.": "証明書有効期限",
days: "日間",
day: "日",
day: "日 | 日間",
"-day": "-日",
hour: "時間",
"-hour": "-時間",

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

Loading…
Cancel
Save