Merge remote-tracking branch 'origin/master' into ntml-auth

# Conflicts:
#	package-lock.json
#	package.json
#	server/database.js
#	server/model/monitor.js
#	server/server.js
#	server/util-server.js
pull/1639/head
Louis Lam 3 years ago
commit 6682839ec8

@ -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 # Description
Fixes #(issue) Fixes #(issue)

@ -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? ## 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: ✅ Accept:
- Bug/Security fix - Bug/Security fix
- Translations - Translations
- Adding notification providers - Adding notification providers
⚠️ Discuss First ⚠️ Discussion First
- Large pull requests - Large pull requests
- New features - 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 ### Recommended Pull Request Guideline
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended. 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. Click "Change to draft"
1. Discussion 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 ## 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. 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 - 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 - 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 - Easy to use
- The web UI styling should be consistent and nice.
## Coding Styles ## Coding Styles

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

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

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

4414
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.15.1", "version": "1.16.1",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "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-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", "build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain", "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.15.1 && npm ci --production && npm run download-dist", "setup": "git checkout 1.16.1 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js", "download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js", "mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js", "reset-password": "node extra/reset-password.js",
@ -57,7 +57,8 @@
"ncu-patch": "npm-check-updates -u -t patch", "ncu-patch": "npm-check-updates -u -t patch",
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js", "release-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", "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": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "~1.2.36", "@fortawesome/fontawesome-svg-core": "~1.2.36",
@ -68,6 +69,7 @@
"@popperjs/core": "~2.10.2", "@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0", "args-parser": "~1.3.0",
"axios": "~0.26.1", "axios": "~0.26.1",
"axios-cached-dns-resolve": "^3.0.6",
"axios-ntlm": "^1.3.0", "axios-ntlm": "^1.3.0",
"badge-maker": "^3.3.1", "badge-maker": "^3.3.1",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
@ -77,12 +79,16 @@
"chart.js": "~3.6.2", "chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0", "chartjs-adapter-dayjs": "~1.0.0",
"check-password-strength": "^2.0.5", "check-password-strength": "^2.0.5",
"cheerio": "^1.0.0-rc.10",
"chroma-js": "^2.1.2", "chroma-js": "^2.1.2",
"command-exists": "~1.2.9", "command-exists": "~1.2.9",
"compare-versions": "~3.6.0", "compare-versions": "~3.6.0",
"dayjs": "~1.10.8", "compression": "^1.7.4",
"dayjs": "^1.11.0",
"esm-wallaby": "^3.2.26",
"express": "~4.17.3", "express": "~4.17.3",
"express-basic-auth": "~1.2.1", "express-basic-auth": "~1.2.1",
"express-static-gzip": "^2.1.7",
"favico.js": "^0.3.10", "favico.js": "^0.3.10",
"form-data": "~4.0.0", "form-data": "~4.0.0",
"http-graceful-shutdown": "~3.1.7", "http-graceful-shutdown": "~3.1.7",
@ -93,6 +99,7 @@
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"limiter": "^2.1.0", "limiter": "^2.1.0",
"mqtt": "^4.2.8", "mqtt": "^4.2.8",
"mssql": "^8.1.0",
"node-cloudflared-tunnel": "~1.0.9", "node-cloudflared-tunnel": "~1.0.9",
"nodemailer": "~6.6.5", "nodemailer": "~6.6.5",
"notp": "~2.0.3", "notp": "~2.0.3",
@ -103,7 +110,7 @@
"prom-client": "~13.2.0", "prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1", "prometheus-api-metrics": "~3.2.1",
"qrcode": "~1.5.0", "qrcode": "~1.5.0",
"redbean-node": "0.1.3", "redbean-node": "0.1.4",
"socket.io": "~4.4.1", "socket.io": "~4.4.1",
"socket.io-client": "~4.4.1", "socket.io-client": "~4.4.1",
"socks-proxy-agent": "^6.1.1", "socks-proxy-agent": "^6.1.1",
@ -130,27 +137,31 @@
"@babel/eslint-parser": "~7.17.0", "@babel/eslint-parser": "~7.17.0",
"@babel/preset-env": "^7.15.8", "@babel/preset-env": "^7.15.8",
"@types/bootstrap": "~5.1.9", "@types/bootstrap": "~5.1.9",
"@vitejs/plugin-legacy": "~1.6.4", "@vitejs/plugin-legacy": "~1.8.2",
"@vitejs/plugin-vue": "~1.9.4", "@vitejs/plugin-vue": "~2.3.3",
"@vue/compiler-sfc": "~3.2.31", "@vue/compiler-sfc": "~3.2.36",
"aedes": "^0.46.3", "aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0", "babel-plugin-rewire": "~1.2.0",
"concurrently": "^7.1.0", "concurrently": "^7.1.0",
"core-js": "~3.18.3", "core-js": "~3.18.3",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
"delay": "^5.0.0",
"dns2": "~2.0.1", "dns2": "~2.0.1",
"eslint": "~8.14.0", "eslint": "~8.14.0",
"eslint-plugin-vue": "~8.7.1", "eslint-plugin-vue": "~8.7.1",
"jest": "~27.2.5", "jest": "~27.2.5",
"jest-puppeteer": "~6.0.3", "jest-puppeteer": "~6.0.3",
"lru-cache": "^7.7.1",
"npm-check-updates": "^12.5.9", "npm-check-updates": "^12.5.9",
"postcss-html": "^1.3.1", "postcss-html": "^1.3.1",
"puppeteer": "~13.1.3", "puppeteer": "~13.1.3",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "~1.42.1", "sass": "~1.42.1",
"stylelint": "~14.7.1", "stylelint": "~14.7.1",
"stylelint-config-standard": "~25.0.0", "stylelint-config-standard": "~25.0.0",
"typescript": "~4.4.4", "typescript": "~4.4.4",
"vite": "~2.6.14", "vite": "~2.9.9",
"vite-plugin-compression": "^0.5.1",
"wait-on": "^6.0.1" "wait-on": "^6.0.1"
} }
} }

@ -58,6 +58,7 @@ class Database {
"patch-monitor-expiry-notification.sql": true, "patch-monitor-expiry-notification.sql": true,
"patch-status-page-footer-css.sql": true, "patch-status-page-footer-css.sql": true,
"patch-added-mqtt-monitor.sql": true, "patch-added-mqtt-monitor.sql": true,
"patch-add-sqlserver-monitor.sql": true,
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] }, "patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
}; };

