Merge branch 'master' of github.com:rmtsrc/uptime-kuma into add-home-assistant-notification

pull/1836/head
rmt/src 2 years ago
commit f091e92c70
No known key found for this signature in database
GPG Key ID: 6DD597637A8B880A

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

@ -151,9 +151,9 @@ You can discuss or ask for help in [issues](https://github.com/louislam/uptime-k
### Subreddit ### Subreddit
My Reddit account: louislamlam My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam).
You can mention me if you ask a question on Reddit. You can mention me if you ask a question on Reddit.
https://www.reddit.com/r/UptimeKuma/ [r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
## Contribute ## Contribute

@ -11,6 +11,9 @@ const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i;
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
define: {
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
},
plugins: [ plugins: [
vue(), vue(),
legacy({ legacy({

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

@ -4,5 +4,5 @@ WORKDIR /app
# Install apprise, iputils for non-root ping, setpriv # Install apprise, iputils for non-root ping, setpriv
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \ RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
pip3 --no-cache-dir install apprise==0.9.8.3 && \ pip3 --no-cache-dir install apprise==0.9.9 && \
rm -rf /root/.cache rm -rf /root/.cache

@ -11,7 +11,7 @@ WORKDIR /app
RUN apt update && \ RUN apt update && \
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
sqlite3 iputils-ping util-linux dumb-init && \ sqlite3 iputils-ping util-linux dumb-init && \
pip3 --no-cache-dir install apprise==0.9.8.3 && \ pip3 --no-cache-dir install apprise==0.9.9 && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove apt --yes autoremove

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

7571
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -61,22 +61,15 @@
"build-dist-and-restart": "npm run build && npm run start-server-dev" "build-dist-and-restart": "npm run build && npm run start-server-dev"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "~1.2.36",
"@fortawesome/free-regular-svg-icons": "~5.15.4",
"@fortawesome/free-solid-svg-icons": "~5.15.4",
"@fortawesome/vue-fontawesome": "~3.0.0-5",
"@louislam/sqlite3": "~15.0.6", "@louislam/sqlite3": "~15.0.6",
"@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0", "args-parser": "~1.3.0",
"axios": "~0.26.1", "axios": "~0.26.1",
"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",
"bootstrap": "5.1.3",
"bree": "~7.1.5", "bree": "~7.1.5",
"cacheable-lookup": "~6.0.4",
"chardet": "^1.3.0", "chardet": "^1.3.0",
"chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0",
"check-password-strength": "^2.0.5", "check-password-strength": "^2.0.5",
"cheerio": "^1.0.0-rc.10", "cheerio": "^1.0.0-rc.10",
"chroma-js": "^2.1.2", "chroma-js": "^2.1.2",
@ -87,7 +80,6 @@
"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", "express-static-gzip": "^2.1.7",
"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",
"http-proxy-agent": "^5.0.0", "http-proxy-agent": "^5.0.0",
@ -102,62 +94,72 @@
"nodemailer": "~6.6.5", "nodemailer": "~6.6.5",
"notp": "~2.0.3", "notp": "~2.0.3",
"password-hash": "~1.2.2", "password-hash": "~1.2.2",
"postcss-rtlcss": "~3.4.1", "pg": "^8.7.3",
"postcss-scss": "~4.0.3", "pg-connection-string": "^2.5.0",
"prismjs": "^1.27.0",
"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",
"redbean-node": "0.1.4", "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",
"tar": "^6.1.11", "tar": "^6.1.11",
"tcp-ping": "~0.1.1", "tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2", "thirty-two": "~1.0.2"
"timezones-list": "~3.0.1",
"v-pagination-3": "~0.1.7",
"vue": "next",
"vue-chart-3": "3.0.9",
"vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "~3.0.4",
"vue-i18n": "~9.1.9",
"vue-image-crop-upload": "~3.0.3",
"vue-multiselect": "~3.0.0-alpha.2",
"vue-prism-editor": "^2.0.0-alpha.2",
"vue-qrcode": "~1.0.0",
"vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5",
"vuedraggable": "~4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "~5.0.1", "@actions/github": "~5.0.1",
"@babel/eslint-parser": "~7.17.0", "@babel/eslint-parser": "~7.17.0",
"@babel/preset-env": "^7.15.8", "@babel/preset-env": "^7.15.8",
"@fortawesome/fontawesome-svg-core": "~1.2.36",
"@fortawesome/free-regular-svg-icons": "~5.15.4",
"@fortawesome/free-solid-svg-icons": "~5.15.4",
"@fortawesome/vue-fontawesome": "~3.0.0-5",
"@popperjs/core": "~2.10.2",
"@types/bootstrap": "~5.1.9", "@types/bootstrap": "~5.1.9",
"@vitejs/plugin-legacy": "~1.8.2", "@vitejs/plugin-legacy": "~1.8.2",
"@vitejs/plugin-vue": "~2.3.3", "@vitejs/plugin-vue": "~2.3.3",
"@vue/compiler-sfc": "~3.2.36", "@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",
"bootstrap": "5.1.3",
"chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.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",
"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",
"favico.js": "^0.3.10",
"jest": "~27.2.5", "jest": "~27.2.5",
"jest-puppeteer": "~6.0.3", "jest-puppeteer": "~6.0.3",
"npm-check-updates": "^14.1.1",
"postcss-html": "^1.3.1", "postcss-html": "^1.3.1",
"postcss-rtlcss": "~3.4.1",
"postcss-scss": "~4.0.3",
"prismjs": "^1.27.0",
"puppeteer": "~13.1.3", "puppeteer": "~13.1.3",
"qrcode": "~1.5.0",
"rollup-plugin-visualizer": "^5.6.0", "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",
"timezones-list": "~3.0.1",
"typescript": "~4.4.4", "typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
"vite": "~2.9.9", "vite": "~2.9.9",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vue": "next",
"vue-chart-3": "3.0.9",
"vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "~3.0.4",
"vue-i18n": "~9.1.9",
"vue-image-crop-upload": "~3.0.3",
"vue-multiselect": "~3.0.0-alpha.2",
"vue-prism-editor": "^2.0.0-alpha.2",
"vue-qrcode": "~1.0.0",
"vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5",
"vuedraggable": "~4.1.0",
"wait-on": "^6.0.1" "wait-on": "^6.0.1"
} }
} }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 893 B

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

@ -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-clickable-status-page-link.sql": true,
"patch-add-sqlserver-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" ] },
}; };
@ -177,7 +178,13 @@ class Database {
} else { } else {
log.info("db", "Database patch is needed"); log.info("db", "Database patch is needed");
try {
this.backup(version); this.backup(version);
} catch (e) {
log.error("db", e);
log.error("db", "Unable to create a backup before patching the database. Please make sure you have enough space and permission.");
process.exit(1);
}
// Try catch anything here, if gone wrong, restore the backup // Try catch anything here, if gone wrong, restore the backup
try { try {
@ -445,6 +452,23 @@ class Database {
this.backupWalPath = walPath + ".bak" + version; this.backupWalPath = walPath + ".bak" + version;
fs.copyFileSync(walPath, this.backupWalPath); fs.copyFileSync(walPath, this.backupWalPath);
} }
// Double confirm if all files actually backup
if (!fs.existsSync(this.backupPath)) {
throw new Error("Backup failed! " + this.backupPath);
}
if (fs.existsSync(shmPath)) {
if (!fs.existsSync(this.backupShmPath)) {
throw new Error("Backup failed! " + this.backupShmPath);
}
}
if (fs.existsSync(walPath)) {
if (!fs.existsSync(this.backupWalPath)) {
throw new Error("Backup failed! " + this.backupWalPath);
}
}
} }
} }

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