@ -7,7 +7,7 @@ dayjs.extend(timezone);
const axios = require("axios"); const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mqttAsync, httpNtlm } = require("../util-server"); const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, mqttAsync, setSetting, httpNtlm } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification"); const { Notification } = require("../notification");
@ -17,6 +17,12 @@ const version = require("../../package.json").version;
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server"); const { UptimeKumaServer } = require("../uptime-kuma-server");
const axiosCachedDnsResolve = require("esm-wallaby")(module)("axios-cached-dns-resolve");
// create an axios client instance with the cached DNS resolve interceptor
const axiosClient = axios.create();
axiosCachedDnsResolve.registerInterceptor(axiosClient);
/** /**
* status: * status:
* 0 = DOWN * 0 = DOWN
@ -88,6 +94,8 @@ class Monitor extends BeanModel {
mqttPassword: this.mqttPassword, mqttPassword: this.mqttPassword,
mqttTopic: this.mqttTopic, mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage, mqttSuccessMessage: this.mqttSuccessMessage,
databaseConnectionString: this.databaseConnectionString,
databaseQuery: this.databaseQuery,
authMethod: this.authMethod, authMethod: this.authMethod,
authWorkstation: this.authWorkstation, authWorkstation: this.authWorkstation,
authDomain: this.authDomain, authDomain: this.authDomain,
@ -185,7 +193,7 @@ class Monitor extends BeanModel {
// undefined if not https // undefined if not https
let tlsInfo = undefined; let tlsInfo = undefined;
if (!previousBeat) { if (!previousBeat || this.type === "push") {
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
this.id, this.id,
]); ]);
@ -195,7 +203,7 @@ class Monitor extends BeanModel {
let bean = R.dispense("heartbeat"); let bean = R.dispense("heartbeat");
bean.monitor_id = this.id; bean.monitor_id = this.id;
bean.time = R.isoDateTime(dayjs.utc()); bean.time = R.isoDateTimeMillis(dayjs.utc());
bean.status = DOWN; bean.status = DOWN;
if (this.isUpsideDown()) { if (this.isUpsideDown()) {
@ -279,7 +287,7 @@ class Monitor extends BeanModel {
}); });
} else { } else {
res = await axios.request(options); res = await axiosClient.request(options);
} }
bean.msg = `${res.status} - ${res.statusText}`; bean.msg = `${res.status} - ${res.statusText}`;
@ -329,7 +337,11 @@ class Monitor extends BeanModel {
bean.msg += ", keyword is found"; bean.msg += ", keyword is found";
bean.status = UP; bean.status = UP;
} else { } 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 + "]");
} }
} }
@ -347,7 +359,7 @@ class Monitor extends BeanModel {
let startTime = dayjs().valueOf(); let startTime = dayjs().valueOf();
let dnsMessage = ""; 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; bean.ping = dayjs().valueOf() - startTime;
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") { if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") {
@ -384,25 +396,33 @@ class Monitor extends BeanModel {
bean.msg = dnsMessage; bean.msg = dnsMessage;
bean.status = UP; bean.status = UP;
} else if (this.type === "push") { // Type: Push } 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 > ? ", [ if (previousBeat) {
this.id, const msSinceLastBeat = dayjs.utc().valueOf() - dayjs.utc(previousBeat.time).valueOf();
time
]);
log.debug("monitor", "heartbeatCount" + heartbeatCount + " " + time); log.debug("monitor", `[${this.name}] msSinceLastBeat = ${msSinceLastBeat}`);
if (heartbeatCount <= 0) { // If the previous beat was down or pending we use the regular
// Fix #922, since previous heartbeat could be inserted by api, it should get from database // beatInterval/retryInterval in the setTimeout further below
previousBeat = await Monitor.getPreviousHeartbeat(this.id); if (previousBeat.status !== UP || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
throw new Error("No heartbeat in the time window");
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 { } else {
// No need to insert successful heartbeat for push type, so end here throw new Error("No heartbeat in the time window");
retries = 0;
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
return;
} }
} else if (this.type === "steam") { } else if (this.type === "steam") {
@ -414,7 +434,7 @@ class Monitor extends BeanModel {
throw new Error("Steam API Key not found"); throw new Error("Steam API Key not found");
} }
let res = await axios.get(steamApiUrl, { let res = await axiosClient.get(steamApiUrl, {
timeout: this.interval * 1000 * 0.8, timeout: this.interval * 1000 * 0.8,
headers: { headers: {
"Accept": "*/*", "Accept": "*/*",
@ -452,6 +472,14 @@ class Monitor extends BeanModel {
interval: this.interval, interval: this.interval,
}); });
bean.status = UP; 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 { } else {
bean.msg = "Unknown Monitor Type"; bean.msg = "Unknown Monitor Type";
bean.status = PENDING; bean.status = PENDING;
@ -502,7 +530,7 @@ class Monitor extends BeanModel {
} }
if (bean.status === UP) { 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) { } else if (bean.status === PENDING) {
if (this.retryInterval > 0) { if (this.retryInterval > 0) {
beatInterval = this.retryInterval; beatInterval = this.retryInterval;
@ -846,10 +874,19 @@ class Monitor extends BeanModel {
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) { if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
const notificationList = await Monitor.getNotificationList(this); const notificationList = await Monitor.getNotificationList(this);
log.debug("monitor", "call sendCertNotificationByTargetDays"); let notifyDays = await setting("tlsExpiryNotifyDays");
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList); if (notifyDays == null || !Array.isArray(notifyDays)) {
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList); // Reset Default
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList); 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,104 @@
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const cheerio = require("cheerio");
const { UptimeKumaServer } = require("../uptime-kuma-server");
class StatusPage extends BeanModel { class StatusPage extends BeanModel {
/**
* Like this: { "test-uptime.kuma.pet": "default" }
* @type {{}}
*/
static domainMappingList = { }; 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");
}
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>
`);
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 * Loads domain mapping from DB
* Return object like this: { "test-uptime.kuma.pet": "default" } * Return object like this: { "test-uptime.kuma.pet": "default" }

@ -22,16 +22,23 @@ class Discord extends NotificationProvider {
return okMsg; return okMsg;
} }
let url; let address;
if (monitorJSON["type"] === "port") { switch (monitorJSON["type"]) {
url = monitorJSON["hostname"]; case "ping":
if (monitorJSON["port"]) { address = monitorJSON["hostname"];
url += ":" + monitorJSON["port"]; break;
} case "port":
case "dns":
} else { case "steam":
url = monitorJSON["url"]; address = monitorJSON["hostname"];
if (monitorJSON["port"]) {
address += ":" + monitorJSON["port"];
}
break;
default:
address = monitorJSON["url"];
break;
} }
// If heartbeatJSON is not null, we go into the normal alerting loop. // If heartbeatJSON is not null, we go into the normal alerting loop.
@ -48,8 +55,8 @@ class Discord extends NotificationProvider {
value: monitorJSON["name"], value: monitorJSON["name"],
}, },
{ {
name: "Service URL", name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: url, value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
}, },
{ {
name: "Time (UTC)", name: "Time (UTC)",
@ -83,8 +90,8 @@ class Discord extends NotificationProvider {
value: monitorJSON["name"], value: monitorJSON["name"],
}, },
{ {
name: "Service URL", name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url, value: monitorJSON["type"] === "push" ? "Heartbeat" : address.startsWith("http") ? "[Visit Service](" + address + ")" : address,
}, },
{ {
name: "Time (UTC)", name: "Time (UTC)",
@ -92,7 +99,7 @@ class Discord extends NotificationProvider {
}, },
{ {
name: "Ping", name: "Ping",
value: heartbeatJSON["ping"] + "ms", value: heartbeatJSON["ping"] == null ? "N/A" : heartbeatJSON["ping"] + " ms",
}, },
], ],
}], }],

@ -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;

@ -2,6 +2,7 @@ const { R } = require("redbean-node");
const Apprise = require("./notification-providers/apprise"); const Apprise = require("./notification-providers/apprise");
const Discord = require("./notification-providers/discord"); const Discord = require("./notification-providers/discord");
const Gotify = require("./notification-providers/gotify"); const Gotify = require("./notification-providers/gotify");
const Ntfy = require("./notification-providers/ntfy");
const Line = require("./notification-providers/line"); const Line = require("./notification-providers/line");
const LunaSea = require("./notification-providers/lunasea"); const LunaSea = require("./notification-providers/lunasea");
const Mattermost = require("./notification-providers/mattermost"); const Mattermost = require("./notification-providers/mattermost");
@ -29,6 +30,7 @@ const SerwerSMS = require("./notification-providers/serwersms");
const Stackfield = require("./notification-providers/stackfield"); const Stackfield = require("./notification-providers/stackfield");
const WeCom = require("./notification-providers/wecom"); const WeCom = require("./notification-providers/wecom");
const GoogleChat = require("./notification-providers/google-chat"); const GoogleChat = require("./notification-providers/google-chat");
const PagerDuty = require("./notification-providers/pagerduty");
const Gorush = require("./notification-providers/gorush"); const Gorush = require("./notification-providers/gorush");
const Alerta = require("./notification-providers/alerta"); const Alerta = require("./notification-providers/alerta");
const OneBot = require("./notification-providers/onebot"); const OneBot = require("./notification-providers/onebot");
@ -51,6 +53,7 @@ class Notification {
new Discord(), new Discord(),
new Teams(), new Teams(),
new Gotify(), new Gotify(),
new Ntfy(),
new Line(), new Line(),
new LunaSea(), new LunaSea(),
new Feishu(), new Feishu(),
@ -74,6 +77,7 @@ class Notification {
new Stackfield(), new Stackfield(),
new WeCom(), new WeCom(),
new GoogleChat(), new GoogleChat(),
new PagerDuty(),
new Gorush(), new Gorush(),
new Alerta(), new Alerta(),
new OneBot(), new OneBot(),

@ -1,5 +1,5 @@
let express = require("express"); 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 { R } = require("redbean-node");
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor"); const Monitor = require("../model/monitor");
@ -59,7 +59,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
let duration = 0; let duration = 0;
let bean = R.dispense("heartbeat"); let bean = R.dispense("heartbeat");
bean.time = R.isoDateTime(dayjs.utc()); bean.time = R.isoDateTimeMillis(dayjs.utc());
if (previousHeartbeat) { if (previousHeartbeat) {
isFirstBeat = false; isFirstBeat = false;
@ -67,6 +67,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second"); 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", "PreviousStatus: " + previousStatus);
log.debug("router", "Current Status: " + status); log.debug("router", "Current Status: " + status);
@ -91,115 +92,13 @@ router.get("/api/push/:pushToken", async (request, response) => {
} }
} catch (e) { } catch (e) {
response.json({ response.status(404).json({
ok: false, ok: false,
msg: e.message 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) => { router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => {
allowAllOrigin(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; module.exports = router;

@ -0,0 +1,110 @@
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);
}
});
module.exports = router;

@ -16,7 +16,7 @@ if (nodeVersion < requiredVersion) {
} }
const args = require("args-parser")(process.argv); 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"); const config = require("./config");
log.info("server", "Welcome to Uptime Kuma"); log.info("server", "Welcome to Uptime Kuma");
@ -35,6 +35,7 @@ const fs = require("fs");
log.info("server", "Importing 3rd-party libraries"); log.info("server", "Importing 3rd-party libraries");
log.debug("server", "Importing express"); log.debug("server", "Importing express");
const express = require("express"); const express = require("express");
const expressStaticGzip = require("express-static-gzip");
log.debug("server", "Importing redbean-node"); log.debug("server", "Importing redbean-node");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
log.debug("server", "Importing jsonwebtoken"); log.debug("server", "Importing jsonwebtoken");
@ -148,22 +149,6 @@ let jwtSecret = null;
*/ */
let needSetup = false; 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 () => { (async () => {
Database.init(args); Database.init(args);
await initDatabase(testMode); await initDatabase(testMode);
@ -179,13 +164,17 @@ try {
// Entry Page // Entry Page
app.get("/", async (request, response) => { app.get("/", async (request, response) => {
debug(`Request Domain: ${request.hostname}`); log.debug("entry", `Request Domain: ${request.hostname}`);
if (request.hostname in StatusPage.domainMappingList) { if (request.hostname in StatusPage.domainMappingList) {
debug("This is a status page domain"); log.debug("entry", "This is a status page domain");
response.send(indexHTML);
let slug = StatusPage.domainMappingList[request.hostname];
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) { } else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
response.redirect("/status/" + exports.entryPage.replace("statusPage-", "")); response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
} else { } else {
response.redirect("/dashboard"); response.redirect("/dashboard");
} }
@ -214,7 +203,9 @@ try {
// With Basic Auth using the first user's username/password // With Basic Auth using the first user's username/password
app.get("/metrics", basicAuth, prometheusAPIMetrics()); app.get("/metrics", basicAuth, prometheusAPIMetrics());
app.use("/", express.static("dist")); app.use("/", expressStaticGzip("dist", {
enableBrotli: true,
}));
// ./data/upload // ./data/upload
app.use("/upload", express.static(Database.uploadDir)); app.use("/upload", express.static(Database.uploadDir));
@ -227,12 +218,16 @@ try {
const apiRouter = require("./routers/api-router"); const apiRouter = require("./routers/api-router");
app.use(apiRouter); 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. // Universal Route Handler, must be at the end of all express routes.
app.get("*", async (_request, response) => { app.get("*", async (_request, response) => {
if (_request.originalUrl.startsWith("/upload/")) { if (_request.originalUrl.startsWith("/upload/")) {
response.status(404).send("File not found."); response.status(404).send("File not found.");
} else { } else {
response.send(indexHTML); response.send(server.indexHTML);
} }
}); });
@ -674,6 +669,8 @@ try {
bean.mqttPassword = monitor.mqttPassword; bean.mqttPassword = monitor.mqttPassword;
bean.mqttTopic = monitor.mqttTopic; bean.mqttTopic = monitor.mqttTopic;
bean.mqttSuccessMessage = monitor.mqttSuccessMessage; bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
bean.databaseConnectionString = monitor.databaseConnectionString;
bean.databaseQuery = monitor.databaseQuery;
bean.authMethod = monitor.authMethod; bean.authMethod = monitor.authMethod;
bean.authWorkstation = monitor.authWorkstation; bean.authWorkstation = monitor.authWorkstation;
bean.authDomain = monitor.authDomain; bean.authDomain = monitor.authDomain;

@ -29,6 +29,12 @@ class UptimeKumaServer {
httpServer = undefined; httpServer = undefined;
io = undefined; io = undefined;
/**
* Cache Index HTML
* @type {string}
*/
indexHTML = "";
static getInstance(args) { static getInstance(args) {
if (UptimeKumaServer.instance == null) { if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args); UptimeKumaServer.instance = new UptimeKumaServer(args);
@ -55,6 +61,16 @@ class UptimeKumaServer {
this.httpServer = http.createServer(this.app); 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);
}
}
this.io = new Server(this.httpServer); this.io = new Server(this.httpServer);
} }

@ -10,6 +10,7 @@ const chardet = require("chardet");
const mqtt = require("mqtt"); const mqtt = require("mqtt");
const chroma = require("chroma-js"); const chroma = require("chroma-js");
const { badgeConstants } = require("./config"); const { badgeConstants } = require("./config");
const mssql = require("mssql");
const { NtlmClient } = require("axios-ntlm"); const { NtlmClient } = require("axios-ntlm");
// From ping-lite // From ping-lite
@ -197,12 +198,16 @@ exports.httpNtlm = function (options, ntlmOptions) {
* Resolves a given record using the specified DNS server * Resolves a given record using the specified DNS server
* @param {string} hostname The hostname of the record to lookup * @param {string} hostname The hostname of the record to lookup
* @param {string} resolverServer The DNS server to use * @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 * @param {string} rrtype The type of record to request
* @returns {Promise<(string[]|Object[]|Object)>} * @returns {Promise<(string[]|Object[]|Object)>}
*/ */
exports.dnsResolve = function (hostname, resolverServer, rrtype) { exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
const resolver = new Resolver(); 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) => { return new Promise((resolve, reject) => {
if (rrtype === "PTR") { if (rrtype === "PTR") {
resolver.reverse(hostname, (err, records) => { resolver.reverse(hostname, (err, records) => {
@ -224,6 +229,31 @@ 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.on("error", err => {
reject(err);
});
mssql.connect(connectionString).then(pool => {
return pool.request()
.query(query);
}).then(result => {
resolve(result);
}).catch(err => {
reject(err);
}).finally(() => {
mssql.close();
});
});
};
/** /**
* Retrieve value of setting based on key * Retrieve value of setting based on key
* @param {string} key Key of setting to retrieve * @param {string} key Key of setting to retrieve
@ -575,3 +605,15 @@ exports.percentageToColor = (percentage, maxHue = 90, minHue = 10) => {
exports.filterAndJoin = (parts, connector = "") => { exports.filterAndJoin = (parts, connector = "") => {
return parts.filter((part) => !!part && part !== "").join(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,
});
};

@ -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>

@ -168,7 +168,8 @@ export default {
getBeatTitle(beat) { getBeatTitle(beat) {
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : ""); return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
} },
}, },
}; };
</script> </script>

@ -1,12 +1,11 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="clicksendsms-login" class="form-label">API Username</label> <label for="clicksendsms-login" class="form-label">{{ $t("API Username") }}</label>
<div class="form-text"> <i18n-t tag="div" class="form-text" keypath="wayToGetClickSendSMSToken">
{{ $t("apiCredentials") }}
<a href="http://dashboard.clicksend.com/account/subaccounts" target="_blank">{{ $t("here") }}</a> <a href="http://dashboard.clicksend.com/account/subaccounts" target="_blank">{{ $t("here") }}</a>
</div> </i18n-t>
<input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required> <input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
<label for="clicksendsms-key" class="form-label">API Key</label> <label for="clicksendsms-key" class="form-label">{{ $t("API Key") }}</label>
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@ -16,15 +15,15 @@
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="clicksendsms-to-number" class="form-label">Recipient Number</label> <label for="clicksendsms-to-number" class="form-label">{{ $t("Recipient Number") }}</label>
<input id="clicksendsms-to-number" v-model="$parent.notification.clicksendsmsToNumber" type="text" minlength="8" maxlength="14" class="form-control" required> <input id="clicksendsms-to-number" v-model="$parent.notification.clicksendsmsToNumber" type="text" minlength="8" maxlength="14" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="clicksendsms-sender-name" class="form-label">From Name/Number - <label for="clicksendsms-sender-name" class="form-label">{{ $t("From Name/Number") }} -
<a href="https://help.clicksend.com/article/4kgj7krx00-what-is-a-sender-id-or-sender-number" target="_blank">More Info</a> <a href="https://help.clicksend.com/article/4kgj7krx00-what-is-a-sender-id-or-sender-number" target="_blank">{{ $t("Read more") }}</a>
</label> </label>
<input id="clicksendsms-sender-name" v-model="$parent.notification.clicksendsmsSenderName" type="text" minlength="3" maxlength="11" class="form-control"> <input id="clicksendsms-sender-name" v-model="$parent.notification.clicksendsmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
<div class="form-text">Leave blank to use a shared sender number.</div> <div class="form-text">{{ $t("Leave blank to use a shared sender number.") }}</div>
</div> </div>
</template> </template>
<script> <script>

@ -7,7 +7,7 @@
<b>{{ $t("Basic Settings") }}</b> <b>{{ $t("Basic Settings") }}</b>
</i18n-t> </i18n-t>
<div class="mb-3" style="margin-top: 12px;"> <div class="mb-3" style="margin-top: 12px;">
<label for="line-user-id" class="form-label">User ID</label> <label for="line-user-id" class="form-label">{{ $t("User ID") }}</label>
<input id="line-user-id" v-model="$parent.notification.lineUserID" type="text" class="form-control" required> <input id="line-user-id" v-model="$parent.notification.lineUserID" type="text" class="form-control" required>
</div> </div>
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text"> <i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">

@ -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>

@ -1,18 +1,18 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="octopush-version" class="form-label">Octopush API Version</label> <label for="octopush-version" class="form-label">{{ $t("Octopush API Version") }}</label>
<select id="octopush-version" v-model="$parent.notification.octopushVersion" class="form-select"> <select id="octopush-version" v-model="$parent.notification.octopushVersion" class="form-select">
<option value="2">Octopush (endpoint: api.octopush.com)</option> <option value="2">{{ $t("octopush") }} ({{ $t("endpoint") }}: api.octopush.com)</option>
<option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option> <option value="1">{{ $t("Legacy Octopush-DM") }} ({{ $t("endpoint") }}: www.octopush-dm.com)</option>
</select> </select>
<div class="form-text"> <div class="form-text">
{{ $t("octopushLegacyHint") }} {{ $t("octopushLegacyHint") }}
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="octopush-key" class="form-label">API KEY</label> <label for="octopush-key" class="form-label">{{ $t("octopushAPIKey") }}</label>
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
<label for="octopush-login" class="form-label">API LOGIN</label> <label for="octopush-login" class="form-label">{{ $t("octopushLogin") }}</label>
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required> <input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">

@ -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> <template>
<div class="mb-3"> <div class="mb-3">
<label for="promosms-login" class="form-label">API LOGIN</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> <input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
<label for="promosms-key" class="form-label">API PASSWORD</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> <HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
</div> </div>
<div class="mb-3"> <div class="mb-3">

@ -18,28 +18,29 @@
</select> </select>
<label for="pushover-sound" class="form-label">{{ $t("Notification Sound") }}</label> <label for="pushover-sound" class="form-label">{{ $t("Notification Sound") }}</label>
<select id="pushover-sound" v-model="$parent.notification.pushoversounds" class="form-select"> <select id="pushover-sound" v-model="$parent.notification.pushoversounds" class="form-select">
<option>pushover</option> <option value="pushover">{{ $t("pushoversounds pushover") }}</option>
<option>bike</option> <option value="bike">{{ $t("pushoversounds bike") }}</option>
<option>bugle</option> <option value="bugle">{{ $t("pushoversounds bugle") }}</option>
<option>cashregister</option> <option value="cashregister">{{ $t("pushoversounds cashregister") }}</option>
<option>classical</option> <option value="classical">{{ $t("pushoversounds classical") }}</option>
<option>cosmic</option> <option value="cosmic">{{ $t("pushoversounds cosmic") }}</option>
<option>falling</option> <option value="falling">{{ $t("pushoversounds falling") }}</option>
<option>gamelan</option> <option value="gamelan">{{ $t("pushoversounds gamelan") }}</option>
<option>incoming</option> <option value="incoming">{{ $t("pushoversounds incoming") }}</option>
<option>intermission</option> <option value="intermission">{{ $t("pushoversounds intermission") }}</option>
<option>mechanical</option> <option value="magic">{{ $t("pushoversounds magic") }}</option>
<option>pianobar</option> <option value="mechanical">{{ $t("pushoversounds mechanical") }}</option>
<option>siren</option> <option value="pianobar">{{ $t("pushoversounds pianobar") }}</option>
<option>spacealarm</option> <option value="siren">{{ $t("pushoversounds siren") }}</option>
<option>tugboat</option> <option value="spacealarm">{{ $t("pushoversounds spacealarm") }}</option>
<option>alien</option> <option value="tugboat">{{ $t("pushoversounds tugboat") }}</option>
<option>climb</option> <option value="alien">{{ $t("pushoversounds alien") }}</option>
<option>persistent</option> <option value="climb">{{ $t("pushoversounds climb") }}</option>
<option>echo</option> <option value="persistent">{{ $t("pushoversounds persistent") }}</option>
<option>updown</option> <option value="echo">{{ $t("pushoversounds echo") }}</option>
<option>vibrate</option> <option value="updown">{{ $t("pushoversounds updown") }}</option>
<option>none</option> <option value="vibrate">{{ $t("pushoversounds vibrate") }}</option>
<option value="none">{{ $t("pushoversounds none") }}</option>
</select> </select>
<div class="form-text"> <div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }} <span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}

@ -1,11 +1,11 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="pushy-app-token" class="form-label">API_KEY</label> <label for="pushy-app-token" class="form-label">{{ $t("pushyAPIKey") }}</label>
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="pushy-user-key" class="form-label">USER_TOKEN</label> <label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
<div class="input-group mb-3"> <div class="input-group mb-3">
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div> </div>

@ -1,6 +1,6 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="push-api-key" class="form-label">API_KEY</label> <label for="push-api-key" class="form-label">{{ $t("API Key") }}</label>
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
</div> </div>

@ -4,6 +4,7 @@ import Discord from "./Discord.vue";
import Webhook from "./Webhook.vue"; import Webhook from "./Webhook.vue";
import Signal from "./Signal.vue"; import Signal from "./Signal.vue";
import Gotify from "./Gotify.vue"; import Gotify from "./Gotify.vue";
import Ntfy from "./Ntfy.vue";
import Slack from "./Slack.vue"; import Slack from "./Slack.vue";
import RocketChat from "./RocketChat.vue"; import RocketChat from "./RocketChat.vue";
import Teams from "./Teams.vue"; import Teams from "./Teams.vue";
@ -27,6 +28,7 @@ import SerwerSMS from "./SerwerSMS.vue";
import Stackfield from "./Stackfield.vue"; import Stackfield from "./Stackfield.vue";
import WeCom from "./WeCom.vue"; import WeCom from "./WeCom.vue";
import GoogleChat from "./GoogleChat.vue"; import GoogleChat from "./GoogleChat.vue";
import PagerDuty from "./PagerDuty.vue";
import Gorush from "./Gorush.vue"; import Gorush from "./Gorush.vue";
import Alerta from "./Alerta.vue"; import Alerta from "./Alerta.vue";
import OneBot from "./OneBot.vue"; import OneBot from "./OneBot.vue";
@ -45,6 +47,7 @@ const NotificationFormList = {
"teams": Teams, "teams": Teams,
"signal": Signal, "signal": Signal,
"gotify": Gotify, "gotify": Gotify,
"ntfy": Ntfy,
"slack": Slack, "slack": Slack,
"rocket.chat": RocketChat, "rocket.chat": RocketChat,
"pushover": Pushover, "pushover": Pushover,
@ -67,6 +70,7 @@ const NotificationFormList = {
"stackfield": Stackfield, "stackfield": Stackfield,
"WeCom": WeCom, "WeCom": WeCom,
"GoogleChat": GoogleChat, "GoogleChat": GoogleChat,
"PagerDuty": PagerDuty,
"gorush": Gorush, "gorush": Gorush,
"alerta": Alerta, "alerta": Alerta,
"OneBot": OneBot, "OneBot": OneBot,

@ -9,11 +9,11 @@
<div class="mt-1"> <div class="mt-1">
<div class="form-check"> <div class="form-check">
<label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> Show update if available</label> <label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> {{ $t("Show update if available") }}</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> Also check beta release</label> <label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> {{ $t("Also check beta release") }}</label>
</div> </div>
</div> </div>
</div> </div>

@ -20,16 +20,91 @@
</button> </button>
</div> </div>
<div class="my-4">
<h4>{{ $t("settingsCertificateExpiry") }}</h4>
<p>{{ $t("certificationExpiryDescription") }}</p>
<div class="mt-2 mb-4 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" /> <NotificationDialog ref="notificationDialog" />
</div> </div>
</template> </template>
<script> <script>
import NotificationDialog from "../../components/NotificationDialog.vue"; import NotificationDialog from "../../components/NotificationDialog.vue";
import ActionInput from "../ActionInput.vue";
export default { export default {
components: { 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> </script>
@ -37,10 +112,27 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../assets/vars.scss"; @import "../../assets/vars.scss";
.btn-rm-expiry {
padding-left: 11px;
padding-right: 11px;
}
.dark { .dark {
.list-group-item { .list-group-item {
background-color: $dark-bg2; background-color: $dark-bg2;
color: $dark-font-color; 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> </style>

@ -206,7 +206,7 @@
<template v-else-if="$i18n.locale === 'bg-BG' "> <template v-else-if="$i18n.locale === 'bg-BG' ">
<p>Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?</p> <p>Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?</p>
<p>Използва се в случаите, когато <strong>има настроен алтернативен метод за удостоверяване</strong> преди Uptime Kuma, например Cloudflare Access.</p> <p>Използва се в случаите, когато <strong>има настроен алтернативен метод за удостоверяване</strong> преди Uptime Kuma, например Cloudflare Access, Authelia или друг механизъм за удостоверяване.</p>
<p>Моля, използвайте с повишено внимание.</p> <p>Моля, използвайте с повишено внимание.</p>
</template> </template>
@ -234,6 +234,12 @@
<p>Vui lòng <strong>cẩn thận</strong>.</p> <p>Vui lòng <strong>cẩn thận</strong>.</p>
</template> </template>
<template v-else-if="$i18n.locale === 'th-TH' ">
<p>ณตองการทจะ <strong>ดใชงานระบบรบรองความถกตองใชหรอไม</strong>?</p>
<p>ระบบนกออกแบบมาเพอการใชงานกบระบบรบรองความถกตองของบคคลทสามเช Cloudflare Access, Authelia หรอวการอ </p>
<p>โปรดใชความระมดระวงในการเลอกใชงานระบบน !</p>
</template>
<!-- English (en) --> <!-- English (en) -->
<template v-else> <template v-else>
<p>Are you sure want to <strong>disable authentication</strong>?</p> <p>Are you sure want to <strong>disable authentication</strong>?</p>

@ -31,6 +31,7 @@ const languageList = {
"vi-VN": "Tiếng Việt", "vi-VN": "Tiếng Việt",
"zh-TW": "繁體中文 (台灣)", "zh-TW": "繁體中文 (台灣)",
"uk-UA": "Український", "uk-UA": "Український",
"th-TH": "ไทย",
}; };
let messages = { let messages = {

@ -55,8 +55,7 @@ export default {
Current: "Текущ", Current: "Текущ",
Uptime: "Достъпност", Uptime: "Достъпност",
"Cert Exp.": "Вал. сертификат", "Cert Exp.": "Вал. сертификат",
days: "дни", day: "ден | дни",
day: "ден",
"-day": "-дни", "-day": "-дни",
hour: "час", hour: "час",
"-hour": "-часa", "-hour": "-часa",
@ -464,4 +463,55 @@ export default {
"Domain Names": "Домейни", "Domain Names": "Домейни",
signedInDisp: "Вписан като {0}", signedInDisp: "Вписан като {0}",
signedInDispDisabled: "Удостоверяването е изключено.", 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} .",
}; };

@ -56,8 +56,7 @@ export default {
Current: "Aktuální", Current: "Aktuální",
Uptime: "Doba provozu", Uptime: "Doba provozu",
"Cert Exp.": "Platnost certifikátu", "Cert Exp.": "Platnost certifikátu",
days: "dny/í", day: "den | dny/í",
day: "den",
"-day": "-dní", "-day": "-dní",
hour: "hodina", hour: "hodina",
"-hour": "-hodin", "-hour": "-hodin",

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

@ -30,8 +30,7 @@ export default {
Current: "Aktuell", Current: "Aktuell",
Uptime: "Verfügbarkeit", Uptime: "Verfügbarkeit",
"Cert Exp.": "Zertifikatsablauf", "Cert Exp.": "Zertifikatsablauf",
days: "Tage", day: "Tag | Tage",
day: "Tag",
"-day": "-Tage", "-day": "-Tage",
hour: "Stunde", hour: "Stunde",
"-hour": "-Stunden", "-hour": "-Stunden",

@ -13,6 +13,7 @@ export default {
pauseDashboardHome: "Pause", pauseDashboardHome: "Pause",
deleteMonitorMsg: "Are you sure want to delete this monitor?", deleteMonitorMsg: "Are you sure want to delete this monitor?",
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?", deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?",
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.", resolverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.",
rrtypeDescription: "Select the RR type you want to monitor", rrtypeDescription: "Select the RR type you want to monitor",
pauseMonitorMsg: "Are you sure want to pause?", pauseMonitorMsg: "Are you sure want to pause?",
@ -56,8 +57,7 @@ export default {
Current: "Current", Current: "Current",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Cert Exp.", "Cert Exp.": "Cert Exp.",
days: "days", day: "day | days",
day: "day",
"-day": "-day", "-day": "-day",
hour: "hour", hour: "hour",
"-hour": "-hour", "-hour": "-hour",
@ -330,6 +330,8 @@ export default {
info: "info", info: "info",
warning: "warning", warning: "warning",
danger: "danger", danger: "danger",
error: "error",
critical: "critical",
primary: "primary", primary: "primary",
light: "light", light: "light",
dark: "dark", dark: "dark",
@ -370,6 +372,13 @@ export default {
smtpDkimHashAlgo: "Hash Algorithm (Optional)", smtpDkimHashAlgo: "Hash Algorithm (Optional)",
smtpDkimheaderFieldNames: "Header Keys to sign (Optional)", smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
smtpDkimskipFields: "Header Keys not to sign (Optional)", smtpDkimskipFields: "Header Keys not to sign (Optional)",
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", gorush: "Gorush",
alerta: "Alerta", alerta: "Alerta",
alertaApiEndpoint: "API Endpoint", alertaApiEndpoint: "API Endpoint",
@ -464,4 +473,59 @@ export default {
"Domain Names": "Domain Names", "Domain Names": "Domain Names",
signedInDisp: "Signed in as {0}", signedInDisp: "Signed in as {0}",
signedInDispDisabled: "Auth Disabled.", signedInDispDisabled: "Auth Disabled.",
"Certificate Expiry Notification": "Certificate Expiry Notification",
"API Username": "API Username",
"API Key": "API Key",
"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 Login Name",
promosmsPassword: "API Password",
"pushoversounds pushover": "Pushover (default)",
"pushoversounds bike": "Bike",
"pushoversounds bugle": "Bugle",
"pushoversounds cashregister": "Cash Register",
"pushoversounds classical": "Classical",
"pushoversounds cosmic": "Cosmic",
"pushoversounds falling": "Falling",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Incoming",
"pushoversounds intermission": "Intermission",
"pushoversounds magic": "Magic",
"pushoversounds mechanical": "Mechanical",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Siren",
"pushoversounds spacealarm": "Space Alarm",
"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": "Vibrate Only",
"pushoversounds none": "None (silent)",
pushyAPIKey: "Secret API Key",
pushyToken: "Device token",
"Show update if available": "Show update if available",
"Also check beta release": "Also check beta release",
"Using a Reverse Proxy?": "Using a Reverse Proxy?",
"Check how to config it for WebSocket": "Check how to config it for WebSocket",
"Steam Game Server": "Steam Game Server",
"Most likely causes:": "Most likely causes:",
"The resource is no longer available.": "The resource is no longer available.",
"There might be a typing error in the address.": "There might be a typing error in the address.",
"What you can try:": "What you can try:",
"Retype the address.": "Retype the address.",
"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:",
}; };

@ -44,8 +44,7 @@ export default {
Current: "Actual", Current: "Actual",
Uptime: "Tiempo activo", Uptime: "Tiempo activo",
"Cert Exp.": "Caducidad cert.", "Cert Exp.": "Caducidad cert.",
days: "días", day: "día | días",
day: "día",
"-day": "-día", "-day": "-día",
hour: "hora", hour: "hora",
"-hour": "-hora", "-hour": "-hora",

@ -47,8 +47,7 @@ export default {
Current: "Hetkeseisund", Current: "Hetkeseisund",
Uptime: "Eluiga", Uptime: "Eluiga",
"Cert Exp.": "Sert. aegumine", "Cert Exp.": "Sert. aegumine",
days: "päeva", day: "päev | päeva",
day: "päev",
"-day": "-päev", "-day": "-päev",
hour: "tund", hour: "tund",
"-hour": "-tund", "-hour": "-tund",

@ -55,7 +55,6 @@ export default {
Current: "فعلی", Current: "فعلی",
Uptime: "آپتایم", Uptime: "آپتایم",
"Cert Exp.": "تاریخ انقضای SSL", "Cert Exp.": "تاریخ انقضای SSL",
days: "روز",
day: "روز", day: "روز",
"-day": "-روز", "-day": "-روز",
hour: "ساعت", hour: "ساعت",

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

@ -56,8 +56,7 @@ export default {
Current: "Trenutno", Current: "Trenutno",
Uptime: "Dostupnost", Uptime: "Dostupnost",
"Cert Exp.": "Istek cert.", "Cert Exp.": "Istek cert.",
days: "dana", day: "dan | dana",
day: "dan",
"-day": "-dnevno", "-day": "-dnevno",
hour: "sat", hour: "sat",
"-hour": "-satno", "-hour": "-satno",

@ -55,7 +55,6 @@ export default {
Current: "Aktuális", Current: "Aktuális",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "SSL lejárat", "Cert Exp.": "SSL lejárat",
days: "nap",
day: "nap", day: "nap",
"-day": " nap", "-day": " nap",
hour: "óra", hour: "óra",

@ -55,8 +55,7 @@ export default {
Current: "Saat ini", Current: "Saat ini",
Uptime: "Waktu aktif", Uptime: "Waktu aktif",
"Cert Exp.": "Cert Exp.", "Cert Exp.": "Cert Exp.",
days: "hari-hari", day: "hari | hari-hari",
day: "hari",
"-day": "-hari", "-day": "-hari",
hour: "Jam", hour: "Jam",
"-hour": "-Jam", "-hour": "-Jam",

@ -56,8 +56,7 @@ export default {
Current: "Corrente", Current: "Corrente",
Uptime: "Tempo di attività", Uptime: "Tempo di attività",
"Cert Exp.": "Scadenza certificato", "Cert Exp.": "Scadenza certificato",
days: "giorni", day: "giorno | giorni",
day: "giorno",
"-day": "-giorni", "-day": "-giorni",
hour: "ora", hour: "ora",
"-hour": "-ore", "-hour": "-ore",

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

@ -55,7 +55,6 @@ export default {
Current: "현재", Current: "현재",
Uptime: "업타임", Uptime: "업타임",
"Cert Exp.": "인증서 만료", "Cert Exp.": "인증서 만료",
days: "일",
day: "일", day: "일",
"-day": "-일", "-day": "-일",
hour: "시간", hour: "시간",
@ -187,9 +186,9 @@ export default {
"Bot Token": "봇 토큰", "Bot Token": "봇 토큰",
wayToGetTelegramToken: "토큰은 여기서 얻을 수 있어요: {0}.", wayToGetTelegramToken: "토큰은 여기서 얻을 수 있어요: {0}.",
"Chat ID": "채팅 ID", "Chat ID": "채팅 ID",
supportTelegramChatID: "Direct Chat / Group / Channel's Chat ID를 지원해요.", supportTelegramChatID: "개인 채팅 / 그룹 / 채널의 ID를 지원해요.",
wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.", wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.",
"YOUR BOT TOKEN HERE": "여기에 BOT 토큰을 적어주세요.", "YOUR BOT TOKEN HERE": "봇 토큰",
chatIDNotFound: "채팅 ID를 찾을 수 없어요. 먼저 봇에게 메시지를 보내주세요.", chatIDNotFound: "채팅 ID를 찾을 수 없어요. 먼저 봇에게 메시지를 보내주세요.",
webhook: "Webhook", webhook: "Webhook",
"Post URL": "Post URL", "Post URL": "Post URL",
@ -305,13 +304,13 @@ export default {
PasswordsDoNotMatch: "비밀번호가 일치하지 않아요.", PasswordsDoNotMatch: "비밀번호가 일치하지 않아요.",
records: "records", records: "records",
"One record": "One record", "One record": "One record",
steamApiKeyDescription: "스팀 게임 서버를 모니터링하려면 Steam Web API 키가 필요해요. API 키는 하단 사이트에서 등록할 수 있어요: ", steamApiKeyDescription: "스팀 게임 서버를 모니터링하려면 Steam Web API 키가 필요해요. API 키는 하단 사이트에서 등록할 수 있어요: ",
"Current User": "현재 사용자", "Current User": "현재 사용자",
recent: "최근", recent: "최근",
Done: "완료", Done: "완료",
Info: "정보", Info: "정보",
Security: "보안", Security: "보안",
"Steam API Key": "Steam API Key", "Steam API Key": "스팀 API 키",
"Shrink Database": "데이터베이스 축소", "Shrink Database": "데이터베이스 축소",
"Pick a RR-Type...": "RR-Type을 골라주세요...", "Pick a RR-Type...": "RR-Type을 골라주세요...",
"Pick Accepted Status Codes...": "상태 코드를 골라주세요...", "Pick Accepted Status Codes...": "상태 코드를 골라주세요...",
@ -352,4 +351,177 @@ export default {
serwersmsPhoneNumber: "휴대전화 번호", serwersmsPhoneNumber: "휴대전화 번호",
serwersmsSenderName: "보내는 사람 이름 (customer portal를 통해 가입된 정보)", serwersmsSenderName: "보내는 사람 이름 (customer portal를 통해 가입된 정보)",
stackfield: "Stackfield", stackfield: "Stackfield",
dnsPortDescription: "DNS 서버 포트, 기본값은 53 이에요. 포트는 언제나 변경할 수 있어요.",
PushByTechulus: "Push by Techulus",
GoogleChat: "Google Chat (Google Workspace only)",
topic: "Topic",
topicExplanation: "모니터링할 MQTT Topic",
successMessage: "성공 메시지",
successMessageExplanation: "성공으로 간주되는 MQTT 메시지",
error: "error",
critical: "critical",
Customize: "커스터마이즈",
"Custom Footer": "커스텀 Footer",
"Custom CSS": "커스텀 CSS",
smtpDkimSettings: "DKIM 설정",
smtpDkimDesc: "사용 방법은 DKIM {0}를 참조하세요.",
documentation: "문서",
smtpDkimDomain: "도메인 이름",
smtpDkimKeySelector: "Key Selector",
smtpDkimPrivateKey: "Private Key",
smtpDkimHashAlgo: "해시 알고리즘 (선택)",
smtpDkimheaderFieldNames: "서명할 헤더 키 (선택)",
smtpDkimskipFields: "서명하지 않을 헤더 키 (선택)",
wayToGetPagerDutyKey: "Service -> Service Directory -> (서비스 선택) -> Integrations -> Add integration. 에서 찾을 수 있어요. 자세히 알아보려면 {0}에서 \"Events API V2\"를 검색해봐요.",
"Integration Key": "Integration 키",
"Integration URL": "Integration URL",
"Auto resolve or acknowledged": "자동 해결 혹은 승인",
"do nothing": "아무것도 하지 않기",
"auto acknowledged": "자동 승인 (acknowledged)",
"auto resolve": "자동 해결 (resolve)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "환경변수",
alertaApiKey: "API 키",
alertaAlertState: "경고 상태",
alertaRecoverState: "해결된 상태",
deleteStatusPageMsg: "정말 이 상태 페이지를 삭제할까요?",
Proxies: "프록시",
default: "Default",
enabled: "활성화",
setAsDefault: "기본 프록시로 설정",
deleteProxyMsg: "정말 이 프록시를 모든 모니터링에서 삭제할까요?",
proxyDescription: "프록시가 작동하려면 모니터에 할당되어야 해요.",
enableProxyDescription: "이 프록시는 활성화될 때까지 영향을 미치지 않아요. 활성화 상태에 따라 모든 모니터에서 프록시를 일시정지할 수 있어요.",
setAsDefaultProxyDescription: "새로 추가하는 모든 모니터링에 이 프록시를 기본적으로 활성화해요. 각 모니터에 대해 별도로 프록시를 비활성화할 수 있어요.",
"Certificate Chain": "인증서 체인",
Valid: "유효",
Invalid: "유효하지 않음",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "휴대전화 번호",
TemplateCode: "템플릿 코드",
SignName: "SignName",
"Sms template must contain parameters: ": "Sms 템플릿은 다음과 같은 파라미터가 포함되어야 해요:",
"Bark Endpoint": "Bark Endpoint",
WebHookUrl: "웹훅 URL",
SecretKey: "Secret Key",
"For safety, must use secret key": "안전을 위해 꼭 Secret Key를 사용하세요.",
"Device Token": "기기 Token",
Platform: "플랫폼",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "High",
Retry: "재시도",
Topic: "Topic",
"WeCom Bot Key": "WeCom Bot Key",
"Setup Proxy": "프록시 설정",
"Proxy Protocol": "프록시 프로토콜",
"Proxy Server": "프록시 서버",
"Proxy server has authentication": "프록시 서버에 인증 절차가 있음",
User: "사용자",
Installed: "설치됨",
"Not installed": "설치되어 있지 않음",
Running: "작동 중",
"Not running": "작동하고 있지 않음",
"Remove Token": "토큰 삭제",
Start: "시작",
Stop: "정지",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "새로운 상태 페이지 만들기",
Slug: "주소",
"Accept characters:": "허용되는 문자열:",
startOrEndWithOnly: "{0}로 시작하거나 끝나야 해요.",
"No consecutive dashes": "연속되는 대시는 허용되지 않아요",
Next: "다음",
"The slug is already taken. Please choose another slug.": "이미 존재하는 주소에요. 다른 주소를 사용해 주세요.",
"No Proxy": "프록시 없음",
"HTTP Basic Auth": "HTTP 인증",
"New Status Page": "새로운 상태 페이지",
"Page Not Found": "페이지를 찾을 수 없어요",
"Reverse Proxy": "리버스 프록시",
Backup: "백업",
About: "정보",
wayToGetCloudflaredURL: "({0}에서 Cloudflare 다운로드 하기)",
cloudflareWebsite: "Cloudflare 웹사이트",
"Message:": "메시지:",
"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.": "Cloudflare Tunnel를 연결하면 현재 연결이 끊길 수 있어요. 정말 중지할까요? 비밀번호를 입력해 확인하세요.",
"Other Software": "다른 소프트웨어",
"For example: nginx, Apache and Traefik.": "nginx, Apache, Traefik 등을 사용할 수 있어요.",
"Please read": "이 문서를 참조하세요:",
"Subject:": "Subject:",
"Valid To:": "Valid To:",
"Days Remaining:": "남은 일수:",
"Issuer:": "Issuer:",
"Fingerprint:": "Fingerprint:",
"No status pages": "상태 페이지 없음",
"Domain Name Expiry Notification": "도메인 이름 만료 알림",
Proxy: "프록시",
"Date Created": "생성된 날짜",
onebotHttpAddress: "OneBot HTTP 주소",
onebotMessageType: "OneBot 메시지 종류",
onebotGroupMessage: "그룹 메시지",
onebotPrivateMessage: "개인 메시지",
onebotUserOrGroupId: "그룹/사용자 ID",
onebotSafetyTips: "안전을 위해 Access 토큰을 설정하세요.",
"PushDeer Key": "PushDeer 키",
"Footer Text": "Footer 문구",
"Show Powered By": "Powered By 문구 표시하기",
"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",
endpoint: "endpoint",
octopushAPIKey: "제어판 HTTP API credentials 에서 \"API key\"",
octopushLogin: "제어판 HTTP API credentials 에서 \"Login\"",
promosmsLogin: "API 로그인 이름",
promosmsPassword: "API 비밀번호",
"pushoversounds pushover": "Pushover (기본)",
"pushoversounds bike": "Bike",
"pushoversounds bugle": "Bugle",
"pushoversounds cashregister": "Cash Register",
"pushoversounds classical": "Classical",
"pushoversounds cosmic": "Cosmic",
"pushoversounds falling": "Falling",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Incoming",
"pushoversounds intermission": "Intermission",
"pushoversounds magic": "Magic",
"pushoversounds mechanical": "Mechanical",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Siren",
"pushoversounds spacealarm": "Space Alarm",
"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": "진동만",
"pushoversounds none": "없음 (무음)",
pushyAPIKey: "비밀 API 키",
pushyToken: "기기 토큰",
"Show update if available": "사용 가능한 경우에 업데이트 표시",
"Also check beta release": "베타 릴리즈 확인",
"Using a Reverse Proxy?": "리버스 프록시를 사용하시나요?",
"Check how to config it for WebSocket": "웹소켓에 대한 설정 방법 확인",
"Steam Game Server": "스팀 게임 서버",
"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": "Coming Soon",
wayToGetClickSendSMSToken: "{0}에서 API 사용자 이름과 키를 얻을 수 있어요.",
}; };

@ -55,8 +55,7 @@ export default {
Current: "Nåværende", Current: "Nåværende",
Uptime: "Oppetid", Uptime: "Oppetid",
"Cert Exp.": "Sertifikat utløper", "Cert Exp.": "Sertifikat utløper",
days: "dager", day: "dag | dager",
day: "dag",
"-day": "-dag", "-day": "-dag",
hour: "time", hour: "time",
"-hour": "-time", "-hour": "-time",

@ -52,8 +52,7 @@ export default {
Current: "Huidig", Current: "Huidig",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Cert. verl.", "Cert Exp.": "Cert. verl.",
days: "dagen", day: "dag | dagen",
day: "dag",
"-day": "-dag", "-day": "-dag",
hour: "uur", hour: "uur",
"-hour": "-uur", "-hour": "-uur",

@ -55,8 +55,7 @@ export default {
Current: "Aktualny", Current: "Aktualny",
Uptime: "Czas pracy", Uptime: "Czas pracy",
"Cert Exp.": "Certyfikat wygasa", "Cert Exp.": "Certyfikat wygasa",
days: "dni", day: "dzień | dni",
day: "dzień",
"-day": " dni", "-day": " dni",
hour: "godzina", hour: "godzina",
"-hour": " godzin", "-hour": " godzin",

@ -55,8 +55,7 @@ export default {
Current: "Atual", Current: "Atual",
Uptime: "Tempo de atividade", Uptime: "Tempo de atividade",
"Cert Exp.": "Cert Exp.", "Cert Exp.": "Cert Exp.",
days: "dias", day: "dia | dias",
day: "dia",
"-day": "-dia", "-day": "-dia",
hour: "hora", hour: "hora",
"-hour": "-hora", "-hour": "-hora",

@ -44,8 +44,7 @@ export default {
Current: "Текущий", Current: "Текущий",
Uptime: "Аптайм", Uptime: "Аптайм",
"Cert Exp.": "Сертификат истекает", "Cert Exp.": "Сертификат истекает",
days: "дней", day: "день | дней",
day: "день",
"-day": " дней", "-day": " дней",
hour: "час", hour: "час",
"-hour": " часа", "-hour": " часа",

@ -56,8 +56,7 @@ export default {
Current: "Trenutno", Current: "Trenutno",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Potek certifikata", "Cert Exp.": "Potek certifikata",
days: "dni", day: "dan | dni",
day: "dan",
"-day": "-dni", "-day": "-dni",
hour: "ura", hour: "ura",
"-hour": "-ur", "-hour": "-ur",

@ -44,8 +44,7 @@ export default {
Current: "Trenutno", Current: "Trenutno",
Uptime: "Vreme rada", Uptime: "Vreme rada",
"Cert Exp.": "Istek sert.", "Cert Exp.": "Istek sert.",
days: "dana", day: "dan | dana",
day: "dan",
"-day": "-dana", "-day": "-dana",
hour: "sat", hour: "sat",
"-hour": "-sata", "-hour": "-sata",

@ -44,8 +44,7 @@ export default {
Current: "Тренутно", Current: "Тренутно",
Uptime: "Време рада", Uptime: "Време рада",
"Cert Exp.": "Истек серт.", "Cert Exp.": "Истек серт.",
days: "дана", day: "дан | дана",
day: "дан",
"-day": "-дана", "-day": "-дана",
hour: "сат", hour: "сат",
"-hour": "-сата", "-hour": "-сата",

@ -44,8 +44,7 @@ export default {
Current: "Nuvarande", Current: "Nuvarande",
Uptime: "Drifttid", Uptime: "Drifttid",
"Cert Exp.": "Certifikat utgår", "Cert Exp.": "Certifikat utgår",
days: "dagar", day: "dag | dagar",
day: "dag",
"-day": " dagar", "-day": " dagar",
hour: "timme", hour: "timme",
"-hour": " timmar", "-hour": " timmar",

@ -0,0 +1,518 @@
export default {
languageName: "ไทย",
checkEverySecond: "ตรวจสอบทุก {0} วินาที",
retryCheckEverySecond: "ลองใหม่ทุก {0} วินาที",
retriesDescription: "จำนวนครั้งสูงสุดที่จะลองก่อนบริการถูกระบุว่าไม่สามารถใช้งานได้และส่งการแจ้งเตือน",
ignoreTLSError: "ไม่สนใจข้อผิดพลาด TLS/SSL สำหรับเว็บไซต์ HTTPS",
upsideDownModeDescription: "กลับด้านสถานะ เช่น ถ้าบริการสามารถใช้งานได้จะถูกเปลี่ยนเป็นใช้งานไม่ได้",
maxRedirectDescription: "จำนวนครั้งสูงสุดที่จะเปลี่ยนเส้นทาง, ตั่งเป็น 0 เพื่อปิดการเปลี่ยนเส้นทาง",
acceptedStatusCodesDescription: "เลือกรหัสสถานะที่ถือว่าการตอบกลับสำเร็จ",
passwordNotMatchMsg: "รหัสผ่านไม่ตรงกัน",
notificationDescription: "การแจ้งเตือนต้องกำหนดให้มอนิเตอร์เพื่อให้สามารถใช้งานได้",
keywordDescription: "ค้นหาคำสำคัญใน HTML หรือ JSON ของการตอบกลับ, คำสำคัญต้องคำนึงถึงตัวพิมพ์เล็กและตัวพิมพ์ใหญ่",
pauseDashboardHome: "หยุดชั่วคราว",
deleteMonitorMsg: "คุณแน่ใจหรือไม่ที่จะลบมอนิเตอร์?",
deleteNotificationMsg: "คุณแน่ใจหรือไม่ที่จะลบการแจ้งเตือนสำหรับมอนิเตอร์ทั้งหมด?",
resolverserverDescription: "Cloudflare เป็นเซิร์ฟเวอร์ค้นหาเริ่มต้น, คุณสามารถเปลี่ยนเซิร์ฟเวอร์ได้ตลอดเวลา",
rrtypeDescription: "เลือกประเภท DNS Record ที่คุณต้องการจะมอนิเตอร์",
pauseMonitorMsg: "คุณแน่ใจหรือไม่ที่จะหยุดมอนิเตอร์ชั่วคราว?",
enableDefaultNotificationDescription: "การแจ้งเตือนนี้จะถูกเปิดโดนค่าเริ่มต้นสำหรับมอนิเตอร์ใหม่, คุณสามารถปิดการแจ้งเตือนสำหรับแต่ละมอนิเตอร์ได้",
clearEventsMsg: "คุณแน่ใจหรือไม่ที่จะลบเหตุการณ์ทั้งหมดสำหรับมอนิเตอร์นี้?",
clearHeartbeatsMsg: "คุณแน่ใจหรือไม่ที่จะลบประวัติการตรวจสอบทั้งหมดสำหรับมอนิเตอร์นี้?",
confirmClearStatisticsMsg: "คุณแน่ใจหรือไม่ที่จะลบสถิติทั้งหมด?",
importHandleDescription: "เลือก \"ข้ามรายการที่มีอยู่แล้ว\" ถ้าคุณต้องการข้ามทุกมอนิเตอร์หรือการแจ้งเตือนที่มีชื่อซ้ำกัน, \"เขียนทับ\" จะลบทุกมอนิเตอร์หรือการแจ้งเตือนที่มีชื่อซ้ำกัน",
confirmImportMsg: "คุณแน่ใจหรือไม่ที่จะนำเข้าข้อมูลสำรอง, กรุณาตรวจสอบว่าคุณเลือกข้อมูลที่ถูกต้อง",
twoFAVerifyLabel: "โปรดกรอกกุญแจ 2FA ของคุณเพื่อยืนยัน:",
tokenValidSettingsMsg: "กุญแจถูกต้อง, ตอนนี้คุณสามารถบันทึกการตั้งค่า 2FA ของคุณได้แล้ว",
confirmEnableTwoFAMsg: "คุณแน่ใจหรือไม่ที่จะเปิดใช้งาน 2FA?",
confirmDisableTwoFAMsg: "คุณแน่ใจหรือไม่ที่จะปิดใช้งาน 2FA?",
Settings: "การตั้งค่า",
Dashboard: "แผงควบคุม",
"New Update": "อัพเดทใหม่",
Language: "ภาษา",
Appearance: "รูปร่าง",
Theme: "หน้าตา",
General: "ทั่วไป",
"Primary Base URL": "URL หลัก",
Version: "เวอร์ชั่น",
"Check Update On GitHub": "ตรวจสอบการอัปเดตบน GitHub",
List: "รายการ",
Add: "เพิ่ม",
"Add New Monitor": "เพิ่มมอนิเตอร์ใหม่",
"Quick Stats": "สถิติด่วน",
Up: "ใช้งานได้",
Down: "ไม่สามารถใช้งานได้",
Pending: "รอดำเนินการ",
Unknown: "ไม่ทราบ",
Pause: "หยุดชั่วคราว",
Name: "ชื่อ",
Status: "สถานะ",
DateTime: "วันที่และเวลา",
Message: "ข้อความ",
"No important events": "ไม่มีกิจกรรมที่สำคัญ",
Resume: "ดำเนินการต่อ",
Edit: "แก้ไข",
Delete: "ลบ",
Current: "ปัจจุบัน",
Uptime: "เวลาที่ใช้งาน",
"Cert Exp.": "วันหมดอายุใบรับรอง",
days: "วัน",
day: "วัน",
"-day": "-วัน",
hour: "ชั่วโมง",
"-hour": "-ชั่วโมง",
Response: "การตอบสนอง",
Ping: "การตอบสนอง",
"Monitor Type": "ประเภทมอนิเตอร์",
Keyword: "คำสำคัญ",
"Friendly Name": "ชื่อที่เป็นมิตร",
URL: "URL",
Hostname: "ชื่อโฮสต์",
Port: "พอร์ต",
"Heartbeat Interval": "ระยะห่างระหว่างการทดสอบ",
Retries: "จำนวนครั้งที่จะลองใหม่",
"Heartbeat Retry Interval": "ระยะห่างระหว่างการทดสอบใหม่หลังจากไม่สำเร็จ",
Advanced: "ขั้นสูง",
"Upside Down Mode": "โหมดกลับด้าน",
"Max. Redirects": "จำนวนการเปลี่ยนเส้นทางสูงสุด",
"Accepted Status Codes": "รหัสสถานะที่ยอมรับ",
"Push URL": "URL เป้าหมาย",
needPushEvery: "คุณควรเรียก URL นี้ทุก {0} วินาที",
pushOptionalParams: "ตัวแปรเสริม: {0}",
Save: "บันทึก",
Notifications: "การแจ้งเตือน",
"Not available, please setup.": "ไม่พร้อมใช้งาน, กรุณาตั้งค่า",
"Setup Notification": "ตั้งค่าการแจ้งเตือน",
Light: "สว่าง",
Dark: "มืด",
Auto: "อัตโนมัติ",
"Theme - Heartbeat Bar": "หน้าตา - แถบการตอบสนอง",
Normal: "ปกติ",
Bottom: "ด้านล่าง",
None: "ไม่มี",
Timezone: "เขตเวลา",
"Search Engine Visibility": "การมองเห็นของเครื่องมือค้นหา",
"Allow indexing": "อนุญาตให้สร้างดัชนี",
"Discourage search engines from indexing site": "ปฏิเสธเครื่องมือค้นหาไม่ให้สร้างดัชนีของเว็บไซต์",
"Change Password": "เปลี่ยนรหัสผ่าน",
"Current Password": "รหัสผ่านปัจจุบัน",
"New Password": "รหัสผ่านใหม่",
"Repeat New Password": "ยืนยันรหัสผ่านใหม่",
"Update Password": "อัพเดทรหัสผ่าน",
"Disable Auth": "ปิดใช้งานการตรวจสอบสิทธิ์",
"Enable Auth": "เปิดใช้งานการตรวจสอบสิทธิ์",
Logout: "ออกจากระบบ",
Leave: "ออก",
"I understand, please disable": "ฉันเข้าใจแล้ว, กรุณาปิดการใช้งาน",
Confirm: "ยืนยัน",
Yes: "ใช่",
No: "ไม่",
Username: "ชื่อผู้ใช้",
Password: "รหัสผ่าน",
"Remember me": "คงอยู่ในระบบ",
Login: "เข้าสู่ระบบ",
"No Monitors, please": "ไม่มีมอนิเตอร์, กรุณา",
"add one": "สร้าง",
"Notification Type": "ประเภทการแจ้งเตือน",
Email: "อีเมล",
Test: "ทดสอบ",
"Certificate Info": "ข้อมูลใบรับรอง",
"Resolver Server": "เซิร์ฟเวอร์ทีค้นหา",
"Resource Record Type": "ประเภท DNS Record",
"Last Result": "ผลล่าสุด",
"Create your admin account": "สร้างบัญชีผู้ดูแลระบบ",
"Repeat Password": "ยืนยันรหัสผ่าน",
"Import Backup": "นำเข้าข้อมูลสำรอง",
"Export Backup": "ส่งออกข้อมูลสำรอง",
Export: "ส่งออก",
Import: "นำเข้า",
respTime: "ระยะเวลาการตอบสนอง (ms)",
notAvailableShort: "ไม่สามารถใช้งานได้",
"Default enabled": "เปิดใช้งานโดยค่าเริ่มต้น",
"Apply on all existing monitors": "ใช้กับมอนิเตอร์ทั้งหมด",
Create: "สร้าง",
"Clear Data": "ล้างข้อมูล",
Events: "เหตุการณ์",
Heartbeats: "ประวัติการตรวจสอบ",
"Auto Get": "ดึงอัตโนมัติ",
backupDescription: "คุณสามารถสำรองข้อมูลการแจ้งเตือนและมอนิเตอร์ทั้งหมดได้ในไฟล์ JSON",
backupDescription2: "หมายเหตุ : ประวัติและข้อมูลกิจกรรมจะไม่ถูกสำรอง",
backupDescription3: "ข้อมูลที่ละเอียดอ่อนเช่นกุญแจการแจ้งเตือนจะรวมอยู่ในไฟล์ข้อมูลสำรอง, โปรดเก็บข้อมูลสำรองอย่างปลอดภัย",
alertNoFile: "กรุณาเลือกไฟล์ที่จะใช้งาน",
alertWrongFileType: "กรุณาเลือกไฟล์ที่เป็น JSON",
"Clear all statistics": "ล้างข้อมูลสถิติทั้งหมด",
"Skip existing": "ข้ามรายการที่มีอยู่แล้ว",
Overwrite: "เขียนทับ",
Options: "ตัวเลือก",
"Keep both": "เก็บทั้งสอง",
"Verify Token": "ยืนยันกุญแจ",
"Setup 2FA": "ติดตั้ง 2FA",
"Enable 2FA": "เปิดใช้งาน 2FA",
"Disable 2FA": "ปิดใช้งาน 2FA",
"2FA Settings": "ตั้งค่า 2FA",
"Two Factor Authentication": "การตรวจสอบสิทธิ์สองปัจจัย",
Active: "ใช้งาน",
Inactive: "ไม่ใช้งาน",
Token: "กุญแจ",
"Show URI": "แสดง URI",
Tags: "แท็ก",
"Add New below or Select...": "เพิ่มใหม่ด้านล่างหรือเลือก...",
"Tag with this name already exist.": "แท็กที่มีชื่อนี้มีอยู่แล้ว",
"Tag with this value already exist.": "แท็กที่มีข้อมูลนี้มีอยู่แล้ว",
color: "สี",
"value (optional)": "ข้อมูล (ไม่จำเป็น)",
Gray: "เทา",
Red: "แดง",
Orange: "ส้ม",
Green: "เขียว",
Blue: "น้ำเงิน",
Indigo: "ม่วง",
Purple: "ม่วง",
Pink: "ชมพู",
"Search...": "ค้นหา...",
"Avg. Ping": "ค่า Ping เฉลี่ย",
"Avg. Response": "ค่า Response เฉลี่ย",
"Entry Page": "หน้าต้อนรับ",
statusPageNothing: "ไม่มีอะไรตรงนี้ !, กรุณาเพิ่มกลุ่มหรือมอนิเตอร์",
"No Services": "ไม่มีบริการ",
"All Systems Operational": "บริการทั้งหมดทำงานได้ปกติ",
"Partially Degraded Service": "บริการมีปัญหาบางส่วน",
"Degraded Service": "บริการมีปัญหา",
"Add Group": "เพิ่มกลุ่ม",
"Add a monitor": "เพิ่มมอนิเตอร์",
"Edit Status Page": "แก้ไขหน้าสถานะ",
"Go to Dashboard": "ไปที่หน้าควบคุม",
"Status Page": "หน้าสถานะ",
"Status Pages": "หน้าสถานะ",
defaultNotificationName: "การแจ้งเตือน {notification} ของฉัน ({number})",
here: "ที่นี่",
Required: "ต้องการ",
telegram: "Telegram",
"Bot Token": "กุญแจของบอท",
wayToGetTelegramToken: "คุณสามารถรับกุญแจได้จาก {0}.",
"Chat ID": "ไอดีแชท",
supportTelegramChatID: "รองรับ แชทส่วนตัว, แชทกลุ่ม, ไอดีแชท",
wayToGetTelegramChatID: "คุณสามารถรับ ID แชทของคุณได้โดยส่งข้อความไปยังบอทและไปที่ URL นี้เพื่อดู chat_id :",
"YOUR BOT TOKEN HERE": "กุญแจของบอทของคุณที่นี่",
chatIDNotFound: "ไม่พบไอดีแชท, กรุณาส่งข้อความไปที่บอท",
webhook: "Webhook",
"Post URL": "URL โพสต์",
"Content Type": "ประเภทเนื้อหา",
webhookJsonDesc: "{0} ดีสำหรับเซิร์ฟเวอร์ HTTP สมัยใหม่เช่น Express.js",
webhookFormDataDesc: "{multipart} ดีสำหรับ PHP, JSON จะต้องถูกประมวลผลด้วย {decodeFunction}",
smtp: "Email (SMTP)",
secureOptionNone: "None / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "Ignore TLS Error",
"From Email": "From Email",
emailCustomSubject: "Custom Subject",
"To Email": "To Email",
smtpCC: "CC",
smtpBCC: "BCC",
discord: "Discord",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "คุณสามารถรับได้โดยการไปที่ Server Settings -> Integrations -> Create Webhook",
"Bot Display Name": "ชื่อบอท",
"Prefix Custom Message": "คำนำหน้าข้อความที่กำหนดเอง",
"Hello @everyone is...": "สวัสดี {'@'}everyone นี่...",
teams: "Microsoft Teams",
"Webhook URL": "Webhook URL",
wayToGetTeamsURL: "คุณสามารถเรียนรู้วิธีการสร้าง Webhook URL {0}",
signal: "Signal",
Number: "หมายเลข",
Recipients: "ผู้รับ",
needSignalAPI: "คุณต้องมี Signal Client ที่มี Rest APIl",
wayToCheckSignalURL: "คุณสามารถตรวจสอบ URL นี้เพื่อดูวิธีตั้งค่า :",
signalImportant: "สำคัญ: คุณไม่สามารถผสมกลุ่มและตัวเลขในผู้รับได้!",
gotify: "Gotify",
"Application Token": "กุญแจของแอพพลิเคชั่น",
"Server URL": "Server URL",
Priority: "ลำดับความสำคัญ",
slack: "Slack",
"Icon Emoji": "Icon Emoji",
"Channel Name": "ชื่อห้อง",
"Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "ข้อมูลเพิ่มเติมสำหรับ Webhooks : {0}",
aboutChannelName: "ใส่ชื่อห้องบน {0} ในช่องชื่อห้องถ้าต้องการที่จะข้าม Webhook, เช่น: #ช่องอื่นๆ",
aboutKumaURL: "ถ้าคุณไม่ใส่ข้อมูลในช่อง Uptime Kuma URL ค่าเริ่มต้นจะเป็นจะเป็น Uptime Kuma Github",
emojiCheatSheet: "ตาราง Emoji : {0}",
"rocket.chat": "Rocket.Chat",
pushover: "Pushover",
pushy: "Pushy",
PushByTechulus: "Push by Techulus",
octopush: "Octopush",
promosms: "PromoSMS",
clicksendsms: "ClickSend SMS",
lunasea: "LunaSea",
apprise: "Apprise (รองรับการแจ้งเตือนมากกว่า 50 บริการ)",
GoogleChat: "Google Chat (Google Workspace only)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"User Key": "กุญแจผู้ใช้งาน",
Device: "อุปกรณ์",
"Message Title": "หัวข้อข้อความ",
"Notification Sound": "เสียงแจ้งเตือน",
"More info on:": "ข้อมูลเพิ่มเติม : {0}",
pushoverDesc1: "ลำดับความสำตคญฉุกเฉิน (2) มีการหมดเวลาเริ่มต้น 30 วินาทีระหว่างลองใหม่และจะหมดอายุหลังจาก 1 ชั่วโมง",
pushoverDesc2: "ถ้าคุณต้องการจะส่งการแจ้งเตือนไปยังอุปกรณ์อื่น ๆ สามารถกำหนดได้ที่ช่องอุปกรณ์",
"SMS Type": "ประเภท SMS",
octopushTypePremium: "พรีเมี่ยม (เร็ว - แนะนำสำหรับการแจ้งเตือน)",
octopushTypeLowCost: "ต้นทุนต่ำ (ช้า - บางครั้งจะถูกบล็อกโดยผู้ให้บริการ)",
checkPrice: "ตรวจสอบราคาของ {0} :",
apiCredentials: "ข้อมูลการตรวจสอบสิทธิ์ API",
octopushLegacyHint: "คุณใช้เวอร์ชันดั้งเดิมของ Octopush (2011 - 2020) หรือเวอร์ชันใหม่หรือไม่?",
"Check octopush prices": "ตรวจสอบราคาของ Octopush {0}",
octopushPhoneNumber: "หมายเลขโทรศัพท์ (รูปแบบสากล เช่น +33612345678) ",
octopushSMSSender: "ชื่อผู้ส่ง SMS : ความยาว 3 - 11 ตัวอักษร, ตัวเลข และช่องว่าง (a-zA-Z0-9 )",
"LunaSea Device ID": "ไอดีอุปกรณ์ LunaSea",
"Apprise URL": "Apprise URL",
"Example:": "ตัวอย่าง : {0}",
"Read more:": "อ่านเพิ่มเติม : {0}",
"Status:": "สถานะ : {0}",
"Read more": "อ่านเพิ่มเติม",
appriseInstalled: "Apprise ถูกติดตั่งแล้ว",
appriseNotInstalled: "Apprise ยังไม่ถูกติดตั่ง {0}",
"Access Token": "กุญแจการเข้าถึง",
"Channel access token": "กุญแจการเข้าถึงของช่อง",
"Line Developers Console": "Line Developers Console",
lineDevConsoleTo: "Line Developers Console - {0}",
"Basic Settings": "การตั้งค่าพื้นฐาน",
"User ID": "ไอดีผู้ใช้",
"Messaging API": "Messaging API",
wayToGetLineChannelToken: "ขั้นแรกให้เข้า {0} สร้างผู้ให้บริการและช่องทาง (Messaging API) จากนั้นคุณจะได้รับกุญแจการเข้าถึงช่องและไอดีผู้ใช้จากรายการเมนูที่กล่าวถึงข้างต้น",
"Icon URL": "Icon URL",
aboutIconURL: "คุณสามารถระบุลิงก์ไปยังรูปภาพใน \"URL ไอคอน\" เพื่อแทนที่รูปภาพโปรไฟล์เริ่มต้น จะไม่ถูกใช้หากมีการตั้งค่า Icon Emoji",
aboutMattermostChannelName: "คุณลบล้างช่องเริ่มต้นที่ Webhook โพสต์ได้ด้วยการป้อนชื่อช่องลงในช่อง \"ชื่อช่อง\" ต้องเปิดใช้งานในการตั้งค่า Mattermost Webhook เช่น #ช่องอื่นๆ",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - ราคาถูก แต่ช้าและมักจะโอเวอร์โหลด จำกัดเฉพาะผู้รับโปแลนด์",
promosmsTypeFlash: "SMS FLASH - ข้อความจะแสดงบนอุปกรณ์ของผู้รับโดยอัตโนมัติ จำกัดเฉพาะผู้รับโปแลนด์",
promosmsTypeFull: "SMS FULL - SMS ระดับพรีเมียม คุณสามารถใช้ชื่อผู้ส่งของคุณได้ (คุณต้องลงทะเบียนชื่อก่อน) เชื่อถือได้สำหรับการแจ้งเตือน",
promosmsTypeSpeed: "SMS SPEED - ลำดับความสำคัญสูงสุดในระบบ รวดเร็วและเชื่อถือได้ แต่มีค่าใช้จ่ายสูง (ประมาณสองเท่าของราคาเต็ม SMS)",
promosmsPhoneNumber: "หมายเลขโทรศัพท์ (สำหรับผู้รับโปแลนด์ คุณสามารถข้ามรหัสพื้นที่ได้)",
promosmsSMSSender: "ชื่อผู้ส่ง SMS : ชื่อที่ลงทะเบียนล่วงหน้าหรือหนึ่งในค่าเริ่มต้น: InfoSMS, ข้อมูล SMS, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "URL ของโฮมเซิร์ฟเวอร์ (พร้อม http(s):// และพอร์ตเสริม)",
"Internal Room Id": "รหัสห้องภายใน",
matrixDesc1: "คุณค้นหารหัสห้องภายในได้โดยดูในส่วนขั้นสูงของการตั้งค่าห้องในไคลเอ็นต์ Matrix มันควรจะมีลักษณะเช่น !PMdRCpsIfLwsfjIye6:kiznick.server.",
matrixDesc2: "ขอแนะนำเป็นอย่างยิ่งให้คุณสร้างผู้ใช้ใหม่และอย่าใช้โทเค็นการเข้าถึงของผู้ใช้ Matrix ของคุณเอง เนื่องจากจะทำให้สามารถเข้าถึงบัญชีของคุณและห้องทั้งหมดที่คุณเข้าร่วมได้อย่างเต็มที่ ให้สร้างผู้ใช้ใหม่และเชิญเฉพาะห้องที่คุณต้องการรับการแจ้งเตือนแทน คุณสามารถรับโทเค็นเพื่อการเข้าถึงได้โดยเรียกใช้ {0}",
Method: "วิธี",
Body: "เนื้อหา",
Headers: "ส่วนหัว",
PushUrl: "Push URL",
HeadersInvalidFormat: "เนื้อหาคำขอส่วนหัวไม่ใช่ JSON ที่ถูกต้อง :",
BodyInvalidFormat: "เนื้อหาคำขอไม่ใช่ JSON ที่ถูกต้อง : ",
"Monitor History": "ประวัติมอนิเตอร์",
clearDataOlderThan: "เก็บข้อมูลมอนิเตอร์ {0} วัน",
PasswordsDoNotMatch: "รหัสผ่านไม่ตรงกัน",
records: "บันทึก",
"One record": "หนึ่งบันทึก",
steamApiKeyDescription: "สำหรับการมอนิเตอร์ Steam Game Server คุณต้องมี Steam Web-API key, คุณสามารถรสมัครได้จากที่นี่ : ",
"Current User": "ผู้ใช้ปัจจุบัน",
topic: "หัวข้อ",
topicExplanation: "MQTT หัวข้อที่จะมอนิเตอร์",
successMessage: "ข้อความที่จะถือว่าประสบความสำเร็จ",
successMessageExplanation: "MQTT ข้อความที่จะถือว่าประสบความสำเร็จ",
recent: "ล่าสุด",
Done: "สำเร็จ",
Info: "ข้อมูล",
Security: "ความปลอดภัย",
"Steam API Key": "Steam API Key",
"Shrink Database": "ย่อฐานข้อมูล",
"Pick a RR-Type...": "เลือกชนิด DNS Record",
"Pick Accepted Status Codes...": "เลือกสถานะที่ยอมรับ...",
Default: "ค่าเริ่มต้น",
"HTTP Options": "ตัวเลือก HTTP",
"Create Incident": "สร้างเหตุการณ์",
Title: "หัวข้อ",
Content: "เนื้อหา",
Style: "สไตล์",
info: "ข้อมูล",
warning: "แจ้งเตือน",
danger: "อันตราย",
primary: "หลัก",
light: "สว่าง",
dark: "มืด",
Post: "โพสต์",
"Please input title and content": "กรุณาใส่ชื่อและเนื้อหา",
Created: "สร้าง",
"Last Updated": "อัพเดทล่าสุด",
Unpin: "เลิกตรึง",
"Switch to Light Theme": "เปลี่ยนเป็นแบบสว่าง",
"Switch to Dark Theme": "เปลี่ยนเป็นแบบมืด",
"Show Tags": "แสดงแท็ก",
"Hide Tags": "ซ่อนแท็ก",
Description: "รายละเอียด",
"No monitors available.": "ไม่มีมอนิเตอร์ที่สามารถใช้งานได้",
"Add one": "เพิ่ม",
"No Monitors": "ไม่มีมอนิเตอร์",
"Untitled Group": "กลุ่มที่ไม่มีชื่อ",
Services: "บริการ",
Discard: "ทิ้ง",
Cancel: "ยกเลิก",
"Powered by": "ขับเคลื่อนโดย",
shrinkDatabaseDescription: "ทริกเกอร์ฐานข้อมูล VACUUM สำหรับ SQLite หากฐานข้อมูลของคุณถูกสร้างขึ้นหลังจาก 1.10.0 แสดงว่า AUTO_VACUUM เปิดใช้งานอยู่แล้วและไม่จำเป็นต้องดำเนินการนี้",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API Username (incl. webapi_ prefix)",
serwersmsAPIPassword: "API Password",
serwersmsPhoneNumber: "หมายเลขโทรศัพท์",
serwersmsSenderName: "ชื่อผู้ส่ง SMS (ลงทะเบียนผ่านหน้าควบคุม)",
stackfield: "Stackfield",
Customize: "ปรับแต่ง",
"Custom Footer": "ส่วนท้ายที่กำหนดเอง",
"Custom CSS": "CSS ที่กำหนดเอง",
smtpDkimSettings: "ตั้งค่า DKIM",
smtpDkimDesc: "โปรดดู Nodemailer DKIM {0} สำหรับการใช้งาน",
documentation: "เอกสาร",
smtpDkimDomain: "ชื่อโดเมน",
smtpDkimKeySelector: "Key Selector",
smtpDkimPrivateKey: "Private Key",
smtpDkimHashAlgo: "อัลกอริทึมแฮช (ไม่บังคับ)",
smtpDkimheaderFieldNames: "คีย์ส่วนหัวเพื่อลงชื่อ (ไม่บังคับ)",
smtpDkimskipFields: "Header Keys ไม่ต้องเซ็น (ไม่บังคับ)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "Environment",
alertaApiKey: "กุญแจ API",
alertaAlertState: "แจ้งเตือนสถานะ",
alertaRecoverState: "กู้คืนสถานะ",
deleteStatusPageMsg: "คุณแน่ใจหรือไม่ว่าต้องการลบหน้าสถานะนี้",
Proxies: "พร็อกซี",
default: "ค่าเริ่มต้น",
enabled: "เปิดใช้งาน",
setAsDefault: "ตั่งเป็นค่าเริ่มต้น",
deleteProxyMsg: "คุณแน่ใจหรือไม่ว่าต้องการลบพร็อกซีสำหรับมอนิเตอร์ทั้งหมด?",
proxyDescription: "พร็อกซีจะต้องตั้งค่าให้มอนิเตอร์เพื่อให้ใช้งานได้",
enableProxyDescription: "พร็อกซีนี้จะไม่ส่งผลต่อมอนิเตอร์จนกว่าจะเปิดใช้งาน คุณสามารถควบคุมการปิดใช้งานพร็อกซีชั่วคราวจากมอนิเตอร์ทั้งหมดได้โดยสถานะการเปิดใช้งาน",
setAsDefaultProxyDescription: "พร็อกซีนี้จะถูกเปิดโดนค่าเริ่มต้นสำหรับมอนิเตอร์ใหม่, คุณสามารถปิดการแจ้งเตือนสำหรับแต่ละมอนิเตอร์ได้",
"Certificate Chain": "ห่วงโซ่ใบรับรอง",
Valid: "ถูกต้อง",
Invalid: "ไม่ถูกต้อง",
AccessKeyId: "กุญแจสิทธิ ID",
SecretAccessKey: "กุญแจสิทธิ Secret",
PhoneNumbers: "PhoneNumbers",
TemplateCode: "รหัสเทมเพลต",
SignName: "ป้ายชื่อ",
"Sms template must contain parameters: ": "เทมเพลต SMS ต้องมีพารามิเตอร์ : ",
"Bark Endpoint": "Bark Endpoint",
WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey",
"For safety, must use secret key": "เพื่อความปลอดภัย จำเป็นต้องตั้งค่ากุญแจการเข้าถึง",
"Device Token": "Device Token",
Platform: "แพลตฟอร์ม",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "สูง",
Retry: "ลองใหม่",
Topic: "หัวข้อ",
"WeCom Bot Key": "WeCom Bot Key",
"Setup Proxy": "ติดตั้งพร็อกซี่",
"Proxy Protocol": "โปรโตคอลพร็อกซี่",
"Proxy Server": "พร็อกซีเซิร์ฟ",
"Proxy server has authentication": "พร็อกซีเซิร์ฟเวอร์มีการตรวจสอบสิทธิ์",
User: "ผู้ใช้",
Installed: "ติดตั้งแล้ว",
"Not installed": "ไม่ได้ติดตั้ง",
Running: "กำลังทำงาน",
"Not running": "ไม่ได้ทำงาน",
"Remove Token": "ลบกุญแจ",
Start: "เริ่ม",
Stop: "หยุด",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "เพิ่มหน้าสถานะใหม่",
Slug: "ชื่อ",
"Accept characters:": "ตัวอักษรที่ใช้งานได้ :",
startOrEndWithOnly: "เริ่มหรือจบด้วย {0} เท่านั้น",
"No consecutive dashes": "ไม่มีขีดกลางติดต่อกัน",
Next: "ต่อไป",
"The slug is already taken. Please choose another slug.": "ชื่อนี้ถูกใช้งานไปแล้ว กรุณาใช้ชื่ออื่น",
"No Proxy": "ไม่มีพร็อกซี่",
"HTTP Basic Auth": "HTTP Basic Auth",
"New Status Page": "หน้าสถานะใหม่",
"Page Not Found": "ไม่พบหน้านี้",
"Reverse Proxy": "พร็อกซีย้อนกลับ",
Backup: "สำรอง",
About: "เกี่ยวกับ",
wayToGetCloudflaredURL: "(ดาวโหลด cloudflared จาก {0})",
cloudflareWebsite: "เว็บไซต์ Cloudflare",
"Message:": "ข้อความ :",
"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.": "การเชื่อมต่อปัจุบันอาจขาดหายหากคุณกำลังเชื่อมต่อ Cloudflare Tunnel คุณแน่ใจหรือไม่ที่จะหยุด, พิมรหัสผ่านของคุณเพื่อยืนยัน",
"Other Software": "ซอฟต์แวร์อื่น ๆ ",
"For example: nginx, Apache and Traefik.": "เช่น: nginx, Apache และ Traefik",
"Please read": "กรุณาอ่าน",
"Subject:": "เรื่อง :",
"Valid To:": "ถูกต้องถึง :",
"Days Remaining:": "จำนวนวันที่เหลือ :",
"Issuer:": "ผู้ออก :",
"Fingerprint:": "ลายนิ้วมือ :",
"No status pages": "ไม่มีหน้าสถานะ",
"Domain Name Expiry Notification": "แจ้งเตือนการหมดอายุโดเมน",
Proxy: "Proxy",
"Date Created": "วันที่สร้าง",
onebotHttpAddress: "ที่อยู่ HTTP OneBot ",
onebotMessageType: "ชนิดข้อความ OneBot",
onebotGroupMessage: "กลุ่ม",
onebotPrivateMessage: "ส่วนตัว",
onebotUserOrGroupId: "กลุ่ม / ไอดีผู้ใช้",
onebotSafetyTips: "เพื่อความปลอดภัย จำเป็นต้องตั้งค่ากุญแจการเข้าถึง",
"PushDeer Key": "กุญแจ PushDeer",
"Footer Text": "ข้อความส่วนท้าย",
"Show Powered By": "แสดงข้อความ \"ขับเคลื่อนโดย\"",
"Domain Names": "Domain Names",
signedInDisp: "เข้าใช้งานในฐานะ {0}",
signedInDispDisabled: "ปิดการตรวจสอบสิทธิ์",
"Certificate Expiry Notification": "แจ้งเตือนการรับรองหมดอายุ",
"API Username": "API Username",
"API Key": "API Key",
"Recipient Number": "หมายเลขผู้รับ",
"From Name/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\" จากข้อมูลรับรอง HTTP API ในแผงควบคุม",
octopushLogin: "\"Login\" จากข้อมูลรับรอง HTTP API ในแผงควบคุม",
promosmsLogin: "API Login Name",
promosmsPassword: "API Password",
"pushoversounds pushover": "Pushover (default)",
"pushoversounds bike": "Bike",
"pushoversounds bugle": "Bugle",
"pushoversounds cashregister": "Cash Register",
"pushoversounds classical": "Classical",
"pushoversounds cosmic": "Cosmic",
"pushoversounds falling": "Falling",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Incoming",
"pushoversounds intermission": "Intermission",
"pushoversounds magic": "Magic",
"pushoversounds mechanical": "Mechanical",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Siren",
"pushoversounds spacealarm": "Space Alarm",
"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": "Vibrate Only",
"pushoversounds none": "None (silent)",
pushyAPIKey: "Secret API Key",
pushyToken: "Device token",
"Show update if available": "แสดงการอัปเดตถ้ามี",
"Also check beta release": "ตรวจสอบรุ่นเบต้า",
"Using a Reverse Proxy?": "ใช้ Reverse Proxy?",
"Check how to config it for WebSocket": "ตรวจสอบวิธีการตั้งค่าสำหรับ WebSocket",
"Steam Game Server": "Steam Game Server",
"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 Username และ API Key ได้จาก {0}",
};

@ -1,6 +1,7 @@
export default { export default {
languageName: "Türkçe", languageName: "Türkçe",
checkEverySecond: "{0} Saniyede bir kontrol et.", checkEverySecond: "{0} Saniyede bir kontrol et.",
retryCheckEverySecond: "{0} Saniyede bir dene.",
retriesDescription: "Servisin kapalı olarak işaretlenmeden ve bir bildirim gönderilmeden önce maksimum yeniden deneme sayısı", retriesDescription: "Servisin kapalı olarak işaretlenmeden ve bir bildirim gönderilmeden önce maksimum yeniden deneme sayısı",
ignoreTLSError: "HTTPS web siteleri için TLS/SSL hatasını yoksay", ignoreTLSError: "HTTPS web siteleri için TLS/SSL hatasını yoksay",
upsideDownModeDescription: "Servisin durumunu tersine çevirir. Servis çalışıyorsa kapalı olarak işaretler.", upsideDownModeDescription: "Servisin durumunu tersine çevirir. Servis çalışıyorsa kapalı olarak işaretler.",
@ -12,12 +13,20 @@ export default {
pauseDashboardHome: "Durdur", pauseDashboardHome: "Durdur",
deleteMonitorMsg: "Servisi silmek istediğinden emin misin?", deleteMonitorMsg: "Servisi silmek istediğinden emin misin?",
deleteNotificationMsg: "Bu bildirimi tüm servisler için silmek istediğinden emin misin?", deleteNotificationMsg: "Bu bildirimi tüm servisler için silmek istediğinden emin misin?",
dnsPortDescription: "DNS sunucusu bağlantı noktası. Varsayılan değer 53'tür. Bağlantı noktasını istediğiniz zaman değiştirebilirsiniz.",
resolverserverDescription: "Cloudflare varsayılan sunucudur, çözümleyici sunucusunu istediğiniz zaman değiştirebilirsiniz.", resolverserverDescription: "Cloudflare varsayılan sunucudur, çözümleyici sunucusunu istediğiniz zaman değiştirebilirsiniz.",
rrtypeDescription: "İzlemek istediğiniz servisin RR-Tipini seçin", rrtypeDescription: "İzlemek istediğiniz servisin RR-Tipini seçin",
pauseMonitorMsg: "Durdurmak istediğinden emin misin?", pauseMonitorMsg: "Durdurmak istediğinden emin misin?",
enableDefaultNotificationDescription: "Bu bildirim her yeni serviste aktif olacaktır. Bildirimi servisler için ayrı ayrı deaktive edebilirsiniz. ",
clearEventsMsg: "Bu servisin bütün kayıtlarını silmek istediğinden emin misin?", clearEventsMsg: "Bu servisin bütün kayıtlarını silmek istediğinden emin misin?",
clearHeartbeatsMsg: "Bu servis için tüm sağlık durumunu silmek istediğinden emin misin?", clearHeartbeatsMsg: "Bu servis için tüm sağlık durumunu silmek istediğinden emin misin?",
confirmClearStatisticsMsg: "Tüm istatistikleri silmek istediğinden emin misin?", confirmClearStatisticsMsg: "Tüm istatistikleri silmek istediğinden emin misin?",
importHandleDescription: "Aynı isimdeki bütün servisleri ve bildirimleri atlamak için 'Var olanı atla' seçiniz. 'Üzerine yaz' var olan bütün servisleri ve bildirimleri silecektir. ",
confirmImportMsg: "Yedeği içeri aktarmak istediğinize emin misiniz? Lütfen doğru içeri aktarma seçeneğini seçtiğinizden emin olunuz. ",
twoFAVerifyLabel: "Lütfen tokeni yazarak 2FA doğrulamanın çalıştığından emin olunuz.",
tokenValidSettingsMsg: "Token geçerli! Şimdi 2FA ayarlarını kaydedebilirsiniz. ",
confirmEnableTwoFAMsg: "2FA'ı etkinleştirmek istediğinizden emin misiniz?",
confirmDisableTwoFAMsg: "2FA'ı devre dışı bırakmak istediğinize emin misiniz?",
Settings: "Ayarlar", Settings: "Ayarlar",
Dashboard: "Panel", Dashboard: "Panel",
"New Update": "Yeni Güncelleme", "New Update": "Yeni Güncelleme",
@ -25,6 +34,7 @@ export default {
Appearance: "Görünüm", Appearance: "Görünüm",
Theme: "Tema", Theme: "Tema",
General: "Genel", General: "Genel",
"Primary Base URL": "Birincil Temel URL",
Version: "Versiyon", Version: "Versiyon",
"Check Update On GitHub": "GitHub'da Güncellemeyi Kontrol Edin", "Check Update On GitHub": "GitHub'da Güncellemeyi Kontrol Edin",
List: "Liste", List: "Liste",
@ -47,8 +57,7 @@ export default {
Current: "Şu anda", Current: "Şu anda",
Uptime: "Çalışma zamanı", Uptime: "Çalışma zamanı",
"Cert Exp.": "Sertifika Süresi", "Cert Exp.": "Sertifika Süresi",
days: "günler", day: "gün | günler",
day: "gün",
"-day": "-gün", "-day": "-gün",
hour: "saat", hour: "saat",
"-hour": "-saat", "-hour": "-saat",
@ -62,10 +71,14 @@ export default {
Port: "Port", Port: "Port",
"Heartbeat Interval": "Servis Test Aralığı", "Heartbeat Interval": "Servis Test Aralığı",
Retries: "Yeniden deneme", Retries: "Yeniden deneme",
"Heartbeat Retry Interval": "Sağlık Durumları Tekrar Deneme Sıklığı",
Advanced: "Gelişmiş", Advanced: "Gelişmiş",
"Upside Down Mode": "Ters/Düz Modu", "Upside Down Mode": "Ters/Düz Modu",
"Max. Redirects": "Maksimum Yönlendirme", "Max. Redirects": "Maksimum Yönlendirme",
"Accepted Status Codes": "Kabul Edilen Durum Kodları", "Accepted Status Codes": "Kabul Edilen Durum Kodları",
"Push URL": "Push URL",
needPushEvery: "Bu URL'yi her {0} saniyede bir aramalısınız.",
pushOptionalParams: "İsteğe bağlı parametreler: {0}",
Save: "Kaydet", Save: "Kaydet",
Notifications: "Bildirimler", Notifications: "Bildirimler",
"Not available, please setup.": "Atanmış bildirim yöntemi yok. Ayarlardan belirleyebilirsiniz.", "Not available, please setup.": "Atanmış bildirim yöntemi yok. Ayarlardan belirleyebilirsiniz.",
@ -109,28 +122,19 @@ export default {
"Last Result": "En son sonuçlar", "Last Result": "En son sonuçlar",
"Create your admin account": "Yönetici hesabınızı oluşturun", "Create your admin account": "Yönetici hesabınızı oluşturun",
"Repeat Password": "Şifrenizi tekrar girin", "Repeat Password": "Şifrenizi tekrar girin",
respTime: "Cevap Süresi (ms)",
notAvailableShort: "N/A",
Create: "Yarat",
"Clear Data": "Verileri Temizle",
Events: "Olaylar",
Heartbeats: "Sağlık Durumları",
"Auto Get": "Otomatik Al",
retryCheckEverySecond: "{0} Saniyede bir dene.",
enableDefaultNotificationDescription: "Bu bildirim her yeni serviste aktif olacaktır. Bildirimi servisler için ayrı ayrı deaktive edebilirsiniz. ",
importHandleDescription: "Aynı isimdeki bütün servisleri ve bildirimleri atlamak için 'Var olanı atla' seçiniz. 'Üzerine yaz' var olan bütün servisleri ve bildirimleri silecektir. ",
confirmImportMsg: "Yedeği içeri aktarmak istediğinize emin misiniz? Lütfen doğru içeri aktarma seçeneğini seçtiğinizden emin olunuz. ",
twoFAVerifyLabel: "Lütfen tokeni yazarak 2FA doğrulamanın çalıştığından emin olunuz.",
tokenValidSettingsMsg: "Token geçerli! Şimdi 2FA ayarlarını kaydedebilirsiniz. ",
confirmEnableTwoFAMsg: "2FA'ı etkinleştirmek istediğinizden emin misiniz?",
confirmDisableTwoFAMsg: "2FA'ı devre dışı bırakmak istediğinize emin misiniz?",
"Heartbeat Retry Interval": "Sağlık Durumları Tekrar Deneme Sıklığı",
"Import Backup": "Yedeği içe aktar", "Import Backup": "Yedeği içe aktar",
"Export Backup": "Yedeği dışa aktar", "Export Backup": "Yedeği dışa aktar",
Export: "Dışa aktar", Export: "Dışa aktar",
Import: "İçe aktar", Import: "İçe aktar",
respTime: "Cevap Süresi (ms)",
notAvailableShort: "N/A",
"Default enabled": "Varsayılan etkinleştirilmiş", "Default enabled": "Varsayılan etkinleştirilmiş",
"Apply on all existing monitors": "Var olan bütün servislere uygula", "Apply on all existing monitors": "Var olan bütün servislere uygula",
Create: "Oluştur",
"Clear Data": "Verileri Temizle",
Events: "Olaylar",
Heartbeats: "Sağlık Durumları",
"Auto Get": "Otomatik Al",
backupDescription: "Bütün servisleri ve bildirimleri JSON dosyasına yedekleyebilirsiniz.", backupDescription: "Bütün servisleri ve bildirimleri JSON dosyasına yedekleyebilirsiniz.",
backupDescription2: "Not: Geçmiş ve etkinlik verileri içinde değildir.", backupDescription2: "Not: Geçmiş ve etkinlik verileri içinde değildir.",
backupDescription3: "Dışa aktarma dosyasında bildirim tokeni gibi hassas veriler bulunur, dikkatli bir şekilde saklayınız.", backupDescription3: "Dışa aktarma dosyasında bildirim tokeni gibi hassas veriler bulunur, dikkatli bir şekilde saklayınız.",
@ -149,4 +153,375 @@ export default {
"Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)", "Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)",
Active: "Aktif", Active: "Aktif",
Inactive: "İnaktif", Inactive: "İnaktif",
Token: "Token",
"Show URI": "URI'yi göster",
Tags: "Etiketler",
"Add New below or Select...": "Aşağıya Yeni Ekle veya Seç...",
"Tag with this name already exist.": "Bu ada sahip etiket zaten var.",
"Tag with this value already exist.": "Bu değere sahip etiket zaten var.",
color: "renk",
"value (optional)": "değer (isteğe bağlı)",
Gray: "Gri",
Red: "Kırmızı",
Orange: "Turuncu",
Green: "Yeşil",
Blue: "Mavi",
Indigo: "Çivit mavisi",
Purple: "Mor",
Pink: "Pembe",
"Search...": "Ara...",
"Avg. Ping": "Ortalama Ping",
"Avg. Response": "Ortalama Cevap Süresi",
"Entry Page": "Giriş Sayfası",
statusPageNothing: "Burada hiçbir şey yok, lütfen bir grup veya servis ekleyin.",
"No Services": "Hizmet Yok",
"All Systems Operational": "Tüm Sistemler Operasyonel",
"Partially Degraded Service": "Kısmen Bozulmuş Hizmet",
"Degraded Service": "Bozulmuş Hizmet",
"Add Group": "Grup Ekle",
"Add a monitor": "Servis Ekle",
"Edit Status Page": "Durum Sayfasını Düzenle",
"Go to Dashboard": "Panele Git",
"Status Page": "Durum Sayfası",
"Status Pages": "Durum Sayfaları",
defaultNotificationName: "My {notification} Alert ({number})",
here: "burada",
Required: "Gerekli",
telegram: "Telegram",
"Bot Token": "Bot Token",
wayToGetTelegramToken: "{0} adresinden bir token alabilirsiniz.",
"Chat ID": "Chat ID",
supportTelegramChatID: "Doğrudan Sohbet / Grup / Kanalın Sohbet Kimliğini Destekleyin",
wayToGetTelegramChatID: "Bot'a bir mesaj göndererek ve chat_id'yi görüntülemek için bu URL'ye giderek sohbet kimliğinizi alabilirsiniz:",
"YOUR BOT TOKEN HERE": "BOT TOKENİNİZ BURADA",
chatIDNotFound: "Chat ID bulunamadı; lütfen önce bu bota bir mesaj gönderin",
webhook: "Webhook",
"Post URL": "Post URL",
"Content Type": "Content Type",
webhookJsonDesc: "{0}, Express.js gibi tüm modern HTTP sunucuları için iyidir",
webhookFormDataDesc: "{multipart} PHP için iyidir. JSON'un {decodeFunction} ile ayrıştırılması gerekecek",
smtp: "E-mail (SMTP)",
secureOptionNone: "Hiçbiri / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "TLS Hatasını Yoksay",
"From Email": "E-postadan",
emailCustomSubject: "Özel Konu",
"To Email": "E-postaya",
smtpCC: "CC",
smtpBCC: "BCC",
discord: "Discord",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "Bunu Sunucu Ayarları -> Entegrasyonlar -> Webhook Oluştur'a giderek alabilirsiniz.",
"Bot Display Name": "Botun Görünecek Adı",
"Prefix Custom Message": "Önek Özel Mesaj",
"Hello @everyone is...": "Merhaba {'@'}everyone ...",
teams: "Microsoft Teams",
"Webhook URL": "Webhook URL",
wayToGetTeamsURL: "Bir webhook URL'sinin nasıl oluşturulacağını öğrenebilirsiniz {0}.",
signal: "Signal",
Number: "Numara",
Recipients: "Alıcılar",
needSignalAPI: "REST API ile bir signal istemciniz olması gerekiyor.",
wayToCheckSignalURL: "Nasıl kurulacağını görmek için bu URL'yi kontrol edebilirsiniz:",
signalImportant: "ÖNEMLİ: Alıcılarda grupları ve sayıları karıştıramazsınız!",
gotify: "Gotify",
"Application Token": "Uygulama Tokeni",
"Server URL": "Sunucu URL",
Priority: "Öncelik",
slack: "Slack",
"Icon Emoji": "İkon Emoji",
"Channel Name": "Kanal Adı",
"Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "Webhook hakkında daha fazla bilgi: {0}",
aboutChannelName: "Webhook kanalını atlamak istiyorsanız, {0} Kanal Adı alanına kanal adını girin. Ör: #diğer-kanal",
aboutKumaURL: "Uptime Kuma URL alanını boş bırakırsanız, varsayılan olarak Project GitHub sayfası olur.",
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 (50'den fazla Bildirim hizmetini destekler)",
GoogleChat: "Google Chat (sadece Google Workspace)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"User Key": "Kullancı Anahtarı",
Device: "Cihaz",
"Message Title": "Mesaj Başlığı",
"Notification Sound": "Bilgilendirme sesi",
"More info on:": "Daha fazla bilgi: {0}",
pushoverDesc1: "Acil durum önceliği (2), yeniden denemeler arasında varsayılan olarak 30 saniyelik bir zaman aşımına sahiptir ve 1 saat sonra sona erecektir.",
pushoverDesc2: "Farklı cihazlara bildirim göndermek istiyorsanız Cihaz alanını doldurunuz.",
"SMS Type": "SMS Tipi",
octopushTypePremium: "Premium (Hızlı - uyarı için önerilir)",
octopushTypeLowCost: "Düşük Maliyet (Yavaş - bazen operatör tarafından engellenir)",
checkPrice: "{0} fiyatlarını kontrol edin:",
apiCredentials: "API kimlik bilgileri",
octopushLegacyHint: "Octopush'un (2011-2020) eski sürümünü mü yoksa yeni sürümünü mü kullanıyorsunuz?",
"Check octopush prices": "Octopush fiyatlarını kontrol edin {0}.",
octopushPhoneNumber: "Telefon numarası (uluslararası biçim, örneğin: +33612345678) ",
octopushSMSSender: "SMS Gönderici Adı : 3-11 alfanümerik karakter ve boşluk (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea Cihaz ID",
"Apprise URL": "Apprise URL",
"Example:": "Örnek: {0}",
"Read more:": "Daha fazla oku: {0}",
"Status:": "Durum: {0}",
"Read more": "Daha fazla oku",
appriseInstalled: "Apprise yüklendi.",
appriseNotInstalled: "Appris yüklü değil. {0}",
"Access Token": "Erişim Tokeni",
"Channel access token": "Kanal erişim tokeni",
"Line Developers Console": "Line Geliştirici Konsolu",
lineDevConsoleTo: "Line Geliştirici Konsolu - {0}",
"Basic Settings": "Temel Ayarlar",
"User ID": "Kullanıcı ID",
"Messaging API": "Messaging API",
wayToGetLineChannelToken: "Önce {0}'e erişin, bir sağlayıcı ve kanal (Messaging API) oluşturun, ardından yukarıda belirtilen menü öğelerinden kanal erişim tokenini ve kullanıcı id alabilirsiniz.",
"Icon URL": "Simge URL",
aboutIconURL: "Varsayılan profil resmini geçersiz kılmak için \"Simge URL\" bölümünde bir resme bağlantı sağlayabilirsiniz. Simge Emojisi ayarlanmışsa kullanılmayacaktır.",
aboutMattermostChannelName: "Kanal adını \"Kanal Adı\" alanına girerek Webhook'un gönderi yaptığı varsayılan kanalı geçersiz kılabilirsiniz. Bunun Mattermost Webhook ayarlarında etkinleştirilmesi gerekir. Ör: #diğer-kanal",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - ucuz ama yavaş ve genellikle aşırı yüklü. Yalnızca Polonyalı alıcılarla sınırlıdır.",
promosmsTypeFlash: "SMS FLASH - Mesaj, alıcı cihazda otomatik olarak gösterilecektir. Yalnızca Polonyalı alıcılarla sınırlıdır.",
promosmsTypeFull: "SMS FULL - Premium SMS katmanı, Gönderici Adınızı kullanabilirsiniz (Önce adınızı kaydetmeniz gerekir). Uyarılar için güvenilir.",
promosmsTypeSpeed: "SMS HIZI - Sistemde en yüksek öncelik. Çok hızlı ve güvenilir ancak maliyetli (SMS FULL fiyatının yaklaşık iki katı).",
promosmsPhoneNumber: "Telefon numarası (Polonyalı alıcı için Alan kodlarını atlayabilirsiniz)",
promosmsSMSSender: "SMS Gönderici Adı : Ön kayıtlı ad veya varsayılanlardan biri: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "Homeserver URL (http(s):// ve isteğe bağlı olarak bağlantı noktası ile)",
"Internal Room Id": "Internal Room ID",
matrixDesc1: "Internal Room ID'sini, Matrix istemcinizdeki oda ayarlarının gelişmiş bölümüne bakarak bulabilirsiniz. !QMdRCpUIfLwsfjxye6:home.server gibi görünmelidir.",
matrixDesc2: "Hesabınıza ve katıldığınız tüm odalara tam erişime izin vereceğinden, yeni bir kullanıcı oluşturmanız ve kendi Matrix kullanıcınızın erişim belirtecini kullanmamanız şiddetle tavsiye edilir. Bunun yerine, yeni bir kullanıcı oluşturun ve onu yalnızca bildirimi almak istediğiniz odaya davet edin. {0} komutunu çalıştırarak erişim tokenini alabilirsiniz.",
Method: "Yöntem",
Body: "Gövde",
Headers: "Başlıklar",
PushUrl: "Push URL",
HeadersInvalidFormat: "İstek başlıkları geçerli JSON değil:",
BodyInvalidFormat: "İstek gövdesi geçerli JSON değil:",
"Monitor History": "Servis Geçmişi",
clearDataOlderThan: "{0} gün boyunca izleme geçmişi verilerini saklayın.",
PasswordsDoNotMatch: "Parolalar uyuşmuyor.",
records: "kayıtlar",
"One record": "Bir Kayıt",
steamApiKeyDescription: "Bir Steam Oyun Sunucusunu izlemek için bir Steam Web-API anahtarına ihtiyacınız vardır. API anahtarınızı buradan kaydedebilirsiniz: ",
"Current User": "Şu anki kullanıcı",
topic: "Başlık",
topicExplanation: "İzlenecek MQTT servisi",
successMessage: "Başarılı Mesaj",
successMessageExplanation: "Başarılı olarak kabul edilecek MQTT mesajı",
recent: "Son",
Done: "Tamamlandı",
Info: "Bilgi",
Security: "Güvenlik",
"Steam API Key": "Steam API Anahtarı",
"Shrink Database": "Veritabanını Küçült",
"Pick a RR-Type...": "Bir RR-Tipi seçin...",
"Pick Accepted Status Codes...": "Kabul Edilen Durum Kodlarını Seçin...",
Default: "Varsayılan",
"HTTP Options": "HTTP Ayarları",
"Create Incident": "Olay Oluştur",
Title: "Başlık",
Content: "İçerik",
Style: "Stil",
info: "info",
warning: "warning",
danger: "danger",
primary: "primary",
light: "light",
dark: "dark",
Post: "Post",
"Please input title and content": "Lütfen başlık ve içerik girin",
Created: "Oluşturuldu",
"Last Updated": "Son Güncelleme",
Unpin: "Unpin",
"Switch to Light Theme": "Açık Temaya Geç",
"Switch to Dark Theme": "Karanlık Temaya Geç",
"Show Tags": "Etiketleri Göster",
"Hide Tags": "Etiketleri Gizle",
Description: "Açıklama",
"No monitors available.": "Kullanılabilir servis yok.",
"Add one": "Bir tane ekle",
"No Monitors": "Servis Yok",
"Untitled Group": "Adsız Grup",
Services: "Hizmetler",
Discard: "İptal Et",
Cancel: "İptal Et",
"Powered by": "Powered by",
shrinkDatabaseDescription: "SQLite için veritabanı VACUUM'unu tetikleyin. Veritabanınız 1.10.0'dan sonra oluşturulduysa, AUTO_VACUUM zaten etkinleştirilmiştir ve bu eyleme gerek yoktur.",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API Kullanıcı Adı (webapi_ öneki dahil)",
serwersmsAPIPassword: "API Şifre",
serwersmsPhoneNumber: "Telefon numarası",
serwersmsSenderName: "SMS Gönderici Adı (müşteri portalı üzerinden kayıtlı)",
stackfield: "Stackfield",
Customize: "Özelleştirme",
"Custom Footer": "Özel Altbilgi",
"Custom CSS": "Özel CSS",
smtpDkimSettings: "DKIM Ayarları",
smtpDkimDesc: "Kullanım için lütfen Nodemailer DKIM'e {0} bakın.",
documentation: "belgeler",
smtpDkimDomain: "Alan adı",
smtpDkimKeySelector: "Anahtar Seçici",
smtpDkimPrivateKey: "Özel anahtar",
smtpDkimHashAlgo: "Hash Algoritması (Opsiyonel)",
smtpDkimheaderFieldNames: "İmzalanacak Başlık Anahtarları (Opsiyonel)",
smtpDkimskipFields: "İmzalamayacak Başlık Anahtarları (Opsiyonel)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "Environment",
alertaApiKey: "API Key",
alertaAlertState: "Uyarı Durumu",
alertaRecoverState: "Kurtarma Durumu",
deleteStatusPageMsg: "Bu durum sayfasını silmek istediğinizden emin misiniz?",
Proxies: "Proxy'ler",
default: "Varsayılan",
enabled: "Etkinleştirilmiş",
setAsDefault: "Varsayılan Olarak Ayarla",
deleteProxyMsg: "Bu proxy'yi tüm servisler için silmek istediğinizden emin misiniz?",
proxyDescription: "Proxy'lerin çalışması için bir servise atanması gerekir.",
enableProxyDescription: "Bu proxy, etkinleştirilene kadar izleme isteklerini etkilemeyecektir. Aktivasyon durumuna göre proxy'yi tüm servislerden geçici olarak devre dışı bırakabilirsiniz.",
setAsDefaultProxyDescription: "Bu proxy, yeni servisler için varsayılan olarak etkinleştirilecektir. Yine de proxy'yi her servis için ayrı ayrı devre dışı bırakabilirsiniz.",
"Certificate Chain": "Sertifika Zinciri",
Valid: "Geçerli",
Invalid: "Geçersiz",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "PhoneNumbers",
TemplateCode: "TemplateCode",
SignName: "SignName",
"Sms template must contain parameters: ": "Sms şablonu parametreleri içermelidir:",
"Bark Endpoint": "Bark Endpoint",
WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey",
"For safety, must use secret key": "Güvenlik için gizli anahtar kullanılmalıdır",
"Device Token": "Cihaz Tokeni",
Platform: "Platform",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "High",
Retry: "Retry",
Topic: "Topic",
"WeCom Bot Key": "WeCom Bot Key",
"Setup Proxy": "Proxy kur",
"Proxy Protocol": "Proxy Protokolü",
"Proxy Server": "Proxy Sunucusu",
"Proxy server has authentication": "Proxy sunucusunun kimlik doğrulaması var",
User: "Kullanıcı",
Installed: "Yüklenmiş",
"Not installed": "Yüklü değil",
Running: "Çalışıyor",
"Not running": "Çalışmıyor",
"Remove Token": "Tokeni Kaldır",
Start: "Başlat",
Stop: "Durdur",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "Yeni Durum Sayfası Ekle",
Slug: "Slug",
"Accept characters:": "Kabul edilen karakterler:",
startOrEndWithOnly: "Yalnızca {0} ile başlayın veya bitirin",
"No consecutive dashes": "Ardışık tire yok",
Next: "Sonraki",
"The slug is already taken. Please choose another slug.": "Slug zaten alındı. Lütfen başka bir slug seçin.",
"No Proxy": "Proxy Yok",
"HTTP Basic Auth": "HTTP Temel Yetkilendirme",
"New Status Page": "Yeni Durum Sayfası",
"Page Not Found": "Sayfa bulunamadı",
"Reverse Proxy": "Ters Proxy",
Backup: "Yedek",
About: "Hakkında",
wayToGetCloudflaredURL: "(Cloudflared'i {0} adresinden indirin)",
cloudflareWebsite: "Cloudflare Website",
"Message:": "Mesaj:",
"Don't know how to get the token? Please read the guide:": "Tokeni nasıl alacağınızı bilmiyor musunuz? Lütfen kılavuzu okuyun:",
"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.": "Halihazırda Cloudflare Tüneli üzerinden bağlanıyorsanız mevcut bağlantı kesilebilir. Durdurmak istediğinden emin misin? Onaylamak için mevcut şifrenizi yazın.",
"Other Software": "Diğer Yazılımlar",
"For example: nginx, Apache and Traefik.": "Örneğin: nginx, Apache ve Traefik.",
"Please read": "Lütfen oku",
"Subject:": "Başlık:",
"Valid To:": "Geçerlilik:",
"Days Remaining:": "Kalan günler:",
"Issuer:": "Veren:",
"Fingerprint:": "Parmak izi:",
"No status pages": "Durum sayfası yok",
"Domain Name Expiry Notification": "Alan Adı Sona Erme Bildirimi",
Proxy: "Proxy",
"Date Created": "Tarih Oluşturuldu",
onebotHttpAddress: "OneBot HTTP Adresi",
onebotMessageType: "OneBot Mesaj Türü",
onebotGroupMessage: "Grup",
onebotPrivateMessage: "Özel",
onebotUserOrGroupId: "Grup/Kullanıcı Kimliği",
onebotSafetyTips: "Güvenlik için erişim tokeni ayarlamalısınız",
"PushDeer Key": "PushDeer Anahtarı",
"Footer Text": "Altbilgi metni",
"Show Powered By": "\"Powered by\" kısmını göster",
"Domain Names": "Alan isimleri",
signedInDisp: "{0} olarak oturum açıldı",
signedInDispDisabled: "Yetkilendirme Devre Dışı.",
"Certificate Expiry Notification": "Sertifika Sona Erme Bildirimi",
"API Username": "API Kullanıc Adı",
"API Key": "API Anahtarı",
"Recipient Number": "Alıcı Numarası",
"From Name/Number": "İsimden/Numaradan",
"Leave blank to use a shared sender number.": "Paylaşılan bir gönderen numarası kullanmak için boş bırakın.",
"Octopush API Version": "Octopush API Sürümü",
"Legacy Octopush-DM": "Eski Octopush-DM",
"endpoint": "endpoint",
octopushAPIKey: "Kontrol panelindeki HTTP API kimlik bilgilerinden \"API Key\"",
octopushLogin: "Kontrol panelindeki HTTP API kimlik bilgilerinden \"Login\"",
promosmsLogin: "API Oturum Açma Adı",
promosmsPassword: "API Şifresi",
"pushoversounds pushover": "Pushover (varsayılan)",
"pushoversounds bike": "Bisiklet",
"pushoversounds bugle": "Boru",
"pushoversounds cashregister": "Yazar kasa",
"pushoversounds classical": "Klasik",
"pushoversounds cosmic": "Kozmik",
"pushoversounds falling": "Düşme",
"pushoversounds gamelan": "Oyun Alanı",
"pushoversounds incoming": "Gelen",
"pushoversounds intermission": "Ara",
"pushoversounds magic": "Büyü",
"pushoversounds mechanical": "Mekanik",
"pushoversounds pianobar": "Piano",
"pushoversounds siren": "Siren",
"pushoversounds spacealarm": "Uzay Alarmı",
"pushoversounds tugboat": "Römorkör",
"pushoversounds alien": "Uzaylı Alarmı (uzun)",
"pushoversounds climb": "Tırmanış (uzun)",
"pushoversounds persistent": "Sürekli (uzun)",
"pushoversounds echo": "Pushover Yankı (uzun)",
"pushoversounds updown": "Yukarı Aşağı (uzun)",
"pushoversounds vibrate": "Sadece titreşim",
"pushoversounds none": "Yok (sessiz)",
pushyAPIKey: "Gizli API Anahtarı",
pushyToken: "Cihaz tokeni",
"Show update if available": "Varsa güncellemeyi göster",
"Also check beta release": "Ayrıca beta sürümünü kontrol edin",
"Using a Reverse Proxy?": "Ters Proxy mi Kullanıyorsunuz?",
"Check how to config it for WebSocket": "WebSocket için nasıl yapılandırılacağını kontrol edin",
"Steam Game Server": "Steam Oyun Sunucusu",
"Most likely causes:": "En olası nedenler:",
"The resource is no longer available.": "Kaynak artık mevcut değil.",
"There might be a typing error in the address.": "Adreste bir yazım hatası olabilir.",
"What you can try:": "Ne deneyebilirsin:",
"Retype the address.": "Adresi tekrar yazın.",
"Go back to the previous page.": "Bir önceki sayfaya geri git.",
"Coming Soon": "Yakında gelecek",
wayToGetClickSendSMSToken: "API Kullanıcı Adı ve API Anahtarını {0} adresinden alabilirsiniz.",
error: "hata",
critical: "kritik",
wayToGetPagerDutyKey: "Bunu şuraya giderek alabilirsiniz: Servis -> Servis Dizini -> (Bir servis seçin) -> Entegrasyonlar -> Entegrasyon ekle. Burada \"Events API V2\" için arama yapabilirsiniz. Daha fazla bilgi {0}",
"Integration Key": "Entegrasyon Anahtarı",
"Integration URL": "Entegrasyon URL",
"Auto resolve or acknowledged": "Otomatik çözümleme veya onaylama",
"do nothing": "hiçbir şey yapma",
"auto acknowledged": "otomatik onaylama",
"auto resolve": "otomatik çözümleme",
}; };

@ -44,8 +44,7 @@ export default {
Current: "Поточний", Current: "Поточний",
Uptime: "Аптайм", Uptime: "Аптайм",
"Cert Exp.": "Сертифікат спливає", "Cert Exp.": "Сертифікат спливає",
days: "днів", day: "день | днів",
day: "день",
"-day": " днів", "-day": " днів",
hour: "година", hour: "година",
"-hour": " години", "-hour": " години",

@ -56,7 +56,6 @@ export default {
Current: "Hiện tại", Current: "Hiện tại",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Cert hết hạn", "Cert Exp.": "Cert hết hạn",
days: "ngày",
day: "ngày", day: "ngày",
"-day": "-ngày", "-day": "-ngày",
hour: "giờ", hour: "giờ",

@ -57,7 +57,6 @@ export default {
Current: "当前", Current: "当前",
Uptime: "在线时间", Uptime: "在线时间",
"Cert Exp.": "证书有效期", "Cert Exp.": "证书有效期",
days: "天",
day: "天", day: "天",
"-day": " 天", "-day": " 天",
hour: "小时", hour: "小时",
@ -88,7 +87,7 @@ export default {
Dark: "黑暗", Dark: "黑暗",
Auto: "自动", Auto: "自动",
"Theme - Heartbeat Bar": "主题 - 心跳栏", "Theme - Heartbeat Bar": "主题 - 心跳栏",
Normal: "正常", // 此处还供 Gorush 的通知优先级功能使用,不应翻译为“正常显示” Normal: "正常",
Bottom: "靠下", Bottom: "靠下",
None: "不显示", None: "不显示",
Timezone: "时区", Timezone: "时区",
@ -398,11 +397,9 @@ export default {
Invalid: "无效", Invalid: "无效",
AccessKeyId: "AccessKey ID", AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret", SecretAccessKey: "AccessKey Secret",
/* 以下为阿里云短信服务 API Dysms#SendSms 的参数 */
PhoneNumbers: "PhoneNumbers", PhoneNumbers: "PhoneNumbers",
TemplateCode: "TemplateCode", TemplateCode: "TemplateCode",
SignName: "SignName", SignName: "SignName",
/* 以上为阿里云短信服务 API Dysms#SendSms 的参数 */
"Bark Endpoint": "Bark 接入点", "Bark Endpoint": "Bark 接入点",
"Device Token": "Apple Device Token", "Device Token": "Apple Device Token",
Platform: "平台", Platform: "平台",
@ -441,7 +438,7 @@ export default {
"No Proxy": "无代理", "No Proxy": "无代理",
"HTTP Basic Auth": "HTTP 基础身份验证", "HTTP Basic Auth": "HTTP 基础身份验证",
"New Status Page": "新的状态页", "New Status Page": "新的状态页",
"Page Not Found": "状态页未找到", "Page Not Found": "未找到该页面",
"Reverse Proxy": "反向代理", "Reverse Proxy": "反向代理",
"Subject:": "颁发给:", "Subject:": "颁发给:",
"Valid To:": "有效期至:", "Valid To:": "有效期至:",
@ -469,4 +466,67 @@ export default {
"Footer Text": "底部自定义文本", "Footer Text": "底部自定义文本",
"Show Powered By": "显示 Powered By", "Show Powered By": "显示 Powered By",
"Domain Names": "域名", "Domain Names": "域名",
"Certificate Expiry Notification": "证书到期时通知",
"API Username": "API 凭证 Username",
"API Key": "API 凭证 Key",
"Recipient Number": "收件人手机号码",
"From Name/Number": "发件人名称/手机号码",
"Leave blank to use a shared sender number.": "留空以使用平台共享的发件人手机号码",
"Octopush API Version": "Octopush API 版本",
"Legacy Octopush-DM": "旧版本 Octopush-DM",
endpoint: "接入点",
octopushAPIKey: "控制台 HTTP API credentials 里的 \"API key\"",
octopushLogin: "控制台 HTTP API credentials 里的 \"Login\"",
promosmsLogin: "API 登录名",
promosmsPassword: "API 密码",
"pushoversounds pushover": "Pushover默认",
"pushoversounds bike": "Bike",
"pushoversounds bugle": "Bugle",
"pushoversounds cashregister": "Cash Register",
"pushoversounds classical": "Classical",
"pushoversounds cosmic": "Cosmic",
"pushoversounds falling": "Falling",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Incoming",
"pushoversounds intermission": "Intermission",
"pushoversounds magic": "Magic",
"pushoversounds mechanical": "Mechanical",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Siren",
"pushoversounds spacealarm": "Space Alarm",
"pushoversounds tugboat": "Tug Boat",
"pushoversounds alien": "Alien Alarm长铃声",
"pushoversounds climb": "Climb长铃声",
"pushoversounds persistent": "Persistent长铃声",
"pushoversounds echo": "Pushover Echo长铃声",
"pushoversounds updown": "Up Down长铃声",
"pushoversounds vibrate": "仅震动",
"pushoversounds none": "无(禁音)",
pushyAPIKey: "API 密钥",
pushyToken: "设备 Token",
"Show update if available": "有更新时通知",
"Also check beta release": "一并检查 Beta 版更新",
"Using a Reverse Proxy?": "正在使用反向代理?",
"Check how to config it for WebSocket": "查看如何将反向代理与 WebSocket 一起使用",
"Steam Game Server": "Steam 游戏服务器",
"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: "您可以从 {0} 获取 API 凭证 Username 和 凭证 Key。",
signedInDisp: "当前用户: {0}",
signedInDispDisabled: "已禁用身份验证",
dnsPortDescription: "DNS 服务器端口,默认为 53你可以在任何时候更改此端口.",
error: "错误",
critical: "关键",
wayToGetPagerDutyKey: "你可以在 Service -> Service Directory -> (Select a service) -> Integrations -> Add integration 页面中搜索 \"Events API V2\" 以获取此 Integration Key更多信息请参见 {0}",
"Integration Key": "Integration Key",
"Integration URL": "Integration URL",
"Auto resolve or acknowledged": "自动标记为已解决或已读",
"do nothing": "不做任何操作",
"auto acknowledged": "自动标记为已读",
"auto resolve": "自动标记为已解决",
}; };

@ -30,7 +30,6 @@ export default {
Current: "目前", Current: "目前",
Uptime: "上線率", Uptime: "上線率",
"Cert Exp.": "証書期限", "Cert Exp.": "証書期限",
days: "日",
day: "日", day: "日",
"-day": "日", "-day": "日",
hour: "小時", hour: "小時",

@ -56,7 +56,6 @@ export default {
Current: "目前", Current: "目前",
Uptime: "運作率", Uptime: "運作率",
"Cert Exp.": "憑證期限", "Cert Exp.": "憑證期限",
days: "天",
day: "天", day: "天",
"-day": "天", "-day": "天",
hour: "小時", hour: "小時",

@ -4,7 +4,7 @@
<div class="container-fluid"> <div class="container-fluid">
{{ $root.connectionErrorMsg }} {{ $root.connectionErrorMsg }}
<div v-if="$root.showReverseProxyGuide"> <div v-if="$root.showReverseProxyGuide">
Using a Reverse Proxy? <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">Check how to config it for WebSocket</a> {{ $t("Using a Reverse Proxy?") }} <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">{{ $t("Check how to config it for WebSocket") }}</a>
</div> </div>
</div> </div>
</div> </div>

@ -77,7 +77,7 @@
<h4>{{ $t("Cert Exp.") }}</h4> <h4>{{ $t("Cert Exp.") }}</h4>
<p>(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p> <p>(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
<span class="num"> <span class="num">
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $t("days") }}</a> <a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $tc("day", tlsInfo.certInfo.daysRemaining) }}</a>
</span> </span>
</div> </div>
</div> </div>

@ -11,30 +11,41 @@
<div class="my-3"> <div class="my-3">
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label> <label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
<select id="type" v-model="monitor.type" class="form-select"> <select id="type" v-model="monitor.type" class="form-select">
<option value="http"> <optgroup label="General Monitor Type">
HTTP(s) <option value="http">
</option> HTTP(s)
<option value="port"> </option>
TCP Port <option value="port">
</option> TCP Port
<option value="ping"> </option>
Ping <option value="ping">
</option> Ping
<option value="keyword"> </option>
HTTP(s) - {{ $t("Keyword") }} <option value="keyword">
</option> HTTP(s) - {{ $t("Keyword") }}
<option value="dns"> </option>
DNS <option value="dns">
</option> DNS
<option value="push"> </option>
Push </optgroup>
</option>
<option value="steam"> <optgroup label="Passive Monitor Type">
Steam Game Server <option value="push">
</option> Push
<option value="mqtt"> </option>
MQTT </optgroup>
</option>
<optgroup label="Specific Monitor Type">
<option value="steam">
{{ $t("Steam Game Server") }}
</option>
<option value="mqtt">
MQTT
</option>
<option value="sqlserver">
SQL Server
</option>
</optgroup>
</select> </select>
</div> </div>
@ -94,6 +105,15 @@
</div> </div>
</div> </div>
<!-- Port -->
<div class="my-3">
<label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
<div class="form-text">
{{ $t("dnsPortDescription") }}
</div>
</div>
<div class="my-3"> <div class="my-3">
<label for="dns_resolve_type" class="form-label">{{ $t("Resource Record Type") }}</label> <label for="dns_resolve_type" class="form-label">{{ $t("Resource Record Type") }}</label>
@ -148,6 +168,18 @@
</div> </div>
</template> </template>
<!-- SQL Server -->
<template v-if="monitor.type === 'sqlserver'">
<div class="my-3">
<label for="sqlserverConnectionString" class="form-label">SQL Server {{ $t("Connection String") }}</label>
<input id="sqlserverConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
</div>
<div class="my-3">
<label for="sqlserverQuery" class="form-label">SQL Server {{ $t("Query") }}</label>
<textarea id="sqlserverQuery" v-model="monitor.databaseQuery" class="form-control" placeholder="Example: select getdate()"></textarea>
</div>
</template>
<!-- Interval --> <!-- Interval -->
<div class="my-3"> <div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label> <label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
@ -497,6 +529,15 @@ export default {
this.monitor.pushToken = genSecret(10); this.monitor.pushToken = genSecret(10);
} }
} }
// Set default port for DNS if not already defined
if (! this.monitor.port || this.monitor.port === "53") {
if (this.monitor.type === "dns") {
this.monitor.port = "53";
} else {
this.monitor.port = "";
}
}
} }
}, },

@ -22,16 +22,17 @@
</div> </div>
<div class="guide"> <div class="guide">
Most likely causes: {{ $t("Most likely causes:") }}
<ul> <ul>
<li>The resource is no longer available.</li> <li>{{ $t("The resource is no longer available.") }}</li>
<li>There might be a typing error in the address.</li> <li>{{ $t("There might be a typing error in the address.") }}</li>
</ul> </ul>
What you can try:<br /> {{ $t("What you can try:") }}<br />
<ul> <ul>
<li>Retype the address.</li> <li>{{ $t("Retype the address.") }}</li>
<li><a href="#" class="go-back" @click="goBack()">Go back to the previous page.</a></li> <li><a href="#" class="go-back" @click="goBack()">{{ $t("Go back to the previous page.") }}</a></li>
<li><a href="/" class="go-back">Go back to home page.</a></li>
</ul> </ul>
</div> </div>
</div> </div>

@ -145,6 +145,10 @@ export default {
this.settings.keepDataPeriodDays = 180; this.settings.keepDataPeriodDays = 180;
} }
if (this.settings.tlsExpiryNotifyDays === undefined) {
this.settings.tlsExpiryNotifyDays = [];
}
this.settingsLoaded = true; this.settingsLoaded = true;
}); });
}, },

@ -45,7 +45,7 @@
</div> </div>
<div v-if="false" class="my-3"> <div v-if="false" class="my-3">
<label for="password" class="form-label">{{ $t("Password") }} <sup>Coming Soon</sup></label> <label for="password" class="form-label">{{ $t("Password") }} <sup>{{ $t("Coming Soon") }}</sup></label>
<input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control"> <input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control">
</div> </div>
@ -98,7 +98,7 @@
<h1 class="mb-4 title-flex"> <h1 class="mb-4 title-flex">
<!-- Logo --> <!-- Logo -->
<span class="logo-wrapper" @click="showImageCropUploadMethod"> <span class="logo-wrapper" @click="showImageCropUploadMethod">
<img :src="logoURL" alt class="logo me-2" :class="logoClass" @load="statusPageLogoLoaded" /> <img :src="logoURL" alt class="logo me-2" :class="logoClass" />
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" /> <font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
</span> </span>
@ -538,7 +538,7 @@ export default {
this.slug = "default"; this.slug = "default";
} }
axios.get("/api/status-page/" + this.slug).then((res) => { this.getData().then((res) => {
this.config = res.data.config; this.config = res.data.config;
if (!this.config.domainNameList) { if (!this.config.domainNameList) {
@ -551,6 +551,11 @@ export default {
this.incident = res.data.incident; this.incident = res.data.incident;
this.$root.publicGroupList = res.data.publicGroupList; this.$root.publicGroupList = res.data.publicGroupList;
}).catch( function (error) {
if (error.response.status === 404) {
location.href = "/page-not-found";
}
console.log(error);
}); });
// 5mins a loop // 5mins a loop
@ -567,6 +572,21 @@ export default {
}, },
methods: { methods: {
/**
* Get status page data
* It should be preloaded in window.preloadData
* @returns {Promise<any>}
*/
getData: function () {
if (window.preloadData) {
return new Promise(resolve => resolve({
data: window.preloadData
}));
} else {
return axios.get("/api/status-page/" + this.slug);
}
},
highlighter(code) { highlighter(code) {
return highlight(code, languages.css); return highlight(code, languages.css);
}, },
@ -604,6 +624,9 @@ export default {
this.$root.initSocketIO(true); this.$root.initSocketIO(true);
this.enableEditMode = true; this.enableEditMode = true;
this.clickedEditButton = true; this.clickedEditButton = true;
// Try to fix #1658
this.loadedData = true;
} }
}, },
@ -687,11 +710,6 @@ export default {
} }
}, },
statusPageLogoLoaded(eventPayload) {
// Remark: may not work in dev, due to cros
favicon.image(eventPayload.target);
},
createIncident() { createIncident() {
this.enableEditIncidentMode = true; this.enableEditIncidentMode = true;

@ -1,4 +1,5 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import EmptyLayout from "./layouts/EmptyLayout.vue"; import EmptyLayout from "./layouts/EmptyLayout.vue";
import Layout from "./layouts/Layout.vue"; import Layout from "./layouts/Layout.vue";
import Dashboard from "./pages/Dashboard.vue"; import Dashboard from "./pages/Dashboard.vue";
@ -8,21 +9,22 @@ import EditMonitor from "./pages/EditMonitor.vue";
import List from "./pages/List.vue"; import List from "./pages/List.vue";
const Settings = () => import("./pages/Settings.vue"); const Settings = () => import("./pages/Settings.vue");
import Setup from "./pages/Setup.vue"; import Setup from "./pages/Setup.vue";
const StatusPage = () => import("./pages/StatusPage.vue"); import StatusPage from "./pages/StatusPage.vue";
import Entry from "./pages/Entry.vue"; import Entry from "./pages/Entry.vue";
import ManageStatusPage from "./pages/ManageStatusPage.vue";
import AddStatusPage from "./pages/AddStatusPage.vue";
import NotFound from "./pages/NotFound.vue";
// Settings - Sub Pages
import Appearance from "./components/settings/Appearance.vue"; import Appearance from "./components/settings/Appearance.vue";
import General from "./components/settings/General.vue"; import General from "./components/settings/General.vue";
import Notifications from "./components/settings/Notifications.vue"; const Notifications = () => import("./components/settings/Notifications.vue");
import ReverseProxy from "./components/settings/ReverseProxy.vue"; import ReverseProxy from "./components/settings/ReverseProxy.vue";
import MonitorHistory from "./components/settings/MonitorHistory.vue"; import MonitorHistory from "./components/settings/MonitorHistory.vue";
import Security from "./components/settings/Security.vue"; const Security = () => import("./components/settings/Security.vue");
import Proxies from "./components/settings/Proxies.vue"; import Proxies from "./components/settings/Proxies.vue";
import Backup from "./components/settings/Backup.vue"; import Backup from "./components/settings/Backup.vue";
import About from "./components/settings/About.vue"; import About from "./components/settings/About.vue";
import ManageStatusPage from "./pages/ManageStatusPage.vue";
import AddStatusPage from "./pages/AddStatusPage.vue";
import NotFound from "./pages/NotFound.vue";
const routes = [ const routes = [
{ {

@ -1,5 +1,9 @@
const { genSecret } = require("../src/util"); const { genSecret, DOWN } = require("../src/util");
const utilServerRewire = require("../server/util-server"); const utilServerRewire = require("../server/util-server");
const Discord = require("../server/notification-providers/discord");
const axios = require("axios");
jest.mock("axios");
describe("Test parseCertificateInfo", () => { describe("Test parseCertificateInfo", () => {
it("should handle undefined", async () => { it("should handle undefined", async () => {
@ -155,7 +159,6 @@ describe("Test genSecret", () => {
expect(secret).toContain("A"); expect(secret).toContain("A");
expect(secret).toContain("9"); expect(secret).toContain("9");
}); });
}); });
describe("Test reset-password", () => { describe("Test reset-password", () => {
@ -164,25 +167,61 @@ describe("Test reset-password", () => {
}, 120000); }, 120000);
}); });
describe("Test Discord Notification Provider", () => {
const hostname = "discord.com";
const port = 1337;
const sendNotification = async (hostname, port, type) => {
const discordProvider = new Discord();
axios.post.mockResolvedValue({});
await discordProvider.send(
{
discordUsername: "Uptime Kuma",
discordWebhookUrl: "https://discord.com",
},
"test message",
{
type,
hostname,
port,
},
{
status: DOWN,
}
);
};
it("should send hostname for ping monitors", async () => {
await sendNotification(hostname, null, "ping");
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(hostname);
});
it.each([ "dns", "port", "steam" ])("should send hostname for %p monitors", async (type) => {
await sendNotification(hostname, port, type);
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(`${hostname}:${port}`);
});
});
describe("The function filterAndJoin", () => { describe("The function filterAndJoin", () => {
it("should join and array of strings to one string", () => { it("should join and array of strings to one string", () => {
const result = utilServerRewire.filterAndJoin(["one", "two", "three"]); const result = utilServerRewire.filterAndJoin([ "one", "two", "three" ]);
expect(result).toBe("onetwothree"); expect(result).toBe("onetwothree");
}); });
it("should join strings using a given connector", () => { it("should join strings using a given connector", () => {
const result = utilServerRewire.filterAndJoin(["one", "two", "three"], "-"); const result = utilServerRewire.filterAndJoin([ "one", "two", "three" ], "-");
expect(result).toBe("one-two-three"); expect(result).toBe("one-two-three");
}); });
it("should filter null, undefined and empty strings before joining", () => { it("should filter null, undefined and empty strings before joining", () => {
const result = utilServerRewire.filterAndJoin([undefined, "", "three"], "--"); const result = utilServerRewire.filterAndJoin([ undefined, "", "three" ], "--");
expect(result).toBe("three"); expect(result).toBe("three");
}); });
it("should return an empty string if all parts are filtered out", () => { it("should return an empty string if all parts are filtered out", () => {
const result = utilServerRewire.filterAndJoin([undefined, "", ""], "--"); const result = utilServerRewire.filterAndJoin([ undefined, "", "" ], "--");
expect(result).toBe(""); expect(result).toBe("");
}); });
}); });

Loading…
Cancel
Save