@ -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, mssqlQuery, mqttAsync, setSetting, httpNtlm } = require("../util-server"); const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, 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");
@ -16,6 +16,7 @@ const { demoMode } = require("../config");
const version = require("../../package.json").version; const version = require("../../package.json").version;
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server"); const { UptimeKumaServer } = require("../uptime-kuma-server");
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
/** /**
* status: * status:
@ -34,7 +35,13 @@ class Monitor extends BeanModel {
let obj = { let obj = {
id: this.id, id: this.id,
name: this.name, name: this.name,
sendUrl: this.sendUrl,
}; };
if (this.sendUrl) {
obj.url = this.url;
}
if (showTags) { if (showTags) {
obj.tags = await this.getTags(); obj.tags = await this.getTags();
} }
@ -434,10 +441,13 @@ class Monitor extends BeanModel {
"Accept": "*/*", "Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version, "User-Agent": "Uptime-Kuma/" + version,
}, },
httpsAgent: new https.Agent({ httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(), rejectUnauthorized: !this.getIgnoreTls(),
}), }),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
maxCachedSessions: 0,
}),
maxRedirects: this.maxredirects, maxRedirects: this.maxredirects,
validateStatus: (status) => { validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes()); return checkStatusCode(status, this.getAcceptedStatuscodes());
@ -471,6 +481,14 @@ class Monitor extends BeanModel {
await mssqlQuery(this.databaseConnectionString, this.databaseQuery); await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "postgres") {
let startTime = dayjs().valueOf();
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = ""; bean.msg = "";
bean.status = UP; bean.status = UP;
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;

@ -45,6 +45,8 @@ class StatusPage extends BeanModel {
$("link[rel=icon]") $("link[rel=icon]")
.attr("href", statusPage.icon) .attr("href", statusPage.icon)
.removeAttr("type"); .removeAttr("type");
$("link[rel=apple-touch-icon]").remove();
} }
const head = $("head"); const head = $("head");
@ -61,6 +63,9 @@ class StatusPage extends BeanModel {
</script> </script>
`); `);
// manifest.json
$("link[rel=manifest]").attr("href", `/api/status-page/${statusPage.slug}/manifest.json`);
return $.root().html(); return $.root().html();
} }

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

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

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

@ -107,4 +107,42 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
} }
}); });
// Status page's manifest.json
router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
try {
// Get Status Page
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
response.statusCode = 404;
response.json({
msg: "Not Found"
});
return;
}
// Response
response.json({
"name": statusPage.title,
"start_url": "/status/" + statusPage.slug,
"display": "standalone",
"icons": [
{
"src": statusPage.icon,
"sizes": "128x128",
"type": "image/png"
}
]
});
} catch (error) {
send403(response, error.message);
}
});
module.exports = router; module.exports = router;

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

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

@ -7,6 +7,7 @@ const { R } = require("redbean-node");
const { log } = require("../src/util"); const { log } = require("../src/util");
const Database = require("./database"); const Database = require("./database");
const util = require("util"); const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
/** /**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue. * `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
@ -71,6 +72,8 @@ class UptimeKumaServer {
} }
} }
CacheableDnsHttpAgent.registerGlobalAgent();
this.io = new Server(this.httpServer); this.io = new Server(this.httpServer);
} }

@ -11,7 +11,10 @@ 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 mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
const { NtlmClient } = require("axios-ntlm"); const { NtlmClient } = require("axios-ntlm");
const { Settings } = require("./settings");
// From ping-lite // From ping-lite
exports.WIN = /^win/.test(process.platform); exports.WIN = /^win/.test(process.platform);
@ -237,10 +240,6 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
*/ */
exports.mssqlQuery = function (connectionString, query) { exports.mssqlQuery = function (connectionString, query) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
mssql.on("error", err => {
reject(err);
});
mssql.connect(connectionString).then(pool => { mssql.connect(connectionString).then(pool => {
return pool.request() return pool.request()
.query(query); .query(query);
@ -254,23 +253,45 @@ exports.mssqlQuery = function (connectionString, query) {
}); });
}; };
/**
* Run a query on Postgres
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.postgresQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
const config = postgresConParse(connectionString);
if (config.password === "") {
// See https://github.com/brianc/node-postgres/issues/1927
return reject(new Error("Password is undefined."));
}
const client = new Client({ connectionString });
client.connect();
return client.query(query)
.then(res => {
resolve(res);
})
.catch(err => {
reject(err);
})
.finally(() => {
client.end();
});
});
};
/** /**
* 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
* @returns {Promise<any>} Value * @returns {Promise<any>} Value
*/ */
exports.setting = async function (key) { exports.setting = async function (key) {
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [ return await Settings.get(key);
key,
]);
try {
const v = JSON.parse(value);
log.debug("util", `Get Setting: ${key}: ${v}`);
return v;
} catch (e) {
return value;
}
}; };
/** /**
@ -281,70 +302,26 @@ exports.setting = async function (key) {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
exports.setSetting = async function (key, value, type = null) { exports.setSetting = async function (key, value, type = null) {
let bean = await R.findOne("setting", " `key` = ? ", [ await Settings.set(key, value, type);
key,
]);
if (!bean) {
bean = R.dispense("setting");
bean.key = key;
}
bean.type = type;
bean.value = JSON.stringify(value);
await R.store(bean);
}; };
/** /**
* Get settings based on type * Get settings based on type
* @param {?string} type The type of setting * @param {string} type The type of setting
* @returns {Promise<Bean>} * @returns {Promise<Bean>}
*/ */
exports.getSettings = async function (type) { exports.getSettings = async function (type) {
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [ return await Settings.getSettings(type);
type,
]);
let result = {};
for (let row of list) {
try {
result[row.key] = JSON.parse(row.value);
} catch (e) {
result[row.key] = row.value;
}
}
return result;
}; };
/** /**
* Set settings based on type * Set settings based on type
* @param {?string} type Type of settings to set * @param {string} type Type of settings to set
* @param {Object} data Values of settings * @param {Object} data Values of settings
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
exports.setSettings = async function (type, data) { exports.setSettings = async function (type, data) {
let keyList = Object.keys(data); await Settings.setSettings(type, data);
let promiseList = [];
for (let key of keyList) {
let bean = await R.findOne("setting", " `key` = ? ", [
key
]);
if (bean == null) {
bean = R.dispense("setting");
bean.type = type;
bean.key = key;
}
if (bean.type === type) {
bean.value = JSON.stringify(data[key]);
promiseList.push(R.store(bean));
}
}
await Promise.all(promiseList);
}; };
// ssl-checker by @dyaa // ssl-checker by @dyaa
@ -437,7 +414,7 @@ exports.checkCertificate = function (res) {
/** /**
* Check if the provided status code is within the accepted ranges * Check if the provided status code is within the accepted ranges
* @param {string} status The status code to check * @param {number} status The status code to check
* @param {string[]} acceptedCodes An array of accepted status codes * @param {string[]} acceptedCodes An array of accepted status codes
* @returns {boolean} True if status code within range, false otherwise * @returns {boolean} True if status code within range, false otherwise
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string * @throws {Error} Will throw an error if the provided status code is not a valid range string or code string

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

@ -0,0 +1,13 @@
<template>
<div class="mb-3">
<label for="alertnow-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="alertnow-webhook-url" v-model="$parent.notification.alertNowWebhookURL" type="text" class="form-control" required>
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
<a href="https://service.opsnow.com/docs/alertnow/en/user-guide-alertnow-en.html#standard" target="_blank">{{ $t("here") }}</a>
</i18n-t>
</div>
</div>
</template>

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

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

@ -4,6 +4,11 @@
<object class="my-4" width="200" height="200" data="/icon.svg" /> <object class="my-4" width="200" height="200" data="/icon.svg" />
<div class="fs-4 fw-bold">Uptime Kuma</div> <div class="fs-4 fw-bold">Uptime Kuma</div>
<div>{{ $t("Version") }}: {{ $root.info.version }}</div> <div>{{ $t("Version") }}: {{ $root.info.version }}</div>
<div class="frontend-version">{{ $t("Frontend Version") }}: {{ $root.frontendVersion }}</div>
<div v-if="!$root.isFrontendBackendVersionMatched" class="alert alert-warning mt-4" role="alert">
{{ $t("Frontend Version do not match backend version!") }}
</div>
<div class="my-3 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div> <div class="my-3 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
@ -46,6 +51,16 @@ export default {
} }
.update-link { .update-link {
font-size: 0.8em;
}
.frontend-version {
font-size: 0.9em; font-size: 0.9em;
color: #cccccc;
.dark & {
color: #333333;
}
} }
</style> </style>

@ -11,6 +11,7 @@ const languageList = {
"es-ES": "Español", "es-ES": "Español",
"eu": "Euskara", "eu": "Euskara",
"fa": "Farsi", "fa": "Farsi",
"pt-PT": "Português (Portugal)",
"pt-BR": "Português (Brasileiro)", "pt-BR": "Português (Brasileiro)",
"fr-FR": "Français (France)", "fr-FR": "Français (France)",
"hu": "Magyar", "hu": "Magyar",

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

@ -536,4 +536,5 @@ export default {
Domain: "Домейн", Domain: "Домейн",
Workstation: "Работна станция", Workstation: "Работна станция",
disableCloudflaredNoAuthMsg: "Тъй като сте в режим \"No Auth mode\", парола не се изисква.", disableCloudflaredNoAuthMsg: "Тъй като сте в режим \"No Auth mode\", парола не се изисква.",
wayToGetLineNotifyToken: "Може да получите токен код за достъп от {0}",
}; };

@ -465,6 +465,7 @@ export default {
"Domain Name Expiry Notification": "Domain Name Expiry Notification", "Domain Name Expiry Notification": "Domain Name Expiry Notification",
Proxy: "Proxy", Proxy: "Proxy",
"Date Created": "Date Created", "Date Created": "Date Created",
HomeAssistant: "Home Assistant",
onebotHttpAddress: "OneBot HTTP Address", onebotHttpAddress: "OneBot HTTP Address",
onebotMessageType: "OneBot Message Type", onebotMessageType: "OneBot Message Type",
onebotGroupMessage: "Group", onebotGroupMessage: "Group",
@ -536,5 +537,5 @@ export default {
"Domain": "Domain", "Domain": "Domain",
"Workstation": "Workstation", "Workstation": "Workstation",
disableCloudflaredNoAuthMsg: "You are in No Auth mode, password is not require.", disableCloudflaredNoAuthMsg: "You are in No Auth mode, password is not require.",
HomeAssistant: "Home Assistant", wayToGetLineNotifyToken: "You can get an access token from {0}",
}; };

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

@ -3,7 +3,7 @@ export default {
checkEverySecond: "{0}초마다 확인해요.", checkEverySecond: "{0}초마다 확인해요.",
retryCheckEverySecond: "{0}초마다 다시 확인해요.", retryCheckEverySecond: "{0}초마다 다시 확인해요.",
retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수", retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수",
ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기", ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 오류 무시하기",
upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거예요.", upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거예요.",
maxRedirectDescription: "최대 리다이렉트 횟수예요. 0을 입력하면 리다이렉트를 꺼요.", maxRedirectDescription: "최대 리다이렉트 횟수예요. 0을 입력하면 리다이렉트를 꺼요.",
acceptedStatusCodesDescription: "응답 성공으로 간주할 상태 코드를 정해요.", acceptedStatusCodesDescription: "응답 성공으로 간주할 상태 코드를 정해요.",
@ -30,7 +30,7 @@ export default {
Dashboard: "대시보드", Dashboard: "대시보드",
"New Update": "새로운 업데이트", "New Update": "새로운 업데이트",
Language: "언어", Language: "언어",
Appearance: "외형", Appearance: "디스플레이",
Theme: "테마", Theme: "테마",
General: "일반", General: "일반",
Version: "버전", Version: "버전",
@ -78,7 +78,7 @@ export default {
Notifications: "알림", Notifications: "알림",
"Not available, please setup.": "존재하지 않아요, 새로운 거 하나 만드는 건 어때요?", "Not available, please setup.": "존재하지 않아요, 새로운 거 하나 만드는 건 어때요?",
"Setup Notification": "알림 설정", "Setup Notification": "알림 설정",
Light: "이트", Light: "이트",
Dark: "다크", Dark: "다크",
Auto: "자동", Auto: "자동",
"Theme - Heartbeat Bar": "테마 - 하트비트 바", "Theme - Heartbeat Bar": "테마 - 하트비트 바",
@ -91,7 +91,7 @@ export default {
"Discourage search engines from indexing site": "검색 엔진 인덱싱 거부", "Discourage search engines from indexing site": "검색 엔진 인덱싱 거부",
"Change Password": "비밀번호 변경", "Change Password": "비밀번호 변경",
"Current Password": "기존 비밀번호", "Current Password": "기존 비밀번호",
"New Password": "새로운 비밀번호", "New Password": "새 비밀번호",
"Repeat New Password": "새로운 비밀번호 재입력", "Repeat New Password": "새로운 비밀번호 재입력",
"Update Password": "비밀번호 변경", "Update Password": "비밀번호 변경",
"Disable Auth": "인증 비활성화", "Disable Auth": "인증 비활성화",
@ -109,14 +109,14 @@ export default {
Password: "비밀번호", Password: "비밀번호",
"Remember me": "비밀번호 기억하기", "Remember me": "비밀번호 기억하기",
Login: "로그인", Login: "로그인",
"No Monitors, please": "모니터링이 없어요,", "No Monitors, please": "모니터링이 현재 없어요,",
"add one": "하나 추가해봐요", "add one": "한번 추가해보실레요?",
"Notification Type": "알림 종류", "Notification Type": "알림 종류",
Email: "이메일", Email: "이메일",
Test: "테스트", Test: "테스트",
"Certificate Info": "인증서 정보", "Certificate Info": "인증서 정보",
"Resolver Server": "Resolver 서버", "Resolver Server": "Resolver 서버",
"Resource Record Type": "자원 레코드 유형", "Resource Record Type": "리소스 레코드 유형",
"Last Result": "최근 결과", "Last Result": "최근 결과",
"Create your admin account": "관리자 계정 만들기", "Create your admin account": "관리자 계정 만들기",
"Repeat Password": "비밀번호 재입력", "Repeat Password": "비밀번호 재입력",
@ -208,19 +208,19 @@ export default {
smtpBCC: "숨은 참조", smtpBCC: "숨은 참조",
discord: "Discord", discord: "Discord",
"Discord Webhook URL": "Discord Webhook URL", "Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "서버 설정 -> 연동 -> 웹후크 보기 -> 새 웹후크에서 얻을 수 있어요.", wayToGetDiscordURL: "서버 설정 -> 연동 -> 웹후크 보기 -> 새 웹후크에서 얻을 수 있어요!",
"Bot Display Name": "표시 이름", "Bot Display Name": "표시 이름",
"Prefix Custom Message": "접두사 메시지", "Prefix Custom Message": "접두사 메시지",
"Hello @everyone is...": "{'@'}everyone 서버 상태 알림이에요...", "Hello @everyone is...": "{'@'}everyone 서버 상태 알림이에요...",
teams: "Microsoft Teams", teams: "Microsoft Teams",
"Webhook URL": "Webhook URL", "Webhook URL": "Webhook URL",
wayToGetTeamsURL: "{0}에서 Webhook을 어떻게 만드는지 알아봐요.", wayToGetTeamsURL: "{0}에서 Webhook을 어떻게 만드는지 알아보세요!",
signal: "Signal", signal: "Signal",
Number: "숫자", Number: "숫자",
Recipients: "받는 사람", Recipients: "받는 사람",
needSignalAPI: "REST API를 사용하는 Signal 클라이언트가 있어야 해요.", needSignalAPI: "REST API를 사용하는 Signal 클라이언트가 있어야 해요.",
wayToCheckSignalURL: "밑에 URL을 확인해 URL 설정 방법을 볼 수 있어요.", wayToCheckSignalURL: "밑에 URL을 확인해 URL 설정 방법을 볼 수 있어요.",
signalImportant: "중요: 받는 사람의 그룹과 숫자는 섞을 수 없어요!", signalImportant: "경고: 받는 사람의 그룹과 숫자는 섞을 수 없어요!",
gotify: "Gotify", gotify: "Gotify",
"Application Token": "애플리케이션 토큰", "Application Token": "애플리케이션 토큰",
"Server URL": "서버 URL", "Server URL": "서버 URL",
@ -230,8 +230,8 @@ export default {
"Channel Name": "채널 이름", "Channel Name": "채널 이름",
"Uptime Kuma URL": "Uptime Kuma URL", "Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "Webhook에 대한 설명: {0}", aboutWebhooks: "Webhook에 대한 설명: {0}",
aboutChannelName: "Webhook 채널을 우회하려면 {0} 채널 이름칸에 채널 이름을 입력해주세요. 예: #기타-채널", aboutChannelName: "Webhook 채널을 무시하려면 {0} 채널 이름칸에 채널 이름을 입력해주세요. 예: #기타-채널",
aboutKumaURL: "Uptime Kuma URL칸을 공백으로 두면 기본적으로 Project Github 페이지로 설정해요.", aboutKumaURL: "Uptime Kuma URL칸을 공백으로 두면 기본적으로 Github Project 페이지로 설정해요.",
emojiCheatSheet: "이모지 목록 시트: {0}", emojiCheatSheet: "이모지 목록 시트: {0}",
"rocket.chat": "Rocket.chat", "rocket.chat": "Rocket.chat",
pushover: "Pushover", pushover: "Pushover",
@ -243,8 +243,8 @@ export default {
pushbullet: "Pushbullet", pushbullet: "Pushbullet",
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
"User Key": "사용자 키", "User Key": "유저 키",
Device: "장치", Device: "디바이스",
"Message Title": "메시지 제목", "Message Title": "메시지 제목",
"Notification Sound": "알림음", "Notification Sound": "알림음",
"More info on:": "자세한 정보: {0}", "More info on:": "자세한 정보: {0}",
@ -254,7 +254,7 @@ export default {
octopushTypePremium: "프리미엄 (빠름) - 알림 기능에 적합해요)", octopushTypePremium: "프리미엄 (빠름) - 알림 기능에 적합해요)",
octopushTypeLowCost: "저렴한 요금 (느림) - 가끔 차단될 수 있어요)", octopushTypeLowCost: "저렴한 요금 (느림) - 가끔 차단될 수 있어요)",
"Check octopush prices": "{0}에서 Octopush 가격을 확인할 수 있어요.", "Check octopush prices": "{0}에서 Octopush 가격을 확인할 수 있어요.",
octopushPhoneNumber: "휴대전화 번호 (intl format, eg : +33612345678) ", octopushPhoneNumber: "휴대전화 번호 (intl format, 예시: +821023456789) ",
octopushSMSSender: "보내는 사람 이름 : 3-11개의 영숫자 및 여백공간 (a-z, A-Z, 0-9)", octopushSMSSender: "보내는 사람 이름 : 3-11개의 영숫자 및 여백공간 (a-z, A-Z, 0-9)",
"LunaSea Device ID": "LunaSea 장치 ID", "LunaSea Device ID": "LunaSea 장치 ID",
"Apprise URL": "Apprise URL", "Apprise URL": "Apprise URL",
@ -324,17 +324,17 @@ export default {
Content: "내용", Content: "내용",
Style: "스타일", Style: "스타일",
info: "정보", info: "정보",
warning: "경고", warning: "주의",
danger: "위험", danger: "경고",
primary: "기본", primary: "기본",
light: "이트", light: "이트",
dark: "다크", dark: "다크",
Post: "올리기", Post: "게시",
"Please input title and content": "제목과 내용을 작성해주세요.", "Please input title and content": "제목과 내용을 작성해주세요.",
Created: "생성 날짜", Created: "생성 날짜",
"Last Updated": "마지막 업데이트", "Last Updated": "마지막 업데이트",
Unpin: "제거", Unpin: "제거",
"Switch to Light Theme": "이트 테마로 전환", "Switch to Light Theme": "이트 테마로 전환",
"Switch to Dark Theme": "다크 테마로 전환", "Switch to Dark Theme": "다크 테마로 전환",
"Show Tags": "태그 보이기", "Show Tags": "태그 보이기",
"Hide Tags": "태그 숨기기", "Hide Tags": "태그 숨기기",
@ -361,8 +361,8 @@ export default {
topicExplanation: "모니터링할 MQTT Topic", topicExplanation: "모니터링할 MQTT Topic",
successMessage: "성공 메시지", successMessage: "성공 메시지",
successMessageExplanation: "성공으로 간주되는 MQTT 메시지", successMessageExplanation: "성공으로 간주되는 MQTT 메시지",
error: "error", error: "오류",
critical: "critical", critical: "크리티컬",
Customize: "커스터마이즈", Customize: "커스터마이즈",
"Custom Footer": "커스텀 Footer", "Custom Footer": "커스텀 Footer",
"Custom CSS": "커스텀 CSS", "Custom CSS": "커스텀 CSS",
@ -406,7 +406,7 @@ export default {
PhoneNumbers: "휴대전화 번호", PhoneNumbers: "휴대전화 번호",
TemplateCode: "템플릿 코드", TemplateCode: "템플릿 코드",
SignName: "SignName", SignName: "SignName",
"Sms template must contain parameters: ": "Sms 템플릿은 다음과 같은 파라미터가 포함되어야 해요:", "Sms template must contain parameters: ": "SMS 템플릿은 다음과 같은 파라미터가 포함되어야 해요:",
"Bark Endpoint": "Bark Endpoint", "Bark Endpoint": "Bark Endpoint",
WebHookUrl: "웹훅 URL", WebHookUrl: "웹훅 URL",
SecretKey: "Secret Key", SecretKey: "Secret Key",
@ -518,14 +518,14 @@ export default {
"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", "Coming Soon": "Coming Soon...",
wayToGetClickSendSMSToken: "{0}에서 API 사용자 이름과 키를 얻을 수 있어요.", wayToGetClickSendSMSToken: "{0}에서 API 사용자 이름과 키를 얻을 수 있어요.",
}; };

@ -90,8 +90,11 @@ export default {
"New Password": "Nieuw wachtwoord", "New Password": "Nieuw wachtwoord",
"Repeat New Password": "Herhaal nieuw wachtwoord", "Repeat New Password": "Herhaal nieuw wachtwoord",
"Update Password": "Vernieuw wachtwoord", "Update Password": "Vernieuw wachtwoord",
"Disable Auth": "Autorisatie uitschakelen", "Disable Auth": "Authenticatie uitschakelen",
"Enable Auth": "Autorisatie inschakelen", "Enable Auth": "Authenticatie inschakelen",
"disableauth.message1": "Weet je zeker dat je <strong>authenticatie wilt uitschakelen</strong>?",
"disableauth.message2": "Er zijn omstandigheden waarbij je <strong>authenticatie door derden wilt implementeren</strong> voor Uptime Kuma, zoals Cloudflare Access, Authelia of andere authenticatiemechanismen.",
"Please use this option carefully!": "Gebruik deze optie zorgvuldig!",
Logout: "Uitloggen", Logout: "Uitloggen",
Leave: "Vertrekken", Leave: "Vertrekken",
"I understand, please disable": "Ik begrijp het, schakel a.u.b. uit", "I understand, please disable": "Ik begrijp het, schakel a.u.b. uit",
@ -351,7 +354,7 @@ export default {
Discard: "Weggooien", Discard: "Weggooien",
Cancel: "Annuleren", Cancel: "Annuleren",
"Powered by": "Mogelijk gemaakt door", "Powered by": "Mogelijk gemaakt door",
shrinkDatabaseDescription: "Trigger database VACUUM voor SQLite. Als de database na 1.10.0 gemaakt is, dan is AUTO_VACUUM al aangezet en deze actie niet nodig.", shrinkDatabaseDescription: "Activeer database VACUUM voor SQLite. Als de database na 1.10.0 aangemaakt is, dan staat AUTO_VACUUM al aan en is deze actie niet nodig.",
serwersms: "SerwerSMS.pl", serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API Gebruikersnaam (incl. webapi_ prefix)", serwersmsAPIUser: "API Gebruikersnaam (incl. webapi_ prefix)",
serwersmsAPIPassword: "API Wachtwoord", serwersmsAPIPassword: "API Wachtwoord",
@ -386,7 +389,7 @@ export default {
proxyDescription: "Proxies moeten worden toegewezen aan een monitor om te functioneren.", proxyDescription: "Proxies moeten worden toegewezen aan een monitor om te functioneren.",
enableProxyDescription: "Deze proxy heeft geen effect op monitor verzoeken totdat het is geactiveerd. Je kunt tijdelijk de proxy uitschakelen voor alle monitors voor activatie status.", enableProxyDescription: "Deze proxy heeft geen effect op monitor verzoeken totdat het is geactiveerd. Je kunt tijdelijk de proxy uitschakelen voor alle monitors voor activatie status.",
setAsDefaultProxyDescription: "Deze proxy wordt standaard aangezet voor alle nieuwe monitors. Je kunt nog steeds de proxy apart uitschakelen voor elke monitor.", setAsDefaultProxyDescription: "Deze proxy wordt standaard aangezet voor alle nieuwe monitors. Je kunt nog steeds de proxy apart uitschakelen voor elke monitor.",
"Certificate Chain": "Certificaat Chain", "Certificate Chain": "Certificaatketen",
Valid: "Geldig", Valid: "Geldig",
Invalid: "Ongeldig", Invalid: "Ongeldig",
AccessKeyId: "AccessKey ID", AccessKeyId: "AccessKey ID",
@ -407,7 +410,7 @@ export default {
High: "Hoog", High: "Hoog",
Retry: "Opnieuw", Retry: "Opnieuw",
Topic: "Onderwerp", Topic: "Onderwerp",
"WeCom Bot Key": "WeCom Bot Sleutel", "WeCom Bot Key": "WeCom Bot Key",
"Setup Proxy": "Proxy instellen", "Setup Proxy": "Proxy instellen",
"Proxy Protocol": "Proxy Protocol", "Proxy Protocol": "Proxy Protocol",
"Proxy Server": "Proxy Server", "Proxy Server": "Proxy Server",
@ -449,7 +452,6 @@ export default {
"Issuer:": "Uitgever:", "Issuer:": "Uitgever:",
"Fingerprint:": "Vingerafruk:", "Fingerprint:": "Vingerafruk:",
"No status pages": "Geen status pagina's", "No status pages": "Geen status pagina's",
"Domain Name Expiry Notification": "Domein Naam Verloop Notificatie",
Proxy: "Proxy", Proxy: "Proxy",
"Date Created": "Datum Aangemaakt", "Date Created": "Datum Aangemaakt",
onebotHttpAddress: "OneBot HTTP Adres", onebotHttpAddress: "OneBot HTTP Adres",
@ -460,6 +462,70 @@ export default {
onebotSafetyTips: "Voor de veiligheid moet een toegangssleutel worden ingesteld", onebotSafetyTips: "Voor de veiligheid moet een toegangssleutel worden ingesteld",
"PushDeer Key": "PushDeer Key", "PushDeer Key": "PushDeer Key",
"Footer Text": "Footer Tekst", "Footer Text": "Footer Tekst",
"Show Powered By": "Laat 'Mogeljik gemaakt door' zien", "Show Powered By": "Laat \"Mogeljik gemaakt door\" zien",
"Domain Names": "Domein Namen", "Domain Names": "Domein Namen",
"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)",
dnsPortDescription: "DNS-serverpoort. Standaard ingesteld op 53. Je kunt de poort op elk moment wijzigen.",
error: "fout",
critical: "kritisch",
wayToGetPagerDutyKey: "Je kunt dit krijgen door naar Service -> Service Directory -> (Selecteer een service) -> Integraties -> Integratie toevoegen te gaan. Hier kunt u zoeken naar \"Events API V2\". Meer informatie {0}",
"Integration Key": "Integration Key",
"Integration URL": "Integration URL",
"Auto resolve or acknowledged": "Automatisch oplossen of bevestigen",
"do nothing": "niets doen",
"auto acknowledged": "automatisch bevestigen",
"auto resolve": "automatisch oplossen",
Authentication: "authenticatie",
signedInDisp: "Aangemeld als {0}",
signedInDispDisabled: "Authenticatie uitgeschakeld.",
"Certificate Expiry Notification": "Melding over verlopen certificaat",
"Recipient Number": "Nummer ontvanger",
"From Name/Number": "Van naam/nummer",
"Leave blank to use a shared sender number.": "Laat leeg om een gedeeld afzendernummer te gebruiken.",
endpoint: "endpoint",
pushyAPIKey: "Secret API Key",
pushyToken: "Device token",
"Show update if available": "Update weergeven indien beschikbaar",
"Also check beta release": "Controleer ook de bètaversies",
"Using a Reverse Proxy?": "Een reverse proxy gebruiken?",
"Check how to config it for WebSocket": "Controleer hoe je het configureert voor een WebSocket",
"Steam Game Server": "Steam gameserver",
"Most likely causes:": "Meest waarschijnlijke oorzaken:",
"The resource is no longer available.": "De paginabron is niet langer beschikbaar.",
"There might be a typing error in the address.": "Er zit een typefout in het de URL.",
"What you can try:": "Wat je kan proberen:",
"Retype the address.": "De URL controleren en/of opnnieuw typen.",
"Go back to the previous page.": "Terug naar de vorige pagina.",
"Coming Soon": "Binnenkort beschikbaar",
wayToGetClickSendSMSToken: "Je kan een API Username en API Key krijgen vanuit {0} .",
"Connection String": "Connection String",
Query: "Query",
settingsCertificateExpiry: "TLS Certificate Expiry",
certificationExpiryDescription: "HTTPS Monitors trigger notification when TLS certificate expires in:",
"ntfy Topic": "ntfy Topic",
Domain: "Domein",
Workstation: "Werkstation",
disableCloudflaredNoAuthMsg: "De \"Geen authenticatie\" modus staat aan, wachtwoord is niet vereist.",
}; };

@ -0,0 +1,203 @@
export default {
languageName: "Português (Portugal)",
checkEverySecond: "Verificar a cada {0} segundos.",
retryCheckEverySecond: "Tentar novamente a cada {0} segundos.",
retriesDescription: "Máximo de tentativas antes que o serviço seja marcado como inativo e uma notificação seja enviada",
ignoreTLSError: "Ignorar erros TLS/SSL para sites HTTPS",
upsideDownModeDescription: "Inverte o status de cabeça para baixo. Se o serviço estiver acessível, ele está OFFLINE.",
maxRedirectDescription: "Número máximo de redirecionamentos a seguir. Define como 0 para desativar redirecionamentos.",
acceptedStatusCodesDescription: "Seleciona os códigos de status que são considerados uma resposta bem-sucedida.",
passwordNotMatchMsg: "A senha repetida não corresponde.",
notificationDescription: "Atribuir uma notificação ao (s) monitor (es) para que funcione.",
keywordDescription: "Pesquisa a palavra-chave em HTML simples ou resposta JSON e diferencia maiúsculas de minúsculas",
pauseDashboardHome: "Pausa",
deleteMonitorMsg: "Tens a certeza de que queres excluir este monitor?",
deleteNotificationMsg: "Tens a certeza de que queres excluir esta notificação para todos os monitores?",
resolverserverDescription: "A Cloudflare é o servidor padrão, podes alterar o servidor 'resolvedor' a qualquer momento.",
rrtypeDescription: "Seleciona o RR-Type que queres monitorizar",
pauseMonitorMsg: "Tens a certeza que queres fazer uma pausa?",
enableDefaultNotificationDescription: "Para cada monitor novo esta notificação vai estar activa por padrão. Podes também desativar a notificação separadamente para cada monitor.",
clearEventsMsg: "Tens a certeza que queres excluir todos os eventos deste monitor?",
clearHeartbeatsMsg: "Tens a certeza de que queres excluir todos os heartbeats deste monitor?",
confirmClearStatisticsMsg: "Tens a certeza que queres excluir TODAS as estatísticas?",
importHandleDescription: "Escolhe 'Ignorar existente' se quiseres ignorar todos os monitores ou notificações com o mesmo nome. 'Substituir' excluirá todos os monitores e notificações existentes.",
confirmImportMsg: "Tens a certeza que queres importar o backup? Certifica-te que selecionaste a opção de importação correta.",
twoFAVerifyLabel: "Insire o teu token para verificares se o 2FA está a funcionar",
tokenValidSettingsMsg: "O token é válido! Agora podes salvar as configurações do 2FA.",
confirmEnableTwoFAMsg: "Tens a certeza de que queres habilitar 2FA?",
confirmDisableTwoFAMsg: "Tens a certeza de que queres desativar 2FA?",
Settings: "Configurações",
Dashboard: "Dashboard",
"New Update": "Nova Atualização",
Language: "Linguagem",
Appearance: "Aparência",
Theme: "Tema",
General: "Geral",
Version: "Versão",
"Check Update On GitHub": "Verificar atualização no Github",
List: "Lista",
Add: "Adicionar",
"Add New Monitor": "Adicionar novo monitor",
"Quick Stats": "Estatísticas rápidas",
Up: "On",
Down: "Off",
Pending: "Pendente",
Unknown: "Desconhecido",
Pause: "Pausa",
Name: "Nome",
Status: "Status",
DateTime: "Data hora",
Message: "Mensagem",
"No important events": "Nenhum evento importante",
Resume: "Resumo",
Edit: "Editar",
Delete: "Apagar",
Current: "Atual",
Uptime: "Tempo de atividade",
"Cert Exp.": "Cert Exp.",
day: "dia | dias",
"-day": "-dia",
hour: "hora",
"-hour": "-hora",
Response: "Resposta",
Ping: "Ping",
"Monitor Type": "Tipo de Monitor",
Keyword: "Palavra-Chave",
"Friendly Name": "Nome Amigável",
URL: "URL",
Hostname: "Hostname",
Port: "Porta",
"Heartbeat Interval": "Intervalo de Heartbeats",
Retries: "Novas tentativas",
"Heartbeat Retry Interval": "Intervalo de repetição de Heartbeats",
Advanced: "Avançado",
"Upside Down Mode": "Modo de cabeça para baixo",
"Max. Redirects": "Redirecionamento Máx.",
"Accepted Status Codes": "Status Code Aceitáveis",
Save: "Guardar",
Notifications: "Notificações",
"Not available, please setup.": "Não disponível, por favor configura.",
"Setup Notification": "Configurar Notificação",
Light: "Claro",
Dark: "Escuro",
Auto: "Auto",
"Theme - Heartbeat Bar": "Tema - Barra de Heartbeat",
Normal: "Normal",
Bottom: "Inferior",
None: "Nenhum",
Timezone: "Fuso horário",
"Search Engine Visibility": "Visibilidade do mecanismo de pesquisa",
"Allow indexing": "Permitir Indexação",
"Discourage search engines from indexing site": "Desencorajar que motores de busca indexem o site",
"Change Password": "Mudar senha",
"Current Password": "Senha atual",
"New Password": "Nova Senha",
"Repeat New Password": "Repetir Nova Senha",
"Update Password": "Atualizar Senha",
"Disable Auth": "Desativar Autenticação",
"Enable Auth": "Ativar Autenticação",
"disableauth.message1": "Tens a certeza que queres <strong>desativar a autenticação</strong>?",
"disableauth.message2": "Isso é para <strong>alguém que tem autenticação de terceiros</strong> em frente ao 'UpTime Kuma' como o Cloudflare Access.",
"Please use this option carefully!": "Por favor, utiliza esta opção com cuidado.",
Logout: "Logout",
Leave: "Sair",
"I understand, please disable": "Eu entendo, por favor desativa.",
Confirm: "Confirmar",
Yes: "Sim",
No: "Não",
Username: "Utilizador",
Password: "Senha",
"Remember me": "Lembra-me",
Login: "Autenticar",
"No Monitors, please": "Nenhum monitor, por favor",
"add one": "adicionar um",
"Notification Type": "Tipo de Notificação",
Email: "Email",
Test: "Testar",
"Certificate Info": "Info. do Certificado ",
"Resolver Server": "Resolver Servidor",
"Resource Record Type": "Tipo de registro de aplicação",
"Last Result": "Último resultado",
"Create your admin account": "Cria a tua conta de admin",
"Repeat Password": "Repete a senha",
"Import Backup": "Importar Backup",
"Export Backup": "Exportar Backup",
Export: "Exportar",
Import: "Importar",
respTime: "Tempo de Resp. (ms)",
notAvailableShort: "N/A",
"Default enabled": "Padrão habilitado",
"Apply on all existing monitors": "Aplicar em todos os monitores existentes",
Create: "Criar",
"Clear Data": "Limpar Dados",
Events: "Eventos",
Heartbeats: "Heartbeats",
"Auto Get": "Obter Automático",
backupDescription: "Podes fazer backup de todos os monitores e todas as notificações num arquivo JSON.",
backupDescription2: "OBS: Os dados do histórico e do evento não estão incluídos.",
backupDescription3: "Dados confidenciais, como tokens de notificação, estão incluídos no arquivo de exportação, mantem-no com cuidado.",
alertNoFile: "Seleciona um arquivo para importar.",
alertWrongFileType: "Seleciona um arquivo JSON.",
"Clear all statistics": "Limpar todas as estatísticas",
"Skip existing": "Saltar existente",
Overwrite: "Sobrescrever",
Options: "Opções",
"Keep both": "Manter os dois",
"Verify Token": "Verificar Token",
"Setup 2FA": "Configurar 2FA",
"Enable 2FA": "Ativar 2FA",
"Disable 2FA": "Desativar 2FA",
"2FA Settings": "Configurações do 2FA ",
"Two Factor Authentication": "Autenticação de Dois Fatores",
Active: "Ativo",
Inactive: "Inativo",
Token: "Token",
"Show URI": "Mostrar URI",
Tags: "Tag",
"Add New below or Select...": "Adicionar Novo abaixo ou Selecionar ...",
"Tag with this name already exist.": "Já existe uma etiqueta com este nome.",
"Tag with this value already exist.": "Já existe uma etiqueta com este valor.",
color: "cor",
"value (optional)": "valor (opcional)",
Gray: "Cinza",
Red: "Vermelho",
Orange: "Laranja",
Green: "Verde",
Blue: "Azul",
Indigo: "Índigo",
Purple: "Roxo",
Pink: "Rosa",
"Search...": "Pesquisa...",
"Avg. Ping": "Ping Médio.",
"Avg. Response": "Resposta Média. ",
"Status Page": "Página de Status",
"Status Pages": "Página de Status",
"Entry Page": "Página de entrada",
statusPageNothing: "Nada aqui, por favor, adiciona um grupo ou monitor.",
"No Services": "Nenhum Serviço",
"All Systems Operational": "Todos os Serviços Operacionais",
"Partially Degraded Service": "Serviço parcialmente degradados",
"Degraded Service": "Serviço Degradado",
"Add Group": "Adicionar Grupo",
"Add a monitor": "Adicionar um monitor",
"Edit Status Page": "Editar Página de Status",
"Go to Dashboard": "Ir para o dashboard",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",
discord: "Discord",
teams: "Microsoft Teams",
signal: "Signal",
gotify: "Gotify",
slack: "Slack",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (Support 50+ Notification services)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
};

@ -184,8 +184,8 @@ export default {
"Add a monitor": "Dodaj monitor", "Add a monitor": "Dodaj monitor",
"Edit Status Page": "Uredi statusno stran", "Edit Status Page": "Uredi statusno stran",
"Go to Dashboard": "Pojdi na nadzorno ploščo", "Go to Dashboard": "Pojdi na nadzorno ploščo",
"Status Page": "Página de Status", "Status Page": "Statusna stran",
"Status Pages": "Página de Status", "Status Pages": "Statusne strani",
defaultNotificationName: "Moje {notification} Obvestilo ({number})", defaultNotificationName: "Moje {notification} Obvestilo ({number})",
here: "tukaj", here: "tukaj",
Required: "Obvezno", Required: "Obvezno",

@ -518,4 +518,5 @@ export default {
"Go back to the previous page.": "กลับไปที่หน้าก่อนหน้า", "Go back to the previous page.": "กลับไปที่หน้าก่อนหน้า",
"Coming Soon": "เร็ว ๆ นี้", "Coming Soon": "เร็ว ๆ นี้",
wayToGetClickSendSMSToken: "คุณสามารถรับ API Username และ API Key ได้จาก {0}", wayToGetClickSendSMSToken: "คุณสามารถรับ API Username และ API Key ได้จาก {0}",
wayToGetLineNotifyToken: "คุณสามารถรับ access token ได้จาก {0}",
}; };

@ -1,5 +1,5 @@
export default { export default {
languageName: "Український", languageName: "Українська",
checkEverySecond: "Перевірка кожні {0} секунд", checkEverySecond: "Перевірка кожні {0} секунд",
retriesDescription: "Максимальна кількість спроб перед позначенням сервісу як недоступного та надсиланням повідомлення", retriesDescription: "Максимальна кількість спроб перед позначенням сервісу як недоступного та надсиланням повідомлення",
ignoreTLSError: "Ігнорувати помилку TLS/SSL для сайтів HTTPS", ignoreTLSError: "Ігнорувати помилку TLS/SSL для сайтів HTTPS",
@ -7,11 +7,11 @@ export default {
maxRedirectDescription: "Максимальна кількість перенаправлень. Поставте 0, щоб вимкнути перенаправлення.", maxRedirectDescription: "Максимальна кількість перенаправлень. Поставте 0, щоб вимкнути перенаправлення.",
acceptedStatusCodesDescription: "Виберіть коди статусів для визначення доступності сервісу.", acceptedStatusCodesDescription: "Виберіть коди статусів для визначення доступності сервісу.",
passwordNotMatchMsg: "Повторення паролю не збігається.", passwordNotMatchMsg: "Повторення паролю не збігається.",
notificationDescription: "Прив'яжіть повідомлення до моніторів.", notificationDescription: "Прив'яжіть сповіщення до моніторів.",
keywordDescription: "Пошук слова в чистому HTML або JSON-відповіді (чутливо до регістру)", keywordDescription: "Пошук слова в чистому HTML або JSON-відповіді (чутливо до регістру)",
pauseDashboardHome: "Пауза", pauseDashboardHome: "Пауза",
deleteMonitorMsg: "Ви дійсно хочете видалити цей монітор?", deleteMonitorMsg: "Ви дійсно хочете видалити цей монітор?",
deleteNotificationMsg: "Ви дійсно хочете видалити це повідомлення для всіх моніторів?", deleteNotificationMsg: "Ви дійсно хочете видалити це сповіщення для всіх моніторів?",
resolverserverDescription: "Cloudflare є сервером за замовчуванням. Ви завжди можете змінити цей сервер.", resolverserverDescription: "Cloudflare є сервером за замовчуванням. Ви завжди можете змінити цей сервер.",
rrtypeDescription: "Виберіть тип ресурсного запису, який ви хочете відстежувати", rrtypeDescription: "Виберіть тип ресурсного запису, який ви хочете відстежувати",
pauseMonitorMsg: "Ви дійсно хочете поставити на паузу?", pauseMonitorMsg: "Ви дійсно хочете поставити на паузу?",
@ -54,7 +54,7 @@ export default {
Keyword: "Ключове слово", Keyword: "Ключове слово",
"Friendly Name": "Ім'я", "Friendly Name": "Ім'я",
URL: "URL", URL: "URL",
Hostname: "Ім'я хоста", Hostname: "Адреса хоста",
Port: "Порт", Port: "Порт",
"Heartbeat Interval": "Частота опитування", "Heartbeat Interval": "Частота опитування",
Retries: "Спроб", Retries: "Спроб",
@ -63,7 +63,7 @@ export default {
"Max. Redirects": "Макс. кількість перенаправлень", "Max. Redirects": "Макс. кількість перенаправлень",
"Accepted Status Codes": "Припустимі коди статусу", "Accepted Status Codes": "Припустимі коди статусу",
Save: "Зберегти", Save: "Зберегти",
Notifications: "Повідомлення", Notifications: "Сповіщення",
"Not available, please setup.": "Доступних сповіщень немає, необхідно створити.", "Not available, please setup.": "Доступних сповіщень немає, необхідно створити.",
"Setup Notification": "Створити сповіщення", "Setup Notification": "Створити сповіщення",
Light: "Світла", Light: "Світла",
@ -100,7 +100,7 @@ export default {
"No Monitors, please": "Моніторів немає, будь ласка", "No Monitors, please": "Моніторів немає, будь ласка",
"No Monitors": "Монітори відсутні", "No Monitors": "Монітори відсутні",
"add one": "створіть новий", "add one": "створіть новий",
"Notification Type": "Тип повідомлення", "Notification Type": "Тип сповіщення",
Email: "Пошта", Email: "Пошта",
Test: "Перевірка", Test: "Перевірка",
"Certificate Info": "Інформація про сертифікат", "Certificate Info": "Інформація про сертифікат",
@ -119,7 +119,7 @@ export default {
Events: "Події", Events: "Події",
Heartbeats: "Опитування", Heartbeats: "Опитування",
"Auto Get": "Авто-отримання", "Auto Get": "Авто-отримання",
enableDefaultNotificationDescription: "Для кожного нового монітора це повідомлення буде включено за замовчуванням. Ви все ще можете відключити повідомлення в кожному моніторі окремо.", enableDefaultNotificationDescription: "Для кожного нового монітора це сповіщення буде включено за замовчуванням. Ви все ще можете відключити сповіщення в кожному моніторі окремо.",
"Default enabled": "Використовувати за промовчанням", "Default enabled": "Використовувати за промовчанням",
"Also apply to existing monitors": "Застосувати до існуючих моніторів", "Also apply to existing monitors": "Застосувати до існуючих моніторів",
Export: "Експорт", Export: "Експорт",
@ -170,7 +170,7 @@ export default {
Purple: "Пурпурний", Purple: "Пурпурний",
Pink: "Рожевий", Pink: "Рожевий",
"Search...": "Пошук...", "Search...": "Пошук...",
"Avg. Ping": "Середнє значення пінгу", "Avg. Ping": "Середній пінг",
"Avg. Response": "Середній час відповіді", "Avg. Response": "Середній час відповіді",
"Entry Page": "Головна сторінка", "Entry Page": "Головна сторінка",
statusPageNothing: "Тут порожньо. Додайте групу або монітор.", statusPageNothing: "Тут порожньо. Додайте групу або монітор.",
@ -210,7 +210,7 @@ export default {
"Push URL": "URL пуша", "Push URL": "URL пуша",
needPushEvery: "До цієї URL необхідно звертатися кожні {0} секунд", needPushEvery: "До цієї URL необхідно звертатися кожні {0} секунд",
pushOptionalParams: "Опціональні параметри: {0}", pushOptionalParams: "Опціональні параметри: {0}",
defaultNotificationName: "Моє повідомлення {notification} ({number})", defaultNotificationName: "Моє сповіщення {notification} ({number})",
here: "тут", here: "тут",
Required: "Потрібно", Required: "Потрібно",
"Bot Token": "Токен бота", "Bot Token": "Токен бота",
@ -257,7 +257,7 @@ export default {
"User Key": "Ключ користувача", "User Key": "Ключ користувача",
Device: "Пристрій", Device: "Пристрій",
"Message Title": "Заголовок повідомлення", "Message Title": "Заголовок повідомлення",
"Notification Sound": "Звук повідомлення", "Notification Sound": "Звук сповіщення",
"More info on:": "Більше інформації: {0}", "More info on:": "Більше інформації: {0}",
pushoverDesc1: "Екстренний пріоритет (2) має таймуут повтору за замовчуванням 30 секунд і закінчується через 1 годину.", pushoverDesc1: "Екстренний пріоритет (2) має таймуут повтору за замовчуванням 30 секунд і закінчується через 1 годину.",
pushoverDesc2: "Якщо ви бажаєте надсилати повідомлення різним пристроям, необхідно заповнити поле Пристрій.", pushoverDesc2: "Якщо ви бажаєте надсилати повідомлення різним пристроям, необхідно заповнити поле Пристрій.",
@ -354,7 +354,7 @@ export default {
"No consecutive dashes --": "Заборонено використовувати тире --", "No consecutive dashes --": "Заборонено використовувати тире --",
"HTTP Options": "HTTP Опції", "HTTP Options": "HTTP Опції",
Authentication: "Аутентифікація", Authentication: "Аутентифікація",
"HTTP Basic Auth": "HTTP Авторизація", "HTTP Basic Auth": "Базова HTTP",
PushByTechulus: "Push by Techulus", PushByTechulus: "Push by Techulus",
clicksendsms: "ClickSend SMS", clicksendsms: "ClickSend SMS",
GoogleChat: "Google Chat (тільки Google Workspace)", GoogleChat: "Google Chat (тільки Google Workspace)",
@ -392,4 +392,139 @@ export default {
alertaAlertState: "Стан алерту", alertaAlertState: "Стан алерту",
alertaRecoverState: "Стан відновлення", alertaRecoverState: "Стан відновлення",
deleteStatusPageMsg: "Дійсно хочете видалити цю сторінку статусів?", deleteStatusPageMsg: "Дійсно хочете видалити цю сторінку статусів?",
Proxies: "Проксі",
default: "За замовчуванням",
enabled: "Активно",
setAsDefault: "Встановити за замовчуванням",
deleteProxyMsg: "Ви впевнені, що хочете видалити цей проксі для всіх моніторів?",
proxyDescription: "Щоб функціонувати, монітору потрібно призначити проксі.",
enableProxyDescription: "Цей проксі не впливатиме на запити моніторингу, доки його не буде активовано. Ви можете контролювати тимчасове відключення проксі з усіх моніторів за статусом активації.",
setAsDefaultProxyDescription: "Цей проксі буде ввімкнено за умовчанням для нових моніторів. Ви все одно можете вимкнути проксі окремо для кожного монітора.",
Invalid: "Недійсний",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "PhoneNumbers",
TemplateCode: "TemplateCode",
SignName: "SignName",
"Sms template must contain parameters: ": "Шаблон смс повинен містити параметри: ",
"Bark Endpoint": "Bark Endpoint",
WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey",
"For safety, must use secret key": "Для безпеки необхідно використовувати секретний ключ",
"Device Token": "Токен пристрою",
Platform: "Платформа",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "Високий",
Retry: "Повтор",
Topic: "Тема",
"WeCom Bot Key": "WeCom Bot ключ",
"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",
Slug: "Slug",
"Accept characters:": "Прийняти символи:",
startOrEndWithOnly: "Починається або закінчується лише {0}",
"No consecutive dashes": "Немає послідовних тире",
"The slug is already taken. Please choose another slug.": "The slug is already taken. Please choose another slug.",
"No Proxy": "Без проксі",
"Page Not Found": "Сторінку не знайдено",
"Reverse Proxy": "Реверсивний проксі",
wayToGetCloudflaredURL: "(Завантажити Cloudflare з {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 and Traefik.",
"Please read": "Будь ласка, прочитайте",
"Subject:": "Тема:",
"Valid To:": "Дійсний до:",
"Days Remaining:": "Залишилось днів:",
"Issuer:": "Емітент:",
"Fingerprint:": "Відбиток:",
"No status pages": "Немає сторінок статусу",
"Domain Name Expiry Notification": "Сповіщення про закінчення терміну дії доменного імені",
Proxy: "Проксі",
"Date Created": "Дата створення",
onebotHttpAddress: "OneBot адреса HTTP",
onebotMessageType: "OneBot тип повідомлення",
onebotGroupMessage: "Група",
onebotPrivateMessage: "Приватне",
onebotUserOrGroupId: "Група/Користувач ID",
onebotSafetyTips: "Для безпеки необхідно встановити маркер доступу",
"PushDeer Key": "PushDeer ключ",
"Footer Text": "Текст нижнього колонтитула",
"Show 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": "Legacy Octopush-DM",
"endpoint": "кінцева точка",
octopushAPIKey: "\"Ключ API\" з облікових даних HTTP API в панелі керування",
octopushLogin: "\"Ім'я користувача\" з облікових даних HTTP API на панелі керування",
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": "Vibrate Only",
"pushoversounds none": "None (silent)",
pushyAPIKey: "Секретний ключ API",
pushyToken: "Токен пристрою",
"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: "Ви можете отримати ім’я користувача API та ключ API з {0} .",
"Connection String": "Рядок підключення",
"Query": "Запит",
settingsCertificateExpiry: "Закінчення терміну дії сертифіката TLS",
certificationExpiryDescription: "Запуск сповіщення для HTTPS моніторів коли до закінчення терміну дії TLS сертифіката:",
"ntfy Topic": "ntfy Тема",
"Domain": "Домен",
"Workstation": "Робоча станція",
disableCloudflaredNoAuthMsg: "Ви перебуваєте в режимі без авторизації, пароль не потрібен.",
}; };

@ -13,6 +13,7 @@ export default {
pauseDashboardHome: "暫停", pauseDashboardHome: "暫停",
deleteMonitorMsg: "您確定要刪除此監測器嗎?", deleteMonitorMsg: "您確定要刪除此監測器嗎?",
deleteNotificationMsg: "您確定要為所有監測器刪除此通知嗎?", deleteNotificationMsg: "您確定要為所有監測器刪除此通知嗎?",
dnsPortDescription: "DNS 伺服器連接埠。預設為 53。您可以隨時變更連接埠。",
resolverserverDescription: "Cloudflare 為預設伺服器。您可以隨時更換解析伺服器。", resolverserverDescription: "Cloudflare 為預設伺服器。您可以隨時更換解析伺服器。",
rrtypeDescription: "選擇您想要監測的資源記錄類型", rrtypeDescription: "選擇您想要監測的資源記錄類型",
pauseMonitorMsg: "您確定要暫停嗎?", pauseMonitorMsg: "您確定要暫停嗎?",
@ -332,6 +333,8 @@ export default {
info: "資訊", info: "資訊",
warning: "警告", warning: "警告",
danger: "危險", danger: "危險",
error: "錯誤",
critical: "嚴重",
primary: "主要", primary: "主要",
light: "淺色", light: "淺色",
dark: "暗色", dark: "暗色",
@ -372,6 +375,13 @@ export default {
smtpDkimHashAlgo: "雜湊演算法 (選填)", smtpDkimHashAlgo: "雜湊演算法 (選填)",
smtpDkimheaderFieldNames: "要簽署的郵件標頭 (選填)", smtpDkimheaderFieldNames: "要簽署的郵件標頭 (選填)",
smtpDkimskipFields: "不簽署的郵件標頭 (選填)", smtpDkimskipFields: "不簽署的郵件標頭 (選填)",
wayToGetPagerDutyKey: "您可以前往服務 -> 服務目錄 -> (選取服務) -> 整合 -> 新增整合以取得。您可以搜尋 \"Events API V2\"。詳細資訊 {0}",
"Integration Key": "整合金鑰",
"Integration URL": "整合網址",
"Auto resolve or acknowledged": "自動解決或認可",
"do nothing": "不進行任何操作",
"auto acknowledged": "自動認可",
"auto resolve": "自動解決",
gorush: "Gorush", gorush: "Gorush",
alerta: "Alerta", alerta: "Alerta",
alertaApiEndpoint: "API 端點", alertaApiEndpoint: "API 端點",
@ -465,4 +475,65 @@ export default {
"Footer Text": "頁尾文字", "Footer Text": "頁尾文字",
"Show Powered By": "顯示技術支援文字", "Show Powered By": "顯示技術支援文字",
"Domain Names": "網域名稱", "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": "端",
octopushAPIKey: "\"API key\" from HTTP API credentials in control panel",
octopushLogin: "\"Login\" from HTTP API credentials in control panel",
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": "Piano Bar",
"pushoversounds siren": "Siren",
"pushoversounds spacealarm": "Space Alarm",
"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": "檢查 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 使用者名稱和金鑰。",
"Connection String": "連線字串",
"Query": "查詢",
settingsCertificateExpiry: "TLS 憑證到期",
certificationExpiryDescription: "TLS 將於 X 天後到期時觸發 HTTPS 監測器通知:",
"ntfy Topic": "ntfy 主題",
"Domain": "網域",
"Workstation": "工作站",
disableCloudflaredNoAuthMsg: "您處於無驗證模式。無須輸入密碼。",
}; };

@ -77,7 +77,7 @@
<!-- Mobile Only --> <!-- Mobile Only -->
<div v-if="$root.isMobile" style="width: 100%; height: 60px;" /> <div v-if="$root.isMobile" style="width: 100%; height: 60px;" />
<nav v-if="$root.isMobile" class="bottom-nav"> <nav v-if="$root.isMobile && $root.loggedIn" class="bottom-nav">
<router-link to="/dashboard" class="nav-link"> <router-link to="/dashboard" class="nav-link">
<div><font-awesome-icon icon="tachometer-alt" /></div> <div><font-awesome-icon icon="tachometer-alt" /></div>
{{ $t("Dashboard") }} {{ $t("Dashboard") }}

@ -601,6 +601,28 @@ export default {
return result; return result;
}, },
/**
* Frontend Version
* It should be compiled to a static value while building the frontend.
* Please see ./config/vite.config.js, it is defined via vite.js
* @returns {string}
*/
frontendVersion() {
// eslint-disable-next-line no-undef
return FRONTEND_VERSION;
},
/**
* Are both frontend and backend in the same version?
* @returns {boolean}
*/
isFrontendBackendVersionMatched() {
if (!this.info.version) {
return true;
}
return this.info.version === this.frontendVersion;
}
}, },
watch: { watch: {

@ -45,6 +45,9 @@
<option value="sqlserver"> <option value="sqlserver">
SQL Server SQL Server
</option> </option>
<option value="postgres">
PostgreSQL
</option>
</optgroup> </optgroup>
</select> </select>
</div> </div>
@ -168,15 +171,21 @@
</div> </div>
</template> </template>
<!-- SQL Server --> <!-- SQL Server and PostgreSQL -->
<template v-if="monitor.type === 'sqlserver'"> <template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres'">
<div class="my-3"> <div class="my-3">
<label for="sqlserverConnectionString" class="form-label">SQL Server {{ $t("Connection String") }}</label> <label for="sqlConnectionString" class="form-label">{{ $t("Connection String") }}</label>
<input id="sqlserverConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
<template v-if="monitor.type === 'sqlserver'">
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" placeholder="Server=<hostname>,<port>;Database=<your database>;User Id=<your user id>;Password=<your password>;Encrypt=<true/false>;TrustServerCertificate=<Yes/No>;Connection Timeout=<int>">
</template>
<template v-if="monitor.type === 'postgres'">
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" placeholder="postgres://username:password@host:port/database">
</template>
</div> </div>
<div class="my-3"> <div class="my-3">
<label for="sqlserverQuery" class="form-label">SQL Server {{ $t("Query") }}</label> <label for="sqlQuery" class="form-label">{{ $t("Query") }}</label>
<textarea id="sqlserverQuery" v-model="monitor.databaseQuery" class="form-control" placeholder="Example: select getdate()"></textarea> <textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" placeholder="Example: select getdate()"></textarea>
</div> </div>
</template> </template>
@ -584,7 +593,6 @@ export default {
method: "GET", method: "GET",
interval: 60, interval: 60,
retryInterval: this.interval, retryInterval: this.interval,
databaseConnectionString: "Server=<hostname>,<port>;Database=<your database>;User Id=<your user id>;Password=<your password>;Encrypt=<true/false>;TrustServerCertificate=<Yes/No>;Connection Timeout=<int>",
maxretries: 0, maxretries: 0,
notificationIDList: {}, notificationIDList: {},
ignoreTls: false, ignoreTls: false,

Loading…
Cancel
Save