Merge branch 'master' into feature/add-tags-to-metrics

This commit is contained in:
Frank Elsinga 2025-06-24 17:32:29 +02:00 committed by GitHub
commit 7c6ebf4593
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 3023 additions and 1626 deletions

View file

@ -3,7 +3,7 @@ name: ❓ Ask for help
description: | description: |
Submit any question related to Uptime Kuma Submit any question related to Uptime Kuma
#title: "[Help]" #title: "[Help]"
labels: ["help", "P3-low"] labels: ["help"]
body: body:
- type: markdown - type: markdown
attributes: attributes:

View file

@ -3,7 +3,7 @@ name: 🐛 Bug Report
description: | description: |
Submit a bug report to help us improve Submit a bug report to help us improve
#title: "[Bug]" #title: "[Bug]"
labels: ["bug", "P2-medium"] labels: ["bug"]
body: body:
- type: markdown - type: markdown
attributes: attributes:

View file

@ -3,7 +3,7 @@ name: 🚀 Feature Request
description: | description: |
Submit a proposal for a new feature Submit a proposal for a new feature
# title: "[Feature]" # title: "[Feature]"
labels: ["feature-request", "P3-low"] labels: ["feature-request"]
body: body:
- type: markdown - type: markdown
attributes: attributes:

View file

@ -3,7 +3,7 @@ name: 🛡️ Security Issue
description: | description: |
Notify Louis Lam about a security concern. Please do NOT include any sensitive details in this issue. Notify Louis Lam about a security concern. Please do NOT include any sensitive details in this issue.
# title: "Security Issue" # title: "Security Issue"
labels: ["security", "P1-high"] labels: ["security"]
assignees: [louislam] assignees: [louislam]
body: body:
- type: markdown - type: markdown

View file

@ -1,10 +1,10 @@
**⚠️ Please Note: We do not accept all types of pull requests, and we want to ensure we dont waste your time. Before submitting, make sure you have read our pull request guidelines: [Pull Request Rules](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma)**
## ❗ Important Announcement ## ❗ Important Announcement
<details><summary>Click here for more details:</summary> <details><summary>Click here for more details:</summary>
</p> </p>
**⚠️ Please Note: We do not accept all types of pull requests, and we want to ensure we dont waste your time. Before submitting, make sure you have read our pull request guidelines: [Pull Request Rules](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma)**
### 🚧 Temporary Delay in Feature Requests and Pull Request Reviews ### 🚧 Temporary Delay in Feature Requests and Pull Request Reviews
**At this time, we may be slower to respond to new feature requests and review pull requests. Existing requests and PRs will remain in the backlog but may not be prioritized immediately.** **At this time, we may be slower to respond to new feature requests and review pull requests. Existing requests and PRs will remain in the backlog but may not be prioritized immediately.**
@ -26,16 +26,22 @@ We appreciate your patience and understanding as we continue to improve Uptime K
## 📋 Overview ## 📋 Overview
Provide a clear summary of the purpose and scope of this pull request: <!-- Provide a clear summary of the purpose and scope of this pull request:-->
- **What problem does this pull request address?** - **What problem does this pull request address?**
- Please provide a detailed explanation here. - Please provide a detailed explanation here.
- **What features or functionality does this pull request introduce or enhance?** - **What features or functionality does this pull request introduce or enhance?**
- Please provide a detailed explanation here. - Please provide a detailed explanation here.
## 🔗 Related Issues
<!--
Please link any GitHub issues or tasks that this pull request addresses. Use the appropriate issue numbers or links.
-->
- Relates to #issue-number
- Resolves #issue-number
## 🔄 Changes ## 🔄 Changes
### 🛠️ Type of change ### 🛠️ Type of change
@ -52,19 +58,7 @@ Provide a clear summary of the purpose and scope of this pull request:
- [ ] 🔧 Other (please specify): - [ ] 🔧 Other (please specify):
- Provide additional details here. - Provide additional details here.
## 🔗 Related Issues ## 📄 Checklist
<!--
Please link any GitHub issues or tasks that this pull request addresses. Use the appropriate issue numbers or links.
**Note**: Include only issues directly related to this PR. Remove any irrelevant reference.
-->
- Relates to #issue-number
- Resolves #issue-number
- Fixes #issue-number
## 📄 Checklist *
<!-- Please select all options that apply --> <!-- Please select all options that apply -->
@ -97,26 +91,3 @@ If not, remove this section.
| `DOWN` | ![Before](image-link) | ![After](image-link) | | `DOWN` | ![Before](image-link) | ![After](image-link) |
| Certificate-expiry | ![Before](image-link) | ![After](image-link) | | Certificate-expiry | ![Before](image-link) | ![After](image-link) |
| Testing | ![Before](image-link) | ![After](image-link) | | Testing | ![Before](image-link) | ![After](image-link) |
## Additional Context
Provide any relevant details to assist reviewers in understanding the changes.
<details><summary>Click here for more details:</summary>
</p>
**Key Considerations**:
- **Design decisions** Key choices or trade-offs made during development.
- **Alternative solutions** Approaches considered but not implemented, along with reasons.
- **Relevant links** Specifications, discussions, or resources that provide context.
- **Dependencies** Related pull requests or issues that must be resolved before merging.
- **Additional context** Any other details that may help reviewers understand the changes.
Provide details here
## 💬 Requested Feedback
<!-- If a part of our docs is unclear, you are unsure how to do something/.. this is where we would appreciate your feedback -->
- `Mention documents needing feedback here`

36
.github/workflows/pr-reply.yml vendored Normal file
View file

@ -0,0 +1,36 @@
# Replys a message to all new PRs
# The message:
# - Say hello and thanks to the contributor
# - Mention maintainers will review the PR soon
# - To other people, show the testing pr command: npx kuma-pr <username:branch>
# - Also show the advanced usage link: https://github.com/louislam/uptime-kuma/wiki/Test-Pull-Requests
name: Reply to PRs
on:
pull_request:
types: [opened, reopened]
permissions:
issues: write
pull-requests: write
contents: read
jobs:
reply:
runs-on: ubuntu-latest
steps:
- name: Reply to PR
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const message = `Hello @${pr.user.login}, thank you for your contribution! :tada:\n` +
`The maintainers will review your PR soon.\n\n` +
`If anyone would like to help test this PR, you can use the command:\n` +
`\`\`\`bash\nnpx kuma-pr ${pr.user.login}:${pr.head.ref}\n\`\`\`\n\n` +
`<sub> For advanced usage, please refer to our [wiki](https://github.com/louislam/uptime-kuma/wiki/Test-Pull-Requests) </sub>`;
await github.rest.issues.createComment({
issue_number: pr.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: message
});

View file

@ -0,0 +1,12 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.string("smtp_security").defaultTo(null);
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("smtp_security");
});
};

View file

@ -0,0 +1,24 @@
/* SQL:
ALTER TABLE monitor ADD ping_count INTEGER default 1 not null;
ALTER TABLE monitor ADD ping_numeric BOOLEAN default true not null;
ALTER TABLE monitor ADD ping_per_request_timeout INTEGER default 2 not null;
*/
exports.up = function (knex) {
// Add new columns to table monitor
return knex.schema
.alterTable("monitor", function (table) {
table.integer("ping_count").defaultTo(1).notNullable();
table.boolean("ping_numeric").defaultTo(true).notNullable();
table.integer("ping_per_request_timeout").defaultTo(2).notNullable();
});
};
exports.down = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.dropColumn("ping_count");
table.dropColumn("ping_numeric");
table.dropColumn("ping_per_request_timeout");
});
};

View file

@ -0,0 +1,13 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.boolean("ip_family").defaultTo(null);
});
};
exports.down = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.dropColumn("ip_family");
});
};

View file

@ -0,0 +1,12 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.string("manual_status").defaultTo(null);
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("manual_status");
});
};

View file

@ -0,0 +1,13 @@
// Fix: Change manual_status column type to smallint
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.smallint("manual_status").alter();
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.string("manual_status").alter();
});
};

View file

@ -79,6 +79,10 @@ USER node
RUN git config --global user.email "no-reply@no-reply.com" RUN git config --global user.email "no-reply@no-reply.com"
RUN git config --global user.name "PR Tester" RUN git config --global user.name "PR Tester"
RUN git clone https://github.com/louislam/uptime-kuma.git . RUN git clone https://github.com/louislam/uptime-kuma.git .
# Hide the warning when running in detached head state
RUN git config --global advice.detachedHead false
RUN npm ci RUN npm ci
EXPOSE 3000 3001 EXPOSE 3000 3001

View file

@ -1,33 +0,0 @@
const childProcess = require("child_process");
if (!process.env.UPTIME_KUMA_GH_REPO) {
console.error("Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
process.exit(1);
}
let inputArray = process.env.UPTIME_KUMA_GH_REPO.split(":");
if (inputArray.length !== 2) {
console.error("Invalid format. Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
}
let name = inputArray[0];
let branch = inputArray[1];
console.log("Checkout pr");
// Checkout the pr
let result = childProcess.spawnSync("git", [ "remote", "add", name, `https://github.com/${name}/uptime-kuma` ]);
console.log(result.stdout.toString());
console.error(result.stderr.toString());
result = childProcess.spawnSync("git", [ "fetch", name, branch ]);
console.log(result.stdout.toString());
console.error(result.stderr.toString());
result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ]);
console.log(result.stdout.toString());
console.error(result.stderr.toString());

34
extra/checkout-pr.mjs Normal file
View file

@ -0,0 +1,34 @@
import childProcess from "child_process";
import { parsePrName } from "./kuma-pr/pr-lib.mjs";
let { name, branch } = parsePrName(process.env.UPTIME_KUMA_GH_REPO);
console.log(`Checking out PR from ${name}:${branch}`);
// Checkout the pr
let result = childProcess.spawnSync("git", [ "remote", "add", name, `https://github.com/${name}/uptime-kuma` ], {
stdio: "inherit"
});
if (result.status !== 0) {
console.error("Failed to add remote repository.");
process.exit(1);
}
result = childProcess.spawnSync("git", [ "fetch", name, branch ], {
stdio: "inherit"
});
if (result.status !== 0) {
console.error("Failed to fetch the branch.");
process.exit(1);
}
result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ], {
stdio: "inherit"
});
if (result.status !== 0) {
console.error("Failed to checkout the branch.");
process.exit(1);
}

26
extra/kuma-pr/index.mjs Normal file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env node
import { spawn } from "child_process";
import { parsePrName } from "./pr-lib.mjs";
const prName = process.argv[2];
// Pre-check the prName here, so testers don't need to wait until the Docker image is pulled to see the error.
try {
parsePrName(prName);
} catch (error) {
console.error(error.message);
process.exit(1);
}
spawn("docker", [
"run",
"--rm",
"-it",
"-p", "3000:3000",
"-p", "3001:3001",
"--pull", "always",
"-e", `UPTIME_KUMA_GH_REPO=${prName}`,
"louislam/uptime-kuma:pr-test2"
], {
stdio: "inherit",
});

View file

@ -0,0 +1,8 @@
{
"name": "kuma-pr",
"version": "1.0.0",
"type": "module",
"bin": {
"kuma-pr": "./index.mjs"
}
}

39
extra/kuma-pr/pr-lib.mjs Normal file
View file

@ -0,0 +1,39 @@
/**
* Parse <name>:<branch> to an object.
* @param {string} prName <name>:<branch>
* @returns {object} An object with name and branch properties.
*/
export function parsePrName(prName) {
let name = "louislam";
let branch;
const errorMessage = "Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)";
if (!prName) {
throw new Error(errorMessage);
}
prName = prName.trim();
if (prName === "") {
throw new Error(errorMessage);
}
let inputArray = prName.split(":");
// Just realized that owner's prs are not prefixed with "louislam:"
if (inputArray.length === 1) {
branch = inputArray[0];
} else if (inputArray.length === 2) {
name = inputArray[0];
branch = inputArray[1];
} else {
throw new Error("Invalid format. The format is like this: mhkarimi1383:goalert-notification");
}
return {
name,
branch
};
}

2029
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "2.0.0-beta.2", "version": "2.0.0-beta.3",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -32,7 +32,7 @@
"test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063", "test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063",
"playwright-codegen": "playwright codegen localhost:3000 --save-storage=./private/e2e-auth.json", "playwright-codegen": "playwright codegen localhost:3000 --save-storage=./private/e2e-auth.json",
"playwright-show-report": "playwright show-report ./private/playwright-report", "playwright-show-report": "playwright show-report ./private/playwright-report",
"tsc": "tsc", "tsc": "tsc --project ./tsconfig-backend.json",
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js", "vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
"build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2 --target base2 . --push", "build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2 --target base2 . --push",
"build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2-slim --target base2-slim . --push", "build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2-slim --target base2-slim . --push",
@ -57,7 +57,7 @@
"release-nightly": "node ./extra/release/nightly.mjs", "release-nightly": "node ./extra/release/nightly.mjs",
"git-remove-tag": "git tag -d", "git-remove-tag": "git tag -d",
"build-dist-and-restart": "npm run build && npm run start-server-dev", "build-dist-and-restart": "npm run build && npm run start-server-dev",
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev", "start-pr-test": "node extra/checkout-pr.mjs && npm install && npm run dev",
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go", "build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
"deploy-demo-server": "node extra/deploy-demo-server.js", "deploy-demo-server": "node extra/deploy-demo-server.js",
"sort-contributors": "node extra/sort-contributors.js", "sort-contributors": "node extra/sort-contributors.js",

View file

@ -26,7 +26,7 @@ exports.login = async function (username, password) {
// Upgrade the hash to bcrypt // Upgrade the hash to bcrypt
if (passwordHash.needRehash(user.password)) { if (passwordHash.needRehash(user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(password), await passwordHash.generate(password),
user.id, user.id,
]); ]);
} }

View file

@ -1,4 +1,5 @@
const fs = require("fs"); const fs = require("fs");
const fsAsync = fs.promises;
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server"); const { setSetting, setting } = require("./util-server");
const { log, sleep } = require("../src/util"); const { log, sleep } = require("../src/util");
@ -707,12 +708,12 @@ class Database {
/** /**
* Get the size of the database (SQLite only) * Get the size of the database (SQLite only)
* @returns {number} Size of database * @returns {Promise<number>} Size of database
*/ */
static getSize() { static async getSize() {
if (Database.dbConfig.type === "sqlite") { if (Database.dbConfig.type === "sqlite") {
log.debug("db", "Database.getSize()"); log.debug("db", "Database.getSize()");
let stats = fs.statSync(Database.sqlitePath); let stats = await fsAsync.stat(Database.sqlitePath);
log.debug("db", stats); log.debug("db", stats);
return stats.size; return stats.size;
} }
@ -736,7 +737,7 @@ class Database {
if (Database.dbConfig.type === "sqlite") { if (Database.dbConfig.type === "sqlite") {
return "DATETIME('now', ? || ' hours')"; return "DATETIME('now', ? || ' hours')";
} else { } else {
return "DATE_ADD(NOW(), INTERVAL ? HOUR)"; return "DATE_ADD(UTC_TIMESTAMP(), INTERVAL ? HOUR)";
} }
} }

View file

@ -158,12 +158,22 @@ class Maintenance extends BeanModel {
bean.active = obj.active; bean.active = obj.active;
if (obj.dateRange[0]) { if (obj.dateRange[0]) {
const parsedDate = new Date(obj.dateRange[0]);
if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) {
throw new Error("Invalid start date");
}
bean.start_date = obj.dateRange[0]; bean.start_date = obj.dateRange[0];
} else { } else {
bean.start_date = null; bean.start_date = null;
} }
if (obj.dateRange[1]) { if (obj.dateRange[1]) {
const parsedDate = new Date(obj.dateRange[1]);
if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) {
throw new Error("Invalid end date");
}
bean.end_date = obj.dateRange[1]; bean.end_date = obj.dateRange[1];
} else { } else {
bean.end_date = null; bean.end_date = null;
@ -233,7 +243,7 @@ class Maintenance extends BeanModel {
try { try {
this.beanMeta.status = "scheduled"; this.beanMeta.status = "scheduled";
let startEvent = (customDuration = 0) => { let startEvent = async (customDuration = 0) => {
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now"); log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
this.beanMeta.status = "under-maintenance"; this.beanMeta.status = "under-maintenance";

View file

@ -2,7 +2,11 @@ const dayjs = require("dayjs");
const axios = require("axios"); const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
SQL_DATETIME_FORMAT, evaluateJsonQuery SQL_DATETIME_FORMAT, evaluateJsonQuery,
PING_PACKET_SIZE_MIN, PING_PACKET_SIZE_MAX, PING_PACKET_SIZE_DEFAULT,
PING_GLOBAL_TIMEOUT_MIN, PING_GLOBAL_TIMEOUT_MAX, PING_GLOBAL_TIMEOUT_DEFAULT,
PING_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT,
PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT
} = require("../../src/util"); } = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery, const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
@ -153,8 +157,15 @@ class Monitor extends BeanModel {
snmpOid: this.snmpOid, snmpOid: this.snmpOid,
jsonPathOperator: this.jsonPathOperator, jsonPathOperator: this.jsonPathOperator,
snmpVersion: this.snmpVersion, snmpVersion: this.snmpVersion,
smtpSecurity: this.smtpSecurity,
rabbitmqNodes: JSON.parse(this.rabbitmqNodes), rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
conditions: JSON.parse(this.conditions), conditions: JSON.parse(this.conditions),
ipFamily: this.ipFamily,
// ping advanced options
ping_numeric: this.isPingNumeric(),
ping_count: this.ping_count,
ping_per_request_timeout: this.ping_per_request_timeout,
}; };
if (includeSensitiveData) { if (includeSensitiveData) {
@ -247,6 +258,14 @@ class Monitor extends BeanModel {
return Boolean(this.expiryNotification); return Boolean(this.expiryNotification);
} }
/**
* Check if ping should use numeric output only
* @returns {boolean} True if IP addresses will be output instead of symbolic hostnames
*/
isPingNumeric() {
return Boolean(this.ping_numeric);
}
/** /**
* Parse to boolean * Parse to boolean
* @returns {boolean} Should TLS errors be ignored? * @returns {boolean} Should TLS errors be ignored?
@ -408,10 +427,26 @@ class Monitor extends BeanModel {
} }
} }
let agentFamily = undefined;
if (this.ipFamily === "ipv4") {
agentFamily = 4;
}
if (this.ipFamily === "ipv6") {
agentFamily = 6;
}
const httpsAgentOptions = { const httpsAgentOptions = {
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(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
autoSelectFamily: true,
...(agentFamily ? { family: agentFamily } : {})
};
const httpAgentOptions = {
maxCachedSessions: 0,
autoSelectFamily: true,
...(agentFamily ? { family: agentFamily } : {})
}; };
log.debug("monitor", `[${this.name}] Prepare Options for axios`); log.debug("monitor", `[${this.name}] Prepare Options for axios`);
@ -473,6 +508,7 @@ class Monitor extends BeanModel {
if (proxy && proxy.active) { if (proxy && proxy.active) {
const { httpAgent, httpsAgent } = Proxy.createAgents(proxy, { const { httpAgent, httpsAgent } = Proxy.createAgents(proxy, {
httpsAgentOptions: httpsAgentOptions, httpsAgentOptions: httpsAgentOptions,
httpAgentOptions: httpAgentOptions,
}); });
options.proxy = false; options.proxy = false;
@ -481,6 +517,10 @@ class Monitor extends BeanModel {
} }
} }
if (!options.httpAgent) {
options.httpAgent = new http.Agent(httpAgentOptions);
}
if (!options.httpsAgent) { if (!options.httpsAgent) {
let jar = new CookieJar(); let jar = new CookieJar();
let httpsCookieAgentOptions = { let httpsCookieAgentOptions = {
@ -584,7 +624,7 @@ class Monitor extends BeanModel {
bean.status = UP; bean.status = UP;
} else if (this.type === "ping") { } else if (this.type === "ping") {
bean.ping = await ping(this.hostname, this.packetSize); bean.ping = await ping(this.hostname, this.ping_count, "", this.ping_numeric, this.packetSize, this.timeout, this.ping_per_request_timeout);
bean.msg = ""; bean.msg = "";
bean.status = UP; bean.status = UP;
} else if (this.type === "push") { // Type: Push } else if (this.type === "push") { // Type: Push
@ -656,7 +696,7 @@ class Monitor extends BeanModel {
bean.msg = res.data.response.servers[0].name; bean.msg = res.data.response.servers[0].name;
try { try {
bean.ping = await ping(this.hostname, this.packetSize); bean.ping = await ping(this.hostname, PING_COUNT_DEFAULT, "", true, this.packetSize, PING_GLOBAL_TIMEOUT_DEFAULT, PING_PER_REQUEST_TIMEOUT_DEFAULT);
} catch (_) { } } catch (_) { }
} else { } else {
throw new Error("Server not found on Steam"); throw new Error("Server not found on Steam");
@ -1468,6 +1508,31 @@ class Monitor extends BeanModel {
if (this.interval < MIN_INTERVAL_SECOND) { if (this.interval < MIN_INTERVAL_SECOND) {
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`); throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
} }
if (this.type === "ping") {
// ping parameters validation
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
throw new Error(`Packet size must be between ${PING_PACKET_SIZE_MIN} and ${PING_PACKET_SIZE_MAX} (default: ${PING_PACKET_SIZE_DEFAULT})`);
}
if (this.ping_per_request_timeout && (this.ping_per_request_timeout < PING_PER_REQUEST_TIMEOUT_MIN || this.ping_per_request_timeout > PING_PER_REQUEST_TIMEOUT_MAX)) {
throw new Error(`Per-ping timeout must be between ${PING_PER_REQUEST_TIMEOUT_MIN} and ${PING_PER_REQUEST_TIMEOUT_MAX} seconds (default: ${PING_PER_REQUEST_TIMEOUT_DEFAULT})`);
}
if (this.ping_count && (this.ping_count < PING_COUNT_MIN || this.ping_count > PING_COUNT_MAX)) {
throw new Error(`Echo requests count must be between ${PING_COUNT_MIN} and ${PING_COUNT_MAX} (default: ${PING_COUNT_DEFAULT})`);
}
if (this.timeout) {
const pingGlobalTimeout = Math.round(Number(this.timeout));
if (pingGlobalTimeout < this.ping_per_request_timeout || pingGlobalTimeout < PING_GLOBAL_TIMEOUT_MIN || pingGlobalTimeout > PING_GLOBAL_TIMEOUT_MAX) {
throw new Error(`Timeout must be between ${PING_GLOBAL_TIMEOUT_MIN} and ${PING_GLOBAL_TIMEOUT_MAX} seconds (default: ${PING_GLOBAL_TIMEOUT_DEFAULT})`);
}
this.timeout = pingGlobalTimeout;
}
}
} }
/** /**

View file

@ -120,8 +120,8 @@ class StatusPage extends BeanModel {
const head = $("head"); const head = $("head");
if (statusPage.googleAnalyticsTagId) { if (statusPage.google_analytics_tag_id) {
let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.googleAnalyticsTagId); let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.google_analytics_tag_id);
head.append($(escapedGoogleAnalyticsScript)); head.append($(escapedGoogleAnalyticsScript));
} }

View file

@ -14,7 +14,7 @@ class User extends BeanModel {
*/ */
static async resetPassword(userID, newPassword) { static async resetPassword(userID, newPassword) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(newPassword), await passwordHash.generate(newPassword),
userID userID
]); ]);
} }
@ -25,7 +25,7 @@ class User extends BeanModel {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async resetPassword(newPassword) { async resetPassword(newPassword) {
const hashedPassword = passwordHash.generate(newPassword); const hashedPassword = await passwordHash.generate(newPassword);
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
hashedPassword, hashedPassword,

View file

@ -89,6 +89,9 @@ function NtlmClient(credentials, AxiosConfig) {
switch (_b.label) { switch (_b.label) {
case 0: case 0:
error = err.response; error = err.response;
// The header may look like this: `Negotiate, NTLM, Basic realm="itsahiddenrealm.example.net"`Add commentMore actions
// so extract the 'NTLM' part first
const ntlmheader = error.headers['www-authenticate'].split(',').find(_ => _.match(/ *NTLM/))?.trim() || '';
if (!(error && error.status === 401 if (!(error && error.status === 401
&& error.headers['www-authenticate'] && error.headers['www-authenticate']
&& error.headers['www-authenticate'].includes('NTLM'))) return [3 /*break*/, 3]; && error.headers['www-authenticate'].includes('NTLM'))) return [3 /*break*/, 3];
@ -96,12 +99,12 @@ function NtlmClient(credentials, AxiosConfig) {
// include the Negotiate option when responding with the T2 message // include the Negotiate option when responding with the T2 message
// There is nore we could do to ensure we are processing correctly, // There is nore we could do to ensure we are processing correctly,
// but this is the easiest option for now // but this is the easiest option for now
if (error.headers['www-authenticate'].length < 50) { if (ntlmheader.length < 50) {
t1Msg = ntlm.createType1Message(credentials.workstation, credentials.domain); t1Msg = ntlm.createType1Message(credentials.workstation, credentials.domain);
error.config.headers["Authorization"] = t1Msg; error.config.headers["Authorization"] = t1Msg;
} }
else { else {
t2Msg = ntlm.decodeType2Message((error.headers['www-authenticate'].match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]); t2Msg = ntlm.decodeType2Message((ntlmheader.match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]);
t3Msg = ntlm.createType3Message(t2Msg, credentials.username, credentials.password, credentials.workstation, credentials.domain); t3Msg = ntlm.createType3Message(t2Msg, credentials.username, credentials.password, credentials.workstation, credentials.domain);
error.config.headers["X-retry"] = "false"; error.config.headers["X-retry"] = "false";
error.config.headers["Authorization"] = t3Msg; error.config.headers["Authorization"] = t3Msg;

View file

@ -0,0 +1,36 @@
const { MonitorType } = require("./monitor-type");
const { UP, DOWN, PENDING } = require("../../src/util");
class ManualMonitorType extends MonitorType {
name = "Manual";
type = "manual";
description = "A monitor that allows manual control of the status";
supportsConditions = false;
conditionVariables = [];
/**
* @inheritdoc
*/
async check(monitor, heartbeat) {
if (monitor.manual_status !== null) {
heartbeat.status = monitor.manual_status;
switch (monitor.manual_status) {
case UP:
heartbeat.msg = "Up";
break;
case DOWN:
heartbeat.msg = "Down";
break;
default:
heartbeat.msg = "Pending";
}
} else {
heartbeat.status = PENDING;
heartbeat.msg = "Manual monitoring - No status set";
}
}
}
module.exports = {
ManualMonitorType
};

View file

@ -0,0 +1,35 @@
const { MonitorType } = require("./monitor-type");
const { UP } = require("../../src/util");
const nodemailer = require("nodemailer");
class SMTPMonitorType extends MonitorType {
name = "smtp";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
let options = {
port: monitor.port || 25,
host: monitor.hostname,
secure: monitor.smtpSecurity === "secure", // use SMTPS (not STARTTLS)
ignoreTLS: monitor.smtpSecurity === "nostarttls", // don't use STARTTLS even if it's available
requireTLS: monitor.smtpSecurity === "starttls", // use STARTTLS or fail
};
let transporter = nodemailer.createTransport(options);
try {
await transporter.verify();
heartbeat.status = UP;
heartbeat.msg = "SMTP connection verifies successfully";
} catch (e) {
throw new Error(`SMTP connection doesn't verify: ${e}`);
} finally {
transporter.close();
}
}
}
module.exports = {
SMTPMonitorType,
};

View file

@ -18,17 +18,28 @@ class Discord extends NotificationProvider {
webhookUrl.searchParams.append("thread_id", notification.threadId); webhookUrl.searchParams.append("thread_id", notification.threadId);
} }
// Check if the webhook has an avatar
let webhookHasAvatar = true;
try {
const webhookInfo = await axios.get(webhookUrl.toString());
webhookHasAvatar = !!webhookInfo.data.avatar;
} catch (e) {
// If we can't verify, we assume he has an avatar to avoid forcing the default avatar
webhookHasAvatar = true;
}
// If heartbeatJSON is null, assume we're testing. // If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) { if (heartbeatJSON == null) {
let discordtestdata = { let discordtestdata = {
username: discordDisplayName, username: discordDisplayName,
content: msg, content: msg,
}; };
if (!webhookHasAvatar) {
discordtestdata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
if (notification.discordChannelType === "createNewForumPost") { if (notification.discordChannelType === "createNewForumPost") {
discordtestdata.thread_name = notification.postName; discordtestdata.thread_name = notification.postName;
} }
await axios.post(webhookUrl.toString(), discordtestdata); await axios.post(webhookUrl.toString(), discordtestdata);
return okMsg; return okMsg;
} }
@ -61,6 +72,9 @@ class Discord extends NotificationProvider {
], ],
}], }],
}; };
if (!webhookHasAvatar) {
discorddowndata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
if (notification.discordChannelType === "createNewForumPost") { if (notification.discordChannelType === "createNewForumPost") {
discorddowndata.thread_name = notification.postName; discorddowndata.thread_name = notification.postName;
} }
@ -98,6 +112,9 @@ class Discord extends NotificationProvider {
], ],
}], }],
}; };
if (!webhookHasAvatar) {
discordupdata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
if (notification.discordChannelType === "createNewForumPost") { if (notification.discordChannelType === "createNewForumPost") {
discordupdata.thread_name = notification.postName; discordupdata.thread_name = notification.postName;

View file

@ -73,13 +73,13 @@ class FlashDuty extends NotificationProvider {
} }
const options = { const options = {
method: "POST", method: "POST",
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey, url: notification.flashdutyIntegrationKey.startsWith("http") ? notification.flashdutyIntegrationKey : "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
data: { data: {
description: `[${title}] [${monitorInfo.name}] ${body}`, description: `[${title}] [${monitorInfo.name}] ${body}`,
title, title,
event_status: eventStatus || "Info", event_status: eventStatus || "Info",
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7), alert_key: monitorInfo.id ? String(monitorInfo.id) : Math.random().toString(36).substring(7),
labels, labels,
} }
}; };

View file

@ -41,8 +41,8 @@ class Ntfy extends NotificationProvider {
if (heartbeatJSON.status === DOWN) { if (heartbeatJSON.status === DOWN) {
tags = [ "red_circle" ]; tags = [ "red_circle" ];
status = "Down"; status = "Down";
// if priority is not 5, increase priority for down alerts // defaults to max(priority + 1, 5)
priority = priority === 5 ? priority : priority + 1; priority = notification.ntfyPriorityDown || (priority === 5 ? priority : priority + 1);
} else if (heartbeatJSON["status"] === UP) { } else if (heartbeatJSON["status"] === UP) {
tags = [ "green_circle" ]; tags = [ "green_circle" ];
status = "Up"; status = "Up";

View file

@ -5,10 +5,10 @@ const saltRounds = 10;
/** /**
* Hash a password * Hash a password
* @param {string} password Password to hash * @param {string} password Password to hash
* @returns {string} Hash * @returns {Promise<string>} Hash
*/ */
exports.generate = function (password) { exports.generate = function (password) {
return bcrypt.hashSync(password, saltRounds); return bcrypt.hash(password, saltRounds);
}; };
/** /**

View file

@ -2,6 +2,7 @@ const PrometheusClient = require("prom-client");
const { log } = require("../src/util"); const { log } = require("../src/util");
const commonLabels = [ const commonLabels = [
"monitor_id",
"monitor_name", "monitor_name",
"monitor_type", "monitor_type",
"monitor_url", "monitor_url",
@ -42,6 +43,7 @@ class Prometheus {
*/ */
constructor(monitor, tags) { constructor(monitor, tags) {
this.monitorLabelValues = { this.monitorLabelValues = {
monitor_id: monitor.id,
monitor_name: monitor.name, monitor_name: monitor.name,
monitor_type: monitor.type, monitor_type: monitor.type,
monitor_url: monitor.url, monitor_url: monitor.url,

View file

@ -50,7 +50,7 @@ router.all("/api/push/:pushToken", async (request, response) => {
let msg = request.query.msg || "OK"; let msg = request.query.msg || "OK";
let ping = parseFloat(request.query.ping) || null; let ping = parseFloat(request.query.ping) || null;
let statusString = request.query.status || "up"; let statusString = request.query.status || "up";
let status = (statusString === "up") ? UP : DOWN; const statusFromParam = (statusString === "up") ? UP : DOWN;
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [ let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
pushToken pushToken
@ -80,7 +80,7 @@ router.all("/api/push/:pushToken", async (request, response) => {
msg = "Monitor under maintenance"; msg = "Monitor under maintenance";
bean.status = MAINTENANCE; bean.status = MAINTENANCE;
} else { } else {
determineStatus(status, previousHeartbeat, monitor.maxretries, monitor.isUpsideDown(), bean); determineStatus(statusFromParam, previousHeartbeat, monitor.maxretries, monitor.isUpsideDown(), bean);
} }
// Calculate uptime // Calculate uptime
@ -92,21 +92,21 @@ router.all("/api/push/:pushToken", async (request, response) => {
log.debug("router", "PreviousStatus: " + previousHeartbeat?.status); log.debug("router", "PreviousStatus: " + previousHeartbeat?.status);
log.debug("router", "Current Status: " + bean.status); log.debug("router", "Current Status: " + bean.status);
bean.important = Monitor.isImportantBeat(isFirstBeat, previousHeartbeat?.status, status); bean.important = Monitor.isImportantBeat(isFirstBeat, previousHeartbeat?.status, bean.status);
if (Monitor.isImportantForNotification(isFirstBeat, previousHeartbeat?.status, status)) { if (Monitor.isImportantForNotification(isFirstBeat, previousHeartbeat?.status, bean.status)) {
// Reset down count // Reset down count
bean.downCount = 0; bean.downCount = 0;
log.debug("monitor", `[${this.name}] sendNotification`); log.debug("monitor", `[${monitor.name}] sendNotification`);
await Monitor.sendNotification(isFirstBeat, monitor, bean); await Monitor.sendNotification(isFirstBeat, monitor, bean);
} else { } else {
if (bean.status === DOWN && this.resendInterval > 0) { if (bean.status === DOWN && monitor.resendInterval > 0) {
++bean.downCount; ++bean.downCount;
if (bean.downCount >= this.resendInterval) { if (bean.downCount >= monitor.resendInterval) {
// Send notification again, because we are still DOWN // Send notification again, because we are still DOWN
log.debug("monitor", `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`); log.debug("monitor", `[${monitor.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${monitor.resendInterval}`);
await Monitor.sendNotification(isFirstBeat, this, bean); await Monitor.sendNotification(isFirstBeat, monitor, bean);
// Reset down count // Reset down count
bean.downCount = 0; bean.downCount = 0;

View file

@ -674,7 +674,7 @@ let needSetup = false;
let user = R.dispense("user"); let user = R.dispense("user");
user.username = username; user.username = username;
user.password = passwordHash.generate(password); user.password = await passwordHash.generate(password);
await R.store(user); await R.store(user);
needSetup = false; needSetup = false;
@ -792,6 +792,7 @@ let needSetup = false;
bean.url = monitor.url; bean.url = monitor.url;
bean.method = monitor.method; bean.method = monitor.method;
bean.body = monitor.body; bean.body = monitor.body;
bean.ipFamily = monitor.ipFamily;
bean.headers = monitor.headers; bean.headers = monitor.headers;
bean.basic_auth_user = monitor.basic_auth_user; bean.basic_auth_user = monitor.basic_auth_user;
bean.basic_auth_pass = monitor.basic_auth_pass; bean.basic_auth_pass = monitor.basic_auth_pass;
@ -866,6 +867,7 @@ let needSetup = false;
monitor.kafkaProducerAllowAutoTopicCreation; monitor.kafkaProducerAllowAutoTopicCreation;
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly; bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
bean.remote_browser = monitor.remote_browser; bean.remote_browser = monitor.remote_browser;
bean.smtpSecurity = monitor.smtpSecurity;
bean.snmpVersion = monitor.snmpVersion; bean.snmpVersion = monitor.snmpVersion;
bean.snmpOid = monitor.snmpOid; bean.snmpOid = monitor.snmpOid;
bean.jsonPathOperator = monitor.jsonPathOperator; bean.jsonPathOperator = monitor.jsonPathOperator;
@ -874,6 +876,12 @@ let needSetup = false;
bean.rabbitmqUsername = monitor.rabbitmqUsername; bean.rabbitmqUsername = monitor.rabbitmqUsername;
bean.rabbitmqPassword = monitor.rabbitmqPassword; bean.rabbitmqPassword = monitor.rabbitmqPassword;
bean.conditions = JSON.stringify(monitor.conditions); bean.conditions = JSON.stringify(monitor.conditions);
bean.manual_status = monitor.manual_status;
// ping advanced options
bean.ping_numeric = monitor.ping_numeric;
bean.ping_count = monitor.ping_count;
bean.ping_per_request_timeout = monitor.ping_per_request_timeout;
bean.validate(); bean.validate();

View file

@ -20,7 +20,7 @@ module.exports.apiKeySocketHandler = (socket) => {
checkLogin(socket); checkLogin(socket);
let clearKey = nanoid(40); let clearKey = nanoid(40);
let hashedKey = passwordHash.generate(clearKey); let hashedKey = await passwordHash.generate(clearKey);
key["key"] = hashedKey; key["key"] = hashedKey;
let bean = await APIKey.save(key, socket.userID); let bean = await APIKey.save(key, socket.userID);

View file

@ -14,7 +14,7 @@ module.exports.databaseSocketHandler = (socket) => {
checkLogin(socket); checkLogin(socket);
callback({ callback({
ok: true, ok: true,
size: Database.getSize(), size: await Database.getSize(),
}); });
} catch (error) { } catch (error) {
callback({ callback({

View file

@ -4,7 +4,7 @@ const { sendInfo } = require("../client");
const { checkLogin } = require("../util-server"); const { checkLogin } = require("../util-server");
const GameResolver = require("gamedig/lib/GameResolver"); const GameResolver = require("gamedig/lib/GameResolver");
const { testChrome } = require("../monitor-types/real-browser-monitor-type"); const { testChrome } = require("../monitor-types/real-browser-monitor-type");
const fs = require("fs"); const fsAsync = require("fs").promises;
const path = require("path"); const path = require("path");
let gameResolver = new GameResolver(); let gameResolver = new GameResolver();
@ -90,17 +90,29 @@ module.exports.generalSocketHandler = (socket, server) => {
} }
}); });
socket.on("getPushExample", (language, callback) => { socket.on("getPushExample", async (language, callback) => {
try {
checkLogin(socket);
if (!/^[a-z-]+$/.test(language)) {
throw new Error("Invalid language");
}
} catch (e) {
callback({
ok: false,
msg: e.message,
});
return;
}
try { try {
let dir = path.join("./extra/push-examples", language); let dir = path.join("./extra/push-examples", language);
let files = fs.readdirSync(dir); let files = await fsAsync.readdir(dir);
for (let file of files) { for (let file of files) {
if (file.startsWith("index.")) { if (file.startsWith("index.")) {
callback({ callback({
ok: true, ok: true,
code: fs.readFileSync(path.join(dir, file), "utf8"), code: await fsAsync.readFile(path.join(dir, file), "utf8"),
}); });
return; return;
} }

View file

@ -113,10 +113,12 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing(); UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType(); UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType(); UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
UptimeKumaServer.monitorTypeList["smtp"] = new SMTPMonitorType();
UptimeKumaServer.monitorTypeList["group"] = new GroupMonitorType(); UptimeKumaServer.monitorTypeList["group"] = new GroupMonitorType();
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType(); UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType(); UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType(); UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType();
// Allow all CORS origins (polling) in development // Allow all CORS origins (polling) in development
let cors = undefined; let cors = undefined;
@ -552,8 +554,10 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor
const { TailscalePing } = require("./monitor-types/tailscale-ping"); const { TailscalePing } = require("./monitor-types/tailscale-ping");
const { DnsMonitorType } = require("./monitor-types/dns"); const { DnsMonitorType } = require("./monitor-types/dns");
const { MqttMonitorType } = require("./monitor-types/mqtt"); const { MqttMonitorType } = require("./monitor-types/mqtt");
const { SMTPMonitorType } = require("./monitor-types/smtp");
const { GroupMonitorType } = require("./monitor-types/group"); const { GroupMonitorType } = require("./monitor-types/group");
const { SNMPMonitorType } = require("./monitor-types/snmp"); const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb"); const { MongodbMonitorType } = require("./monitor-types/mongodb");
const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq"); const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
const { ManualMonitorType } = require("./monitor-types/manual");
const Monitor = require("./model/monitor"); const Monitor = require("./model/monitor");

View file

@ -1,7 +1,11 @@
const tcpp = require("tcp-ping"); const tcpp = require("tcp-ping");
const ping = require("@louislam/ping"); const ping = require("@louislam/ping");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { log, genSecret, badgeConstants } = require("../src/util"); const {
log, genSecret, badgeConstants,
PING_PACKET_SIZE_DEFAULT, PING_GLOBAL_TIMEOUT_DEFAULT,
PING_COUNT_DEFAULT, PING_PER_REQUEST_TIMEOUT_DEFAULT
} = require("../src/util");
const passwordHash = require("./password-hash"); const passwordHash = require("./password-hash");
const { Resolver } = require("dns"); const { Resolver } = require("dns");
const iconv = require("iconv-lite"); const iconv = require("iconv-lite");
@ -47,7 +51,7 @@ exports.initJWTSecret = async () => {
jwtSecretBean.key = "jwtSecret"; jwtSecretBean.key = "jwtSecret";
} }
jwtSecretBean.value = passwordHash.generate(genSecret()); jwtSecretBean.value = await passwordHash.generate(genSecret());
await R.store(jwtSecretBean); await R.store(jwtSecretBean);
return jwtSecretBean; return jwtSecretBean;
}; };
@ -118,20 +122,33 @@ exports.tcping = function (hostname, port) {
/** /**
* Ping the specified machine * Ping the specified machine
* @param {string} hostname Hostname / address of machine * @param {string} destAddr Hostname / IP address of machine to ping
* @param {number} size Size of packet to send * @param {number} count Number of packets to send before stopping
* @param {string} sourceAddr Source address for sending/receiving echo requests
* @param {boolean} numeric If true, IP addresses will be output instead of symbolic hostnames
* @param {number} size Size (in bytes) of echo request to send
* @param {number} deadline Maximum time in seconds before ping stops, regardless of packets sent
* @param {number} timeout Maximum time in seconds to wait for each response
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer * @returns {Promise<number>} Time for ping in ms rounded to nearest integer
*/ */
exports.ping = async (hostname, size = 56) => { exports.ping = async (
destAddr,
count = PING_COUNT_DEFAULT,
sourceAddr = "",
numeric = true,
size = PING_PACKET_SIZE_DEFAULT,
deadline = PING_GLOBAL_TIMEOUT_DEFAULT,
timeout = PING_PER_REQUEST_TIMEOUT_DEFAULT,
) => {
try { try {
return await exports.pingAsync(hostname, false, size); return await exports.pingAsync(destAddr, false, count, sourceAddr, numeric, size, deadline, timeout);
} catch (e) { } catch (e) {
// If the host cannot be resolved, try again with ipv6 // If the host cannot be resolved, try again with ipv6
log.debug("ping", "IPv6 error message: " + e.message); log.debug("ping", "IPv6 error message: " + e.message);
// As node-ping does not report a specific error for this, try again if it is an empty message with ipv6 no matter what. // As node-ping does not report a specific error for this, try again if it is an empty message with ipv6 no matter what.
if (!e.message) { if (!e.message) {
return await exports.pingAsync(hostname, true, size); return await exports.pingAsync(destAddr, true, count, sourceAddr, numeric, size, deadline, timeout);
} else { } else {
throw e; throw e;
} }
@ -140,18 +157,35 @@ exports.ping = async (hostname, size = 56) => {
/** /**
* Ping the specified machine * Ping the specified machine
* @param {string} hostname Hostname / address of machine to ping * @param {string} destAddr Hostname / IP address of machine to ping
* @param {boolean} ipv6 Should IPv6 be used? * @param {boolean} ipv6 Should IPv6 be used?
* @param {number} size Size of ping packet to send * @param {number} count Number of packets to send before stopping
* @param {string} sourceAddr Source address for sending/receiving echo requests
* @param {boolean} numeric If true, IP addresses will be output instead of symbolic hostnames
* @param {number} size Size (in bytes) of echo request to send
* @param {number} deadline Maximum time in seconds before ping stops, regardless of packets sent
* @param {number} timeout Maximum time in seconds to wait for each response
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer * @returns {Promise<number>} Time for ping in ms rounded to nearest integer
*/ */
exports.pingAsync = function (hostname, ipv6 = false, size = 56) { exports.pingAsync = function (
destAddr,
ipv6 = false,
count = PING_COUNT_DEFAULT,
sourceAddr = "",
numeric = true,
size = PING_PACKET_SIZE_DEFAULT,
deadline = PING_GLOBAL_TIMEOUT_DEFAULT,
timeout = PING_PER_REQUEST_TIMEOUT_DEFAULT,
) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
ping.promise.probe(hostname, { ping.promise.probe(destAddr, {
v6: ipv6, v6: ipv6,
min_reply: 1, min_reply: count,
deadline: 10, sourceAddr: sourceAddr,
numeric: numeric,
packetSize: size, packetSize: size,
deadline: deadline,
timeout: timeout
}).then((res) => { }).then((res) => {
// If ping failed, it will set field to unknown // If ping failed, it will set field to unknown
if (res.alive) { if (res.alive) {

View file

@ -174,6 +174,38 @@ export default {
this.modal.show(); this.modal.show();
}, },
/**
* Show dialog to clone a proxy
* @param {number} proxyID ID of proxy to clone
* @returns {void}
*/
showClone(proxyID) {
if (proxyID) {
for (let proxy of this.$root.proxyList) {
if (proxy.id === proxyID) {
// Create a clone of the proxy data
this.proxy = {
protocol: proxy.protocol,
host: proxy.host,
port: proxy.port,
auth: proxy.auth,
username: proxy.username,
password: proxy.password,
active: proxy.active,
default: false, // Cloned proxy should not be default
applyExisting: false,
};
break;
}
}
}
// Set id to null to indicate this is a new proxy (clone)
this.id = null;
this.modal.show();
},
/** /**
* Submit form data for saving * Submit form data for saving
* @returns {void} * @returns {void}

View file

@ -4,7 +4,7 @@
<div v-if="selectedTags.length > 0" class="mb-2 p-1"> <div v-if="selectedTags.length > 0" class="mb-2 p-1">
<tag <tag
v-for="item in selectedTags" v-for="item in selectedTags"
:key="item.id" :key="`${item.tag_id || item.id}-${item.value || ''}`"
:item="item" :item="item"
:remove="deleteTag" :remove="deleteTag"
/> />
@ -20,10 +20,20 @@
<font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }} <font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }}
</button> </button>
</div> </div>
<div ref="modal" class="modal fade" tabindex="-1"> <div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-body"> <div class="modal-body">
<h4 v-if="stagedForBatchAdd.length > 0">{{ $t("Add Tags") }}</h4>
<div v-if="stagedForBatchAdd.length > 0" class="mb-3 staging-area" style="max-height: 150px; overflow-y: auto;">
<Tag
v-for="stagedTag in stagedForBatchAdd"
:key="stagedTag.keyForList"
:item="mapStagedTagToDisplayItem(stagedTag)"
:remove="() => unstageTag(stagedTag)"
/>
</div>
<vue-multiselect <vue-multiselect
v-model="newDraftTag.select" v-model="newDraftTag.select"
class="mb-2" class="mb-2"
@ -58,14 +68,11 @@
<div class="w-50 pe-2"> <div class="w-50 pe-2">
<input <input
v-model="newDraftTag.name" class="form-control" v-model="newDraftTag.name" class="form-control"
:class="{'is-invalid': validateDraftTag.nameInvalid}" :class="{'is-invalid': validateDraftTag.invalid && (validateDraftTag.messageKey === 'tagNameColorRequired' || validateDraftTag.messageKey === 'tagNameExists')}"
:placeholder="$t('Name')" :placeholder="$t('Name')"
data-testid="tag-name-input" data-testid="tag-name-input"
@keydown.enter.prevent="onEnter" @keydown.enter.prevent="onEnter"
/> />
<div class="invalid-feedback">
{{ $t("Tag with this name already exist.") }}
</div>
</div> </div>
<div class="w-50 ps-2"> <div class="w-50 ps-2">
<vue-multiselect <vue-multiselect
@ -104,27 +111,24 @@
<div class="mb-2"> <div class="mb-2">
<input <input
v-model="newDraftTag.value" class="form-control" v-model="newDraftTag.value" class="form-control"
:class="{'is-invalid': validateDraftTag.valueInvalid}" :class="{'is-invalid': validateDraftTag.invalid && validateDraftTag.messageKey === 'tagAlreadyOnMonitor'}"
:placeholder="$t('value (optional)')" :placeholder="$t('value (optional)')"
data-testid="tag-value-input" data-testid="tag-value-input"
@keydown.enter.prevent="onEnter" @keydown.enter.prevent="onEnter"
/> />
<div class="invalid-feedback">
{{ $t("Tag with this value already exist.") }}
</div>
</div> </div>
<div class="mb-2">
<button <div v-if="validateDraftTag.invalid && validateDraftTag.messageKey" class="form-text text-danger mb-2">
type="button" {{ $t(validateDraftTag.messageKey, validateDraftTag.messageParams) }}
class="btn btn-secondary float-end"
:disabled="processing || validateDraftTag.invalid"
data-testid="tag-submit-button"
@click.stop="addDraftTag"
>
{{ $t("Add") }}
</button>
</div> </div>
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @click.stop="clearStagingAndCloseModal">{{ $t("Cancel") }}</button>
<button type="button" class="btn btn-outline-primary me-2" :disabled="processing || validateDraftTag.invalid" @click.stop="stageCurrentTag">
{{ $t("Add Another Tag") }}
</button>
<button type="button" class="btn btn-primary" :disabled="processing || (stagedForBatchAdd.length === 0 && validateDraftTag.invalid)" data-testid="add-tags-final-button" @click.stop="confirmAndCommitStagedTags">{{ $t("Done") }}</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -176,71 +180,146 @@ export default {
newTags: [], newTags: [],
/** @type {Tag[]} */ /** @type {Tag[]} */
deleteTags: [], deleteTags: [],
/**
* @type {Array<object>} Holds tag objects staged for addition.
* Each object: { name, color, value, isNewSystemTag, systemTagId, keyForList }
*/
stagedForBatchAdd: [],
newDraftTag: { newDraftTag: {
name: null, name: null,
select: null, select: null,
color: null, color: null,
value: "", value: "",
invalid: true,
nameInvalid: false,
}, },
}; };
}, },
computed: { computed: {
tagOptions() { tagOptions() {
const tagOptions = this.existingTags; const tagOptions = [ ...this.existingTags ]; // Create a copy
// Add tags from newTags
for (const tag of this.newTags) { for (const tag of this.newTags) {
if (!tagOptions.find(t => t.name === tag.name && t.color === tag.color)) { if (!tagOptions.find(t => t.name === tag.name && t.color === tag.color)) {
tagOptions.push(tag); tagOptions.push(tag);
} }
} }
// Add newly created system tags from staging area
for (const stagedTag of this.stagedForBatchAdd) {
if (stagedTag.isNewSystemTag) {
// Check if this system tag is already in the options
if (!tagOptions.find(t => t.name === stagedTag.name && t.color === stagedTag.color)) {
// Create a tag option object for the dropdown
tagOptions.push({
id: null, // Will be assigned when actually created
name: stagedTag.name,
color: stagedTag.color
});
}
}
}
return tagOptions; return tagOptions;
}, },
selectedTags() { selectedTags() {
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.tag_id === tag.tag_id)); // Helper function to normalize tag values for comparison
const normalizeValue = (value) => {
if (value === null || value === undefined) {
return "";
}
return String(value).trim();
};
// Helper function to get tag ID from different structures
const getTagId = (tag) => tag.tag_id || tag.id;
return this.preSelectedTags.concat(this.newTags).filter(tag =>
!this.deleteTags.find(monitorTag => {
const tagIdMatch = getTagId(monitorTag) === getTagId(tag);
const valueMatch = normalizeValue(monitorTag.value) === normalizeValue(tag.value);
return tagIdMatch && valueMatch;
})
);
}, },
/**
* @returns {boolean} True if more new system tags can be staged, false otherwise.
*/
canStageMoreNewSystemTags() {
return true; // Always allow adding more tags, no limit
},
/**
* Provides the color options for the tag color selector.
* @returns {Array<object>} Array of color options.
*/
colorOptions() { colorOptions() {
return colorOptions(this); return colorOptions(this);
}, },
/**
* Validates the current draft tag based on several conditions.
* @returns {{invalid: boolean, messageKey: string|null, messageParams: object|null}} Object indicating validity, and a message key/params if invalid.
*/
validateDraftTag() { validateDraftTag() {
let nameInvalid = false; // If defining a new system tag (newDraftTag.select == null)
let valueInvalid = false; if (this.newDraftTag.select == null) {
let invalid = true; if (!this.newDraftTag.name || this.newDraftTag.name.trim() === "" || !this.newDraftTag.color) {
if (this.deleteTags.find(tag => tag.name === this.newDraftTag.select?.name && tag.value === this.newDraftTag.value)) { // Keep button disabled, but don't show the explicit message for this case
// Undo removing a Tag return {
nameInvalid = false; invalid: true,
valueInvalid = false; messageKey: null,
invalid = false; messageParams: null,
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0 && this.newDraftTag.select == null) { };
// Try to create new tag with existing name }
nameInvalid = true; if (this.tagOptions.find(opt => opt.name.toLowerCase() === this.newDraftTag.name.trim().toLowerCase())) {
invalid = true; return {
} else if (this.newTags.concat(this.preSelectedTags).filter(tag => ( invalid: true,
tag.name === this.newDraftTag.select?.name && tag.value === this.newDraftTag.value messageKey: "tagNameExists",
) || ( messageParams: null,
tag.name === this.newDraftTag.name && tag.value === this.newDraftTag.value };
)).length > 0) { }
// Try to add a tag with existing name and value
valueInvalid = true;
invalid = true;
} else if (this.newDraftTag.select != null) {
// Select an existing tag, no need to validate
invalid = false;
valueInvalid = false;
} else if (this.newDraftTag.color == null || this.newDraftTag.name === "") {
// Missing form inputs
nameInvalid = false;
invalid = true;
} else {
// Looks valid
invalid = false;
nameInvalid = false;
valueInvalid = false;
} }
// For any tag definition (new or existing system tag + value)
const draftTagName = this.newDraftTag.select ? this.newDraftTag.select.name : this.newDraftTag.name.trim();
const draftTagValue = this.newDraftTag.value ? this.newDraftTag.value.trim() : ""; // Treat null/undefined value as empty string for comparison
// Check if (name + value) combination already exists in this.stagedForBatchAdd
if (this.stagedForBatchAdd.find(staged => staged.name === draftTagName && staged.value === draftTagValue)) {
return {
invalid: true,
messageKey: "tagAlreadyStaged",
messageParams: null,
};
}
// Check if (name + value) combination already exists in this.selectedTags (final list on monitor)
// AND it's NOT an "undo delete"
const isUndoDelete = this.deleteTags.find(dTag =>
dTag.tag_id === (this.newDraftTag.select ? this.newDraftTag.select.id : null) &&
dTag.value === draftTagValue
);
if (!isUndoDelete && this.selectedTags.find(sTag => sTag.name === draftTagName && sTag.value === draftTagValue)) {
return {
invalid: true,
messageKey: "tagAlreadyOnMonitor",
messageParams: null,
};
}
// If an existing tag is selected at this point, it has passed all relevant checks
if (this.newDraftTag.select != null) {
return {
invalid: false,
messageKey: null,
messageParams: null,
};
}
// If it's a new tag definition, and it passed its specific checks, it's valid.
// (This also serves as a final default to valid if other logic paths were missed, though ideally covered above)
return { return {
invalid, invalid: false,
nameInvalid, messageKey: null,
valueInvalid, messageParams: null,
}; };
}, },
}, },
@ -257,6 +336,9 @@ export default {
* @returns {void} * @returns {void}
*/ */
showAddDialog() { showAddDialog() {
this.stagedForBatchAdd = [];
this.clearDraftTag();
this.getExistingTags();
this.modal.show(); this.modal.show();
}, },
/** /**
@ -300,37 +382,6 @@ export default {
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit"; return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
} }
}, },
/**
* Add a draft tag
* @returns {void}
*/
addDraftTag() {
console.log("Adding Draft Tag: ", this.newDraftTag);
if (this.newDraftTag.select != null) {
if (this.deleteTags.find(tag => tag.name === this.newDraftTag.select.name && tag.value === this.newDraftTag.value)) {
// Undo removing a tag
this.deleteTags = this.deleteTags.filter(tag => !(tag.name === this.newDraftTag.select.name && tag.value === this.newDraftTag.value));
} else {
// Add an existing Tag
this.newTags.push({
id: this.newDraftTag.select.id,
color: this.newDraftTag.select.color,
name: this.newDraftTag.select.name,
value: this.newDraftTag.value,
new: true,
});
}
} else {
// Add new Tag
this.newTags.push({
color: this.newDraftTag.color.color,
name: this.newDraftTag.name.trim(),
value: this.newDraftTag.value,
new: true,
});
}
this.clearDraftTag();
},
/** /**
* Remove a draft tag * Remove a draft tag
* @returns {void} * @returns {void}
@ -341,10 +392,8 @@ export default {
select: null, select: null,
color: null, color: null,
value: "", value: "",
invalid: true, // invalid: true, // Initial validation will be handled by computed prop
nameInvalid: false,
}; };
this.modal.hide();
}, },
/** /**
* Add a tag asynchronously * Add a tag asynchronously
@ -386,7 +435,7 @@ export default {
*/ */
onEnter() { onEnter() {
if (!this.validateDraftTag.invalid) { if (!this.validateDraftTag.invalid) {
this.addDraftTag(); this.stageCurrentTag();
} }
}, },
/** /**
@ -475,7 +524,119 @@ export default {
console.warn("Modal hide failed:", e); console.warn("Modal hide failed:", e);
} }
} }
} this.stagedForBatchAdd = [];
},
/**
* Stages the current draft tag for batch addition.
* @returns {void}
*/
stageCurrentTag() {
if (this.validateDraftTag.invalid) {
return;
}
const isNew = this.newDraftTag.select == null;
const name = isNew ? this.newDraftTag.name.trim() : this.newDraftTag.select.name;
const color = isNew ? this.newDraftTag.color.color : this.newDraftTag.select.color;
const value = this.newDraftTag.value ? this.newDraftTag.value.trim() : "";
const stagedTagObject = {
name: name,
color: color,
value: value,
isNewSystemTag: isNew,
systemTagId: isNew ? null : this.newDraftTag.select.id,
keyForList: `staged-${Date.now()}-${Math.random().toString(36).substring(2, 15)}` // Unique key
};
this.stagedForBatchAdd.push(stagedTagObject);
this.clearDraftTag(); // Reset input fields for the next tag
},
/**
* Removes a tag from the staged list.
* @param {object} tagToUnstage The tag object to remove from staging.
* @returns {void}
*/
unstageTag(tagToUnstage) {
this.stagedForBatchAdd = this.stagedForBatchAdd.filter(tag => tag.keyForList !== tagToUnstage.keyForList);
},
/**
* Maps a staged tag object to the structure expected by the Tag component.
* @param {object} stagedTag The staged tag object.
* @returns {object} Object with name, color, value for the Tag component.
*/
mapStagedTagToDisplayItem(stagedTag) {
return {
name: stagedTag.name,
color: stagedTag.color,
value: stagedTag.value,
// id: stagedTag.keyForList, // Pass keyForList as id for the Tag component if it expects an id for display/keying internally beyond v-for key
};
},
/**
* Clears the staging list, draft inputs, and closes the modal.
* @returns {void}
*/
clearStagingAndCloseModal() {
this.stagedForBatchAdd = [];
this.clearDraftTag(); // Clears input fields
this.modal.hide();
},
/**
* Processes all staged tags, adds them to the monitor, and closes the modal.
* @returns {void}
*/
confirmAndCommitStagedTags() {
// Phase 1: If there's a currently valid newDraftTag that hasn't been staged yet,
// (e.g. user typed a full tag and directly clicked the footer "Add"), then stage it now.
// stageCurrentTag has its own check for validateDraftTag.invalid and will clear the draft.
if (!this.validateDraftTag.invalid) {
// Check if newDraftTag actually has content, to avoid staging an empty cleared draft.
// A valid draft implies it has content, but double-checking select or name is safer.
if (this.newDraftTag.select || (this.newDraftTag.name && this.newDraftTag.color)) {
this.stageCurrentTag();
}
}
// Phase 2: Process everything that is now in stagedForBatchAdd.
if (this.stagedForBatchAdd.length === 0) {
this.clearDraftTag(); // Ensure draft is clear even if nothing was committed
this.modal.hide();
return;
}
for (const sTag of this.stagedForBatchAdd) {
let isAnUndo = false; // Flag to track if this was an undo
// Check if it's an "undo delete"
if (sTag.systemTagId) { // Only existing system tags can be an undo delete
const undoDeleteIndex = this.deleteTags.findIndex(
dTag => dTag.tag_id === sTag.systemTagId && dTag.value === sTag.value
);
if (undoDeleteIndex > -1) {
this.deleteTags.splice(undoDeleteIndex, 1);
isAnUndo = true;
}
}
// Only add to newTags if it's not an "undo delete" operation.
// An "undo delete" means the tag is now considered active again from its previous state.
if (!isAnUndo) {
const tagObjectForNewTags = {
id: sTag.systemTagId, // This will be null for brand new system tags
color: sTag.color,
name: sTag.name,
value: sTag.value,
new: true, // As per plan, signals new to this monitor transaction
};
this.newTags.push(tagObjectForNewTags);
}
}
// newDraftTag should have been cleared if stageCurrentTag ran in Phase 1, or earlier.
// Call clearDraftTag again to be certain the form is reset before closing.
this.clearDraftTag();
this.modal.hide();
},
}, },
}; };
</script> </script>

View file

@ -1,7 +1,10 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="flashduty-integration-url" class="form-label">Integration Key</label> <label for="flashduty-integration-url" class="form-label">{{ $t("FlashDuty Push URL") }} <span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="flashduty-integration-url" v-model="$parent.notification.flashdutyIntegrationKey" autocomplete="false"></HiddenInput> <HiddenInput id="flashduty-integration-url" v-model="$parent.notification.flashdutyIntegrationKey" autocomplete="false" :placeholder="$t('FlashDuty Push URL Placeholder')" />
<div class="form-text">
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
</div>
<i18n-t tag="div" keypath="wayToGetFlashDutyKey" class="form-text"> <i18n-t tag="div" keypath="wayToGetFlashDutyKey" class="form-text">
<a href="https://flashcat.cloud/product/flashduty?from=kuma" target="_blank">{{ $t("here") }}</a> <a href="https://flashcat.cloud/product/flashduty?from=kuma" target="_blank">{{ $t("here") }}</a>
</i18n-t> </i18n-t>
@ -18,7 +21,6 @@
<script> <script>
import HiddenInput from "../HiddenInput.vue"; import HiddenInput from "../HiddenInput.vue";
export default { export default {
components: { components: {
HiddenInput, HiddenInput,

View file

@ -13,13 +13,20 @@
<div class="mb-3"> <div class="mb-3">
<label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label> <label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1"> <input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
<label for="ntfy-priority-down" class="form-label">{{ $t("ntfyPriorityDown") }}</label>
<input id="ntfy-priority-down" v-model="$parent.notification.ntfyPriorityDown" type="number" class="form-control" required min="1" max="5" step="1">
<div class="form-text"> <div class="form-text">
<p v-if="$parent.notification.ntfyPriority >= 5"> <p v-if="$parent.notification.ntfyPriority == $parent.notification.ntfyPriorityDown && $parent.notification.ntfyPriority >= 5">
{{ $t("ntfyPriorityHelptextAllEvents") }} {{ $t("ntfyPriorityHelptextAllEvents") }}
</p> </p>
<i18n-t v-else-if="$parent.notification.ntfyPriority > $parent.notification.ntfyPriorityDown" tag="p" keypath="ntfyPriorityHelptextPriorityHigherThanDown">
<code>DOWN</code>
<code>{{ $parent.notification.ntfyPriority }}</code>
<code>{{ $parent.notification.ntfyPriorityDown }}</code>
</i18n-t>
<i18n-t v-else tag="p" keypath="ntfyPriorityHelptextAllExceptDown"> <i18n-t v-else tag="p" keypath="ntfyPriorityHelptextAllExceptDown">
<code>DOWN</code> <code>DOWN</code>
<code>{{ $parent.notification.ntfyPriority + 1 }}</code> <code>{{ $parent.notification.ntfyPriorityDown }}</code>
</i18n-t> </i18n-t>
</div> </div>
</div> </div>
@ -69,6 +76,11 @@ export default {
this.$parent.notification.ntfyPriority = 5; this.$parent.notification.ntfyPriority = 5;
} }
// Setting down priority if it's undefined
if (typeof this.$parent.notification.ntfyPriorityDown === "undefined") {
this.$parent.notification.ntfyPriorityDown = 5;
}
// Handling notifications that added before 1.22.0 // Handling notifications that added before 1.22.0
if (typeof this.$parent.notification.ntfyAuthenticationMethod === "undefined") { if (typeof this.$parent.notification.ntfyAuthenticationMethod === "undefined") {
if (!this.$parent.notification.ntfyusername) { if (!this.$parent.notification.ntfyusername) {

View file

@ -13,7 +13,8 @@
<li v-for="(proxy, index) in $root.proxyList" :key="index" class="list-group-item"> <li v-for="(proxy, index) in $root.proxyList" :key="index" class="list-group-item">
{{ proxy.host }}:{{ proxy.port }} ({{ proxy.protocol }}) {{ proxy.host }}:{{ proxy.port }} ({{ proxy.protocol }})
<span v-if="proxy.default === true" class="badge bg-primary ms-2">{{ $t("Default") }}</span><br> <span v-if="proxy.default === true" class="badge bg-primary ms-2">{{ $t("Default") }}</span><br>
<a href="#" @click="$refs.proxyDialog.show(proxy.id)">{{ $t("Edit") }}</a> <a href="#" @click="$refs.proxyDialog.show(proxy.id)">{{ $t("Edit") }}</a> |
<a href="#" @click="$refs.proxyDialog.showClone(proxy.id)">{{ $t("Clone") }}</a>
</li> </li>
</ul> </ul>

View file

@ -75,11 +75,20 @@ export function currentLocale() {
if (locale in messages) { if (locale in messages) {
return locale; return locale;
} }
// some locales are further specified such as "en-US". // If the locale is a 2-letter code, we can try to find a regional variant
// If we only have a generic locale for this, we can use it too // e.g. "fr" may not be in the messages, but "fr-FR" is
const genericLocale = locale.split("-")[0]; if (locale.length === 2) {
if (genericLocale in messages) { const regionalLocale = `${locale}-${locale.toUpperCase()}`;
return genericLocale; if (regionalLocale in messages) {
return regionalLocale;
}
} else {
// Some locales are further specified such as "en-US".
// If we only have a generic locale for this, we can use it too
const genericLocale = locale.slice(0, 2);
if (genericLocale in messages) {
return genericLocale;
}
} }
} }
return "en"; return "en";

View file

@ -145,7 +145,7 @@
"Save": "يحفظ", "Save": "يحفظ",
"Notifications": "إشعارات", "Notifications": "إشعارات",
"Not available, please setup.": "غير متوفر من فضلك الإعداد.", "Not available, please setup.": "غير متوفر من فضلك الإعداد.",
"Setup Notification": "إشعار الإعداد", "Setup Notification": "إشعار جديد",
"Light": "نور", "Light": "نور",
"Dark": "داكن", "Dark": "داكن",
"Auto": "آلي", "Auto": "آلي",
@ -598,7 +598,7 @@
"Retry": "إعادة المحاولة", "Retry": "إعادة المحاولة",
"Topic": "عنوان", "Topic": "عنوان",
"WeCom Bot Key": "WECOM BOT KEY", "WeCom Bot Key": "WECOM BOT KEY",
"Setup Proxy": "وكيل الإعداد", "Setup Proxy": "إعداد الوكيل",
"Proxy Protocol": "بروتوكول الوكيل", "Proxy Protocol": "بروتوكول الوكيل",
"Proxy Server": "مخدم بروكسي", "Proxy Server": "مخدم بروكسي",
"Proxy server has authentication": "خادم الوكيل لديه مصادقة", "Proxy server has authentication": "خادم الوكيل لديه مصادقة",
@ -740,5 +740,26 @@
"leave blank for default body": "اترك فارغاً ليتم تعيين النص تلقائياً", "leave blank for default body": "اترك فارغاً ليتم تعيين النص تلقائياً",
"emailTemplateServiceName": "اسم الخدمة", "emailTemplateServiceName": "اسم الخدمة",
"emailTemplateHostnameOrURL": "اسم المضيف أو عنوان URL", "emailTemplateHostnameOrURL": "اسم المضيف أو عنوان URL",
"smspartnerPhoneNumber": "رقم الهاتف" "smspartnerPhoneNumber": "رقم الهاتف",
"endDateTime": "تاريخ/وقت الإنتهاء",
"chromeExecutableAutoDetect": "كشف تلقائي",
"Edit Maintenance": "تعديل الصيانة",
"smspartnerSenderName": "اسم مرسل الرسائل القصيرة",
"telegramServerUrl": "(اختياري) رابط الخادم",
"emailCustomisableContent": "محتوى قابل للتخصيص",
"emailTemplateMsg": "رسالة الإشعار",
"Select message type": "اختر نوع الرسالة",
"Your User ID": "معرف المستخدم الخاص بك",
"setup a new monitor group": "إنشاء مجموعة مراقبة جديدة",
"Expires": "تاريخ الانتهاء",
"templateStatus": "الحالة",
"telegramUseTemplate": "استخدم قالب رسالة مخصص",
"telegramUseTemplateDescription": "إذا تم التفعيل، سيتم إرسال الرسالة باستخدام قالب مخصص.",
"now": "الآن",
"-year": "-سنة",
"and": "و",
"Add a domain": "إضافة نطاق",
"Remove domain": "إزالة النطاق '{0}'",
"time ago": "منذ {0}",
"startDateTime": "تاريخ/وقت البدء"
} }

View file

@ -933,5 +933,43 @@
"threadForumPostID": "Трэд / ID паста", "threadForumPostID": "Трэд / ID паста",
"whatHappensAtForumPost": "Стварыць новы пост на форуме. Гэта НЕ размяшчае паведамленні ў існуючым пасце. Для публікацыі ў існуючай публікацыі выкарыстоўвайце \"{option}\"", "whatHappensAtForumPost": "Стварыць новы пост на форуме. Гэта НЕ размяшчае паведамленні ў існуючым пасце. Для публікацыі ў існуючай публікацыі выкарыстоўвайце \"{option}\"",
"now": "зараз", "now": "зараз",
"-year": "-год" "-year": "-год",
"telegramServerUrl": "(Неабавязкова) URL сервера",
"telegramServerUrlDescription": "Каб зняць абмежаванні API бота Telegram або атрымаць доступ у заблакіраваных рэгіёнах (Кітай, Іран і інш.), націсніце {0} для атрымання дадатковай інфармацыі. Значэнне па змаўчанні: {1}",
"smspartnerPhoneNumber": "Нумар(ы) тэлефона",
"smspartnerSenderName": "Імя адпраўніка SMS",
"Command": "Каманда",
"mongodbCommandDescription": "Выканаць каманду MongoDB для базы даных. Інфармацыю пра даступныя каманды можна знайсці ў {documentation}",
"Community String": "Радок супольнасці",
"snmpCommunityStringHelptext": "Гэты радок выконвае функцыю пароля для аўтэнтыфікацыі і кантролю доступу да прылад з падтрымкай SNMP. Павінен адпавядаць канфігурацыі вашай SNMP-прылады.",
"OID (Object Identifier)": "OID (Ідэнтыфікатар аб’екта)",
"snmpOIDHelptext": "Увядзіце OID для датчыка або статусу, які вы хочаце маніторыць. Выкарыстоўвайце інструменты кіравання сеткай, такія як аглядальнікі MIB або праграмы SNMP, калі вы не ўпэўнены ў патрэбным OID.",
"SNMP Version": "Версія SNMP",
"Please enter a valid OID.": "Калі ласка, увядзіце карэктны OID.",
"wayToGetThreemaGateway": "Вы можаце зарэгістравацца для Threema Gateway {0}.",
"threemaRecipient": "Атрымальнік",
"templateServiceName": "назва сэрвісу",
"templateHostnameOrURL": "імя хоста або URL",
"templateStatus": "статус",
"telegramUseTemplate": "Выкарыстоўваць уласны шаблон паведамлення",
"telegramUseTemplateDescription": "Калі ўключана, паведамленне будзе адпраўлена з выкарыстаннем уласнага шаблона.",
"telegramTemplateFormatDescription": "Telegram дазваляе выкарыстоўваць розныя мовы разметкі для паведамленняў, падрабязнасці глядзіце ў Telegram {0}.",
"cacheBusterParamDescription": "Выпадкова згенераваны параметр для абыходу кэша.",
"Send rich messages": "Адправіць пашыраныя паведамленні",
"Bitrix24 Webhook URL": "URL вэбхука Bitrix24",
"wayToGetBitrix24Webhook": "Вы можаце стварыць вэбхук, выканаўшы крокі, апісаныя тут: {0}",
"bitrix24SupportUserID": "Увядзіце свой ідэнтыфікатар карыстальніка ў Bitrix24. Вы можаце даведацца ідэнтыфікатар па спасылцы, перайшоўшы ў профіль карыстальніка.",
"Condition": "Умова",
"aboutSlackUsername": "Змяняе адлюстраванае імя адпраўніка паведамлення. Калі вы хочаце згадваць кагосьці, уключыце гэта ў сяброўскае імя замест гэтага.",
"time ago": "{0} таму",
"Json Query Expression": "Выраз запыту JSON",
"and": "i",
"smspartnerApiurl": "Вы можаце знайсці свой ключ API на панэлі кіравання па адрасе {0}",
"smspartnerSenderNameInfo": "Павінна быць ад 3 да 11 звычайных сімвалаў",
"Message format": "Фармат паведамлення",
"ignoredTLSError": "Памылкі TLS/SSL былі праігнараваныя",
"shrinkDatabaseDescriptionSqlite": "Запусціць аперацыю {vacuum} для SQLite. {auto_vacuum} ужо ўключаны, але ён не дэфрагментуе базу даных і не перапакоўвае асобныя старонкі базы даных так, як гэта робіць каманда {vacuum}.",
"wayToGetDiscordThreadId": "Атрыманне ідэнтыфікатара тэмы або паведамлення на форуме аналагічнае атрыманню ідэнтыфікатара канала. Падрабязней пра тое, як атрымаць ідэнтыфікатары, чытайце тут {0}",
"smspartnerPhoneNumberHelptext": "Нумар павінен быць у міжнародным фармаце {0}, {1}. Некалькі нумароў павінны быць падзелены {2}",
"cacheBusterParam": "Дадайце параметр {0}"
} }

View file

@ -1118,5 +1118,11 @@
"wayToGetWahaSession": "От тази сесия WAHA изпраща известия до чат ID. Можете да го намерите в таблото за управление на WAHA.", "wayToGetWahaSession": "От тази сесия WAHA изпраща известия до чат ID. Можете да го намерите в таблото за управление на WAHA.",
"telegramServerUrlDescription": "За премахване на API бот ограниченията за Telegram или за получаване на достъп в блокирани зони (Китай, Иран и др.). За повече информация щракнете върху {0}. По подразбиране: {1}", "telegramServerUrlDescription": "За премахване на API бот ограниченията за Telegram или за получаване на достъп в блокирани зони (Китай, Иран и др.). За повече информация щракнете върху {0}. По подразбиране: {1}",
"telegramServerUrl": "(По избор) URL адрес на сървъра", "telegramServerUrl": "(По избор) URL адрес на сървъра",
"Font Twemoji by Twitter licensed under": "Шрифт Twemoji от Twitter, лицензиран под" "Font Twemoji by Twitter licensed under": "Шрифт Twemoji от Twitter, лицензиран под",
"the smsplanet documentation": "документацията на smsplanet",
"Phone numbers": "Телефонни номера",
"Sender name": "Име на подател",
"smsplanetNeedToApproveName": "Трябва да бъде одобрен в клиентския панел",
"smsplanetApiToken": "Токен код за SMSPlanet API",
"smsplanetApiDocs": "Подробна информация, за получаване на API токен кодове, можете да намерите в {the_smsplanet_documentation}."
} }

View file

@ -1115,5 +1115,11 @@
"wayToWriteWahaChatId": "Die Telefonnummer mit internationaler Vorwahl, ohne den anfänglichen Pluszeichen ({0}), die Kontakt-ID ({1}) oder die Gruppen-ID ({2}). Die Benachrichtigungen werden an diese Chat-ID von der WAHA-Sitzung gesendet.", "wayToWriteWahaChatId": "Die Telefonnummer mit internationaler Vorwahl, ohne den anfänglichen Pluszeichen ({0}), die Kontakt-ID ({1}) oder die Gruppen-ID ({2}). Die Benachrichtigungen werden an diese Chat-ID von der WAHA-Sitzung gesendet.",
"telegramServerUrl": "(Optional) Server URL", "telegramServerUrl": "(Optional) Server URL",
"telegramServerUrlDescription": "Um die Telegram-Bot-API-Beschränkungen aufzuheben oder in gesperrten Gebieten (China, Iran usw.) Zugriff zu erhalten. Weitere Informationen findest du unter {0}. Standard: {1}", "telegramServerUrlDescription": "Um die Telegram-Bot-API-Beschränkungen aufzuheben oder in gesperrten Gebieten (China, Iran usw.) Zugriff zu erhalten. Weitere Informationen findest du unter {0}. Standard: {1}",
"Font Twemoji by Twitter licensed under": "Schriftart Twemoji von Twitter lizenziert unter" "Font Twemoji by Twitter licensed under": "Schriftart Twemoji von Twitter lizenziert unter",
"smsplanetApiToken": "Token für die SMSPlanet API",
"smsplanetApiDocs": "Ausführliche Informationen zum Erhalt von API-Tokens findest du in {the_smsplanet_documentation}.",
"the smsplanet documentation": "die smsplanet Dokumentation",
"Phone numbers": "Telefonnummern",
"Sender name": "Absendername",
"smsplanetNeedToApproveName": "Muss im Kundenpanel genehmigt werden"
} }

View file

@ -944,7 +944,7 @@
"whapiRecipient": "Telefonnummer / Kontakt-ID / Gruppen-ID", "whapiRecipient": "Telefonnummer / Kontakt-ID / Gruppen-ID",
"API URL": "API URL", "API URL": "API URL",
"wayToWriteWhapiRecipient": "Die Rufnummer mit der internationalen Vorwahl, aber ohne das Pluszeichen am Anfang ({0}), die Kontakt-ID ({1}) oder die Gruppen-ID ({2}).", "wayToWriteWhapiRecipient": "Die Rufnummer mit der internationalen Vorwahl, aber ohne das Pluszeichen am Anfang ({0}), die Kontakt-ID ({1}) oder die Gruppen-ID ({2}).",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Gib entweder den Hostnamen des Servers ein, mit dem eine Verbindung hergestellt werden soll, oder {localhost}, wenn ein {local_mta} verwendet werden soll.", "Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Gib entweder den Hostnamen des Servers ein, mit dem eine Verbindung hergestellt werden soll, oder {localhost}, wenn ein {local_mta} verwendet werden soll",
"locally configured mail transfer agent": "Lokal konfigurierter Mail-Transfer-Agent", "locally configured mail transfer agent": "Lokal konfigurierter Mail-Transfer-Agent",
"Mentioning": "Erwähnung", "Mentioning": "Erwähnung",
"Don't mention people": "Keine Personen erwähnen", "Don't mention people": "Keine Personen erwähnen",
@ -1118,5 +1118,11 @@
"wayToWriteWahaChatId": "Die Telefonnummer mit internationaler Vorwahl, ohne den anfänglichen Pluszeichen ({0}), die Kontakt-ID ({1}) oder die Gruppen-ID ({2}). Die Benachrichtigungen werden an diese Chat-ID von der WAHA-Sitzung gesendet.", "wayToWriteWahaChatId": "Die Telefonnummer mit internationaler Vorwahl, ohne den anfänglichen Pluszeichen ({0}), die Kontakt-ID ({1}) oder die Gruppen-ID ({2}). Die Benachrichtigungen werden an diese Chat-ID von der WAHA-Sitzung gesendet.",
"telegramServerUrlDescription": "Um die Telegram-Bot-API-Beschränkungen aufzuheben oder in gesperrten Gebieten (China, Iran usw.) Zugriff zu erhalten. Weitere Informationen findest du unter {0}. Standard: {1}", "telegramServerUrlDescription": "Um die Telegram-Bot-API-Beschränkungen aufzuheben oder in gesperrten Gebieten (China, Iran usw.) Zugriff zu erhalten. Weitere Informationen findest du unter {0}. Standard: {1}",
"telegramServerUrl": "(Optional) Server URL", "telegramServerUrl": "(Optional) Server URL",
"Font Twemoji by Twitter licensed under": "Schriftart Twemoji von Twitter lizenziert unter" "Font Twemoji by Twitter licensed under": "Schriftart Twemoji von Twitter lizenziert unter",
"the smsplanet documentation": "die smsplanet Dokumentation",
"Phone numbers": "Telefonnummern",
"Sender name": "Absendername",
"smsplanetNeedToApproveName": "Muss im Kundenpanel genehmigt werden",
"smsplanetApiToken": "Token für die SMSPlanet API",
"smsplanetApiDocs": "Ausführliche Informationen zum Erhalt von API-Tokens findest du in {the_smsplanet_documentation}."
} }

View file

@ -188,9 +188,13 @@
"Show URI": "Show URI", "Show URI": "Show URI",
"Tags": "Tags", "Tags": "Tags",
"Add New Tag": "Add New Tag", "Add New Tag": "Add New Tag",
"Add Tags": "Add Tags",
"Add New below or Select...": "Add New below or Select…", "Add New below or Select...": "Add New below or Select…",
"Tag with this name already exist.": "Tag with this name already exists.", "Tag with this name already exist.": "Tag with this name already exists.",
"Tag with this value already exist.": "Tag with this value already exists.", "Tag with this value already exist.": "Tag with this value already exists.",
"tagAlreadyOnMonitor": "This tag (name and value) is already on the monitor or pending addition.",
"tagAlreadyStaged": "This tag (name and value) is already staged for this batch.",
"tagNameExists": "A system tag with this name already exists. Select it from the list or use a different name.",
"color": "Color", "color": "Color",
"value (optional)": "value (optional)", "value (optional)": "value (optional)",
"Gray": "Gray", "Gray": "Gray",
@ -841,6 +845,8 @@
"ntfyAuthenticationMethod": "Authentication Method", "ntfyAuthenticationMethod": "Authentication Method",
"ntfyPriorityHelptextAllEvents": "All events are sent with the maximum priority", "ntfyPriorityHelptextAllEvents": "All events are sent with the maximum priority",
"ntfyPriorityHelptextAllExceptDown": "All events are sent with this priority, except {0}-events, which have a priority of {1}", "ntfyPriorityHelptextAllExceptDown": "All events are sent with this priority, except {0}-events, which have a priority of {1}",
"ntfyPriorityHelptextPriorityHigherThanDown": "Regular priority should be higher than {0} priority. Priority {1} is higher than {0} priority {2}",
"ntfyPriorityDown": "Priority for DOWN-events",
"ntfyUsernameAndPassword": "Username and Password", "ntfyUsernameAndPassword": "Username and Password",
"twilioAccountSID": "Account SID", "twilioAccountSID": "Account SID",
"twilioApiKey": "Api Key (optional)", "twilioApiKey": "Api Key (optional)",
@ -895,8 +901,10 @@
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.", "noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
"Close": "Close", "Close": "Close",
"Request Body": "Request Body", "Request Body": "Request Body",
"wayToGetFlashDutyKey": "You can go to Channel -> (Select a Channel) -> Integrations -> Add a new integration' page, add a 'Uptime Kuma' to get a push address, copy the Integration Key in the address. For more information, please visit", "wayToGetFlashDutyKey": "To integrate Uptime Kuma with Flashduty: Go to Channels > Select a channel > Integrations > Add a new integration, choose Uptime Kuma, and copy the Push URL.",
"FlashDuty Severity": "Severity", "FlashDuty Severity": "Severity",
"FlashDuty Push URL": "Push URL",
"FlashDuty Push URL Placeholder": "Copy from the alerting integration page",
"nostrRelays": "Nostr relays", "nostrRelays": "Nostr relays",
"nostrRelaysHelp": "One relay URL per line", "nostrRelaysHelp": "One relay URL per line",
"nostrSender": "Sender Private Key (nsec)", "nostrSender": "Sender Private Key (nsec)",
@ -1074,6 +1082,16 @@
"rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.", "rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.",
"SendGrid API Key": "SendGrid API Key", "SendGrid API Key": "SendGrid API Key",
"Separate multiple email addresses with commas": "Separate multiple email addresses with commas", "Separate multiple email addresses with commas": "Separate multiple email addresses with commas",
"pingCountLabel": "Max Packets",
"pingCountDescription": "Number of packets to send before stopping",
"pingNumericLabel": "Numeric Output",
"pingNumericDescription": "If checked, IP addresses will be output instead of symbolic hostnames",
"pingGlobalTimeoutLabel": "Global Timeout",
"pingGlobalTimeoutDescription": "Total time in seconds before ping stops, regardless of packets sent",
"pingPerRequestTimeoutLabel": "Per-Ping Timeout",
"pingPerRequestTimeoutDescription": "This is the maximum waiting time (in seconds) before considering a single ping packet lost",
"pingIntervalAdjustedInfo": "Interval adjusted based on packet count, global timeout and per-ping timeout",
"smtpHelpText": "'SMTPS' tests that SMTP/TLS is working; 'Ignore TLS' connects over plaintext; 'STARTTLS' connects, issues a STARTTLS command and verifies the server certificate. None of these send an email.",
"Custom URL": "Custom URL", "Custom URL": "Custom URL",
"customUrlDescription": "Will be used as the clickable URL instead of the monitor's one.", "customUrlDescription": "Will be used as the clickable URL instead of the monitor's one.",
"OneChatAccessToken": "OneChat Access Token", "OneChatAccessToken": "OneChat Access Token",
@ -1097,5 +1115,13 @@
"Phone numbers": "Phone numbers", "Phone numbers": "Phone numbers",
"Sender name": "Sender name", "Sender name": "Sender name",
"smsplanetNeedToApproveName": "Needs to be approved in the client panel", "smsplanetNeedToApproveName": "Needs to be approved in the client panel",
"Disable URL in Notification": "Disable URL in Notification" "Disable URL in Notification": "Disable URL in Notification",
"Ip Family": "IP Family",
"ipFamilyDescriptionAutoSelect": "Uses the {happyEyeballs} for determining the IP family.",
"Happy Eyeballs algorithm": "Happy Eyeballs algorithm",
"Add Another Tag": "Add Another Tag",
"Staged Tags for Batch Add": "Staged Tags for Batch Add",
"Clear Form": "Clear Form",
"pause": "Pause",
"Manual": "Manual"
} }

View file

@ -1104,7 +1104,7 @@
"wahaChatId": "Viesti ID (Puhelinnumero / Yhteystieto ID / Ryhmä ID)", "wahaChatId": "Viesti ID (Puhelinnumero / Yhteystieto ID / Ryhmä ID)",
"Template Format": "Malli Muotoilu", "Template Format": "Malli Muotoilu",
"wayToGetWahaApiUrl": "Sinun WAHA instanssin URL.", "wayToGetWahaApiUrl": "Sinun WAHA instanssin URL.",
"YZJ Webhook URL": "YZJ Webhook URL", "YZJ Webhook URL": "YZJ Webhook URL-osoite",
"telegramServerUrl": "(Valinnainen) Palvelin Url", "telegramServerUrl": "(Valinnainen) Palvelin Url",
"telegramServerUrlDescription": "Telegramin bot-api-rajoitusten poistamiseksi tai pääsyn saamiseksi estetyille alueille (Kiina, Iran jne.). Saat lisätietoja napsauttamalla {0}. Oletus: {1}", "telegramServerUrlDescription": "Telegramin bot-api-rajoitusten poistamiseksi tai pääsyn saamiseksi estetyille alueille (Kiina, Iran jne.). Saat lisätietoja napsauttamalla {0}. Oletus: {1}",
"Message Template": "Viesti Malli", "Message Template": "Viesti Malli",
@ -1116,5 +1116,11 @@
"templateStatus": "tila", "templateStatus": "tila",
"telegramUseTemplate": "Käytä mukautettua viesti mallia", "telegramUseTemplate": "Käytä mukautettua viesti mallia",
"telegramUseTemplateDescription": "Jos aktivoitu, viesti lähetetään käyttämällä mukautettua mallia.", "telegramUseTemplateDescription": "Jos aktivoitu, viesti lähetetään käyttämällä mukautettua mallia.",
"telegramTemplateFormatDescription": "Telegram sallii erilaisten merkintäkielien käytön viesteissä, katso Telegram {0} tarkempia tietoja." "telegramTemplateFormatDescription": "Telegram sallii erilaisten merkintäkielien käytön viesteissä, katso Telegram {0} tarkempia tietoja.",
"smsplanetApiToken": "SMSPlanet API:n tunnus",
"smsplanetApiDocs": "Yksityiskohtaiset tiedot API-tunnusten hankkimisesta löytyvät osoitteesta {the_smsplanet_documentation}.",
"the smsplanet documentation": "smsplanetin dokumentaatio",
"Phone numbers": "Puhelinnumerot",
"Sender name": "Lähettäjän nimi",
"smsplanetNeedToApproveName": "On hyväksyttävä asiakaspaneelissa"
} }

View file

@ -1118,5 +1118,11 @@
"wayToWriteWahaChatId": "Le numéro de téléphone avec le préfixe international, mais sans le signe plus ({0}), l'identifiant de contact ({1}) ni l'identifiant de groupe ({2}). Les notifications sont envoyées à cet identifiant de chat depuis la session WAHA.", "wayToWriteWahaChatId": "Le numéro de téléphone avec le préfixe international, mais sans le signe plus ({0}), l'identifiant de contact ({1}) ni l'identifiant de groupe ({2}). Les notifications sont envoyées à cet identifiant de chat depuis la session WAHA.",
"telegramServerUrlDescription": "Pour lever les limitations de lAPI des bots Telegram ou accéder aux zones bloquées (Chine, Iran, etc.). Pour plus dinformations, cliquez sur {0}. Par défaut : {1}", "telegramServerUrlDescription": "Pour lever les limitations de lAPI des bots Telegram ou accéder aux zones bloquées (Chine, Iran, etc.). Pour plus dinformations, cliquez sur {0}. Par défaut : {1}",
"telegramServerUrl": "(Facultatif) URL du serveur", "telegramServerUrl": "(Facultatif) URL du serveur",
"Font Twemoji by Twitter licensed under": "La police Twemoji de Twitter est sous licence" "Font Twemoji by Twitter licensed under": "La police Twemoji de Twitter est sous licence",
"the smsplanet documentation": "la documentation de smsplanet",
"Phone numbers": "Numéros de téléphone",
"Sender name": "Nom de l'expéditeur",
"smsplanetNeedToApproveName": "Doit être approuvé dans le panneau client",
"smsplanetApiToken": "Jeton pour l'API SMSPlanet",
"smsplanetApiDocs": "Des informations détaillées sur l'obtention de jetons API peuvent être trouvées dans {the_smsplanet_documentation}."
} }

View file

@ -1111,5 +1111,12 @@
"wayToGetWahaSession": "Iz ove sjednice WAHA šalje obavijesti na identifikator razgovora. Može se pronaći na WAHA nadzornoj ploči.", "wayToGetWahaSession": "Iz ove sjednice WAHA šalje obavijesti na identifikator razgovora. Može se pronaći na WAHA nadzornoj ploči.",
"wayToWriteWahaChatId": "Telefonski broj s međunarodnim prefiksom, ali bez znaka plus na početku ({0}), identifikator kontakta ({1}) ili identifikator grupe ({2}). Obavijesti se šalju na ovaj identifikator chata iz WAHA sesije.", "wayToWriteWahaChatId": "Telefonski broj s međunarodnim prefiksom, ali bez znaka plus na početku ({0}), identifikator kontakta ({1}) ili identifikator grupe ({2}). Obavijesti se šalju na ovaj identifikator chata iz WAHA sesije.",
"telegramServerUrl": "(Neobvezno) URL Poslužitelja", "telegramServerUrl": "(Neobvezno) URL Poslužitelja",
"telegramServerUrlDescription": "Za ukidanje ograničenja API-ja za botove Telegrama ili dobivanje pristupa u blokiranim područjima (Kina, Iran, itd.). Za više informacija kliknite {0}. Zadano: {1}" "telegramServerUrlDescription": "Za ukidanje ograničenja API-ja za botove Telegrama ili dobivanje pristupa u blokiranim područjima (Kina, Iran, itd.). Za više informacija kliknite {0}. Zadano: {1}",
"Font Twemoji by Twitter licensed under": "Font Twemoji tvrtke Twitter je pod licencom",
"the smsplanet documentation": "dokumentaciji usluge SMSPLANET",
"Phone numbers": "Brojevi telefona",
"Sender name": "Naziv pošiljatelja",
"smsplanetNeedToApproveName": "Potrebno je odobrenje u klijentskoj nadzornoj ploči",
"smsplanetApiToken": "Token za pristup SMSPLANET API-ju",
"smsplanetApiDocs": "Detaljne informacije o dobivanju tokena za API možete pronaći u {the_smsplanet_documentation}."
} }

View file

@ -308,7 +308,7 @@
"clearDataOlderThan": "Mantieni lo storico per {0} giorni.", "clearDataOlderThan": "Mantieni lo storico per {0} giorni.",
"PasswordsDoNotMatch": "Le password non corrispondono.", "PasswordsDoNotMatch": "Le password non corrispondono.",
"records": "records", "records": "records",
"One record": "One record", "One record": "Un record",
"steamApiKeyDescription": "Per monitorare un server di gioco Steam è necessaria una Web-API Key di Steam. È possibile registrarne una qui: ", "steamApiKeyDescription": "Per monitorare un server di gioco Steam è necessaria una Web-API Key di Steam. È possibile registrarne una qui: ",
"Current User": "Utente corrente", "Current User": "Utente corrente",
"recent": "Recenti", "recent": "Recenti",
@ -742,5 +742,42 @@
"templateHostnameOrURL": "nome host o URL", "templateHostnameOrURL": "nome host o URL",
"templateStatus": "stato", "templateStatus": "stato",
"templateServiceName": "nome del servizio", "templateServiceName": "nome del servizio",
"locally configured mail transfer agent": "agente mail configurato localmente" "locally configured mail transfer agent": "agente mail configurato localmente",
"shrinkDatabaseDescriptionSqlite": "Un record",
"pushoversounds cashregister": "Registratore di cassa",
"Strategy": "Strategia",
"Add a domain": "Aggiungi un dominio",
"telegramServerUrl": "(Facoltativo) URL del Server",
"pushoversounds magic": "Magico",
"pushoversounds mechanical": "Meccanico",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Sirena",
"pushoversounds spacealarm": "Allarme spaziale",
"pushoversounds alien": "Allarme Alieno (lungo)",
"Remove domain": "Rimuovi il dominio '{0}'",
"Edit Tag": "Modifica il Tag",
"Server Address": "Indirizzo del Server",
"Expiry": "Scadenza",
"telegramUseTemplateDescription": "Se abilitato, il messaggio sarà spedito usando il template personalizzato.",
"high": "alto",
"jsonQueryDescription": "Analizza ed estrai dati specifici dalla risposta JSON del server utilizzando una query JSON oppure usa \"$\" per la risposta grezza, se non ti aspetti JSON. Il risultato viene quindi confrontato con il valore previsto, sotto forma di stringhe. Consulta {0} per la documentazione e usa {1} per sperimentare con le query.",
"Free Mobile User Identifier": "Identificatore utente mobile gratuito",
"telegramServerUrlDescription": "Per rimuovere le limitazioni dell'API bot di Telegram o ottenere l'accesso in aree bloccate (Cina, Iran, ecc.), clicca su {0} per maggiori informazioni. Predefinito: {1}",
"octopushLogin": "“Accedi” dalle credenziali API HTTP nel pannello di controllo",
"promosmsLogin": "Nome di accesso API",
"pushoversounds bike": "Bicicletta",
"pushoversounds bugle": "Bugle",
"pushoversounds classical": "Classico",
"pushoversounds cosmic": "Cosmico",
"pushoversounds incoming": "In arrivo",
"pushoversounds intermission": "Intervallo",
"pushoversounds tugboat": "Rimorchiatore",
"pushoversounds climb": "Salita (lunga)",
"pushoversounds persistent": "Persistente (lungo)",
"pushoversounds vibrate": "Solo vibrazione",
"wayToGetKookGuildID": "Attivare la “Modalità sviluppatore” nelle impostazioni di Kook e fare clic con il pulsante destro del mouse sulla gilda per ottenere il suo ID.",
"Guild ID": "Guild ID",
"Free Mobile API Key": "Chiave API mobile gratuita",
"telegramUseTemplate": "Utilizza un template di messaggio personalizzato",
"telegramTemplateFormatDescription": "Telegram permette l'utilizzo di diversi linguaggi di markup, vedi Telegram {0} per maggiori dettagli."
} }

View file

@ -29,7 +29,7 @@
"Add New Monitor": "監視の追加", "Add New Monitor": "監視の追加",
"Quick Stats": "統計", "Quick Stats": "統計",
"Up": "正常", "Up": "正常",
"Down": "停止", "Down": "異常",
"Pending": "待機中", "Pending": "待機中",
"Unknown": "不明", "Unknown": "不明",
"Pause": "一時停止", "Pause": "一時停止",
@ -1085,5 +1085,11 @@
"telegramUseTemplate": "カスタムメッセージテンプレートを使用", "telegramUseTemplate": "カスタムメッセージテンプレートを使用",
"telegramUseTemplateDescription": "有効にすると、メッセージはカスタムテンプレートを使って送信されます。", "telegramUseTemplateDescription": "有効にすると、メッセージはカスタムテンプレートを使って送信されます。",
"telegramTemplateFormatDescription": "Telegramではメッセージに異なるマークアップ言語を使用することができます。詳細はTelegram {0} を参照してください。", "telegramTemplateFormatDescription": "Telegramではメッセージに異なるマークアップ言語を使用することができます。詳細はTelegram {0} を参照してください。",
"Font Twemoji by Twitter licensed under": "TwemojiフォントはTwitterライセンス下でライセンスされています" "Font Twemoji by Twitter licensed under": "TwemojiフォントはTwitterライセンス下でライセンスされています",
"the smsplanet documentation": "smsplanetドキュメント",
"Phone numbers": "携帯電話番号",
"Sender name": "送信者名",
"smsplanetNeedToApproveName": "クライアントパネルでの承認が必要",
"smsplanetApiToken": "SMSPlanet APIのトークン",
"smsplanetApiDocs": "APIトークンの取得に関する詳細な情報は、{the_smsplanet_documentation}にあります。"
} }

View file

@ -1,26 +1,26 @@
{ {
"languageName": "한국어", "languageName": "한국어",
"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": "응답 성공으로 간주할 상태 코드를 정해요.",
"passwordNotMatchMsg": "비밀번호 재입력이 일치하지 않아요.", "passwordNotMatchMsg": "비밀번호 재입력이 일치하지 않아요.",
"notificationDescription": "모니터링에 알림을 설정할 수 있어요.", "notificationDescription": "알림이 동작하려면 기존 모니터에 할당되어야 합니다.",
"keywordDescription": "HTML 이나 JSON에서 대소문자를 구분해 키워드를 검색해요.", "keywordDescription": "HTML 이나 JSON에서 대소문자를 구분해 키워드를 검색해요.",
"pauseDashboardHome": "일시 정지", "pauseDashboardHome": "정지",
"deleteMonitorMsg": "정말 이 모니터링을 삭제할까요?", "deleteMonitorMsg": "이 모니터를 삭제하시겠습니까?",
"deleteNotificationMsg": "정말 이 알림을 모든 모니터링에서 삭제할까요?", "deleteNotificationMsg": "이 알림을 모든 모니터에서 삭제하시겠습니까?",
"resolverserverDescription": "Cloudflare가 기본 서버예요, 원한다면 언제나 다른 Resolver 서버로 변경할 수 있어요.", "resolverserverDescription": "Cloudflare가 기본 서버예요, 원한다면 언제나 다른 Resolver 서버로 변경할 수 있어요.",
"rrtypeDescription": "모니터링할 RR-Type을 선택해요", "rrtypeDescription": "모니터링할 RR Type을 선택하세요.",
"pauseMonitorMsg": "정말 이 모니터링을 일시 정지할까요?", "pauseMonitorMsg": "이 모니터를 일시 정지하시겠습니까?",
"enableDefaultNotificationDescription": "새로 추가하는 모든 모니터링에 이 알림을 기본적으로 활성화해요. 각 모니터에 대해 별도로 알림을 비활성화할 수 있어요.", "enableDefaultNotificationDescription": "새 모니터에 이 알림을 기본적으로 활성화합니다. 개별 모니터에 대해 알림을 비활성화할 수 있습니다.",
"clearEventsMsg": "정말 이 모니터링에 대한 모든 이벤트를 삭제할까요?", "clearEventsMsg": "이 모니터의 모든 이벤트를 삭제하시겠습니까?",
"clearHeartbeatsMsg": "정말 이 모니터링에 대한 모든 하트비트를 삭제할까요?", "clearHeartbeatsMsg": "이 모니터의 모든 하트비트를 삭제하시겠습니까?",
"confirmClearStatisticsMsg": "정말 모든 통계를 삭제할까요?", "confirmClearStatisticsMsg": "정말 모든 통계를 삭제할까요?",
"importHandleDescription": "이름이 같은 모든 모니터링이나 알림을 건너뛰려면 '기존값 건너뛰기'를 선택해주세요. '덮어쓰기'는 기존의 모든 모니터링과 알림을 삭제해요.", "importHandleDescription": "이름이 같은 모니터나 알림을 건너뛰려면 '기존 항목 건너뛰기'를 선택하세요. '덮어쓰기'를 선택한 경우 존재하는 기존 모니터와 알림을 모두 삭제합니다.",
"confirmImportMsg": "정말 백업을 가져올까요? 가져오기 옵션을 제대로 설정했는지 다시 확인해주세요.", "confirmImportMsg": "정말 백업을 가져올까요? 가져오기 옵션을 제대로 설정했는지 다시 확인해주세요.",
"twoFAVerifyLabel": "토큰을 입력해 2단계 인증이 작동하는지 확인해주세요", "twoFAVerifyLabel": "토큰을 입력해 2단계 인증이 작동하는지 확인해주세요",
"tokenValidSettingsMsg": "토큰이 유효해요! 이제 2단계 인증 설정을 저장할 수 있어요.", "tokenValidSettingsMsg": "토큰이 유효해요! 이제 2단계 인증 설정을 저장할 수 있어요.",
@ -28,17 +28,17 @@
"confirmDisableTwoFAMsg": "정말 2단계 인증을 비활성화할까요?", "confirmDisableTwoFAMsg": "정말 2단계 인증을 비활성화할까요?",
"Settings": "설정", "Settings": "설정",
"Dashboard": "대시보드", "Dashboard": "대시보드",
"New Update": "새로운 업데이트", "New Update": "새 업데이트",
"Language": "언어", "Language": "언어",
"Appearance": "디스플레이", "Appearance": "모양",
"Theme": "테마", "Theme": "테마",
"General": "일반", "General": "일반",
"Version": "버전", "Version": "버전",
"Check Update On GitHub": "깃허브에서 업데이트 확인", "Check Update On GitHub": "Github에서 업데이트 확인",
"List": "목록", "List": "목록",
"Add": "추가", "Add": "추가",
"Add New Monitor": "새로운 모니터 추가하기", "Add New Monitor": "새 모니터 추가",
"Quick Stats": "간단한 정보", "Quick Stats": "요약",
"Up": "온라인", "Up": "온라인",
"Down": "오프라인", "Down": "오프라인",
"Pending": "대기 중", "Pending": "대기 중",
@ -50,22 +50,22 @@
"Message": "메시지", "Message": "메시지",
"No important events": "중요 이벤트 없음", "No important events": "중요 이벤트 없음",
"Resume": "재개", "Resume": "재개",
"Edit": "수정", "Edit": "편집",
"Delete": "삭제", "Delete": "삭제",
"Current": "현재", "Current": "현재",
"Uptime": "업타임", "Uptime": "업타임",
"Cert Exp.": "인증서 만료.", "Cert Exp.": "인증서 만료",
"day": "일", "day": "일",
"-day": "-일", "-day": "일",
"hour": "시간", "hour": "시간",
"-hour": "-시간", "-hour": "시간",
"Response": "응답", "Response": "응답",
"Ping": "핑", "Ping": "핑",
"Monitor Type": "모니터링 종류", "Monitor Type": "모니터 타입",
"Keyword": "키워드", "Keyword": "키워드",
"Friendly Name": "이름", "Friendly Name": "별명",
"URL": "URL", "URL": "URL",
"Hostname": "호스트네임", "Hostname": "호스트",
"Port": "포트", "Port": "포트",
"Heartbeat Interval": "하트비트 주기", "Heartbeat Interval": "하트비트 주기",
"Retries": "재시도", "Retries": "재시도",
@ -73,46 +73,46 @@
"Advanced": "고급", "Advanced": "고급",
"Upside Down Mode": "상태 반전 모드", "Upside Down Mode": "상태 반전 모드",
"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": "이트",
"Dark": "다크", "Dark": "다크",
"Auto": "자동", "Auto": "자동",
"Theme - Heartbeat Bar": "테마 - 하트비트 바", "Theme - Heartbeat Bar": "테마 - 하트비트 바",
"Normal": "기본값", "Normal": "기본값",
"Bottom": "가운데", "Bottom": "하단",
"None": "없음", "None": "없음",
"Timezone": "시간대", "Timezone": "시간대",
"Search Engine Visibility": "검색 엔진 활성화", "Search Engine Visibility": "검색 엔진 노출",
"Allow indexing": "인덱싱 허용", "Allow indexing": "인덱싱 허용",
"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": "인증 비활성화",
"Enable Auth": "인증 활성화", "Enable Auth": "인증 활성화",
"disableauth.message1": "정말로 {disableAuth}?", "disableauth.message1": "{disableAuth}하시겠습니까?",
"disable authentication": "인증 기능을 끌까요", "disable authentication": "인증을 비활성화",
"disableauth.message2": "이 기능은 {intendThirdPartyAuth}을 Uptime Kuma 앞에 둔 사용자를 위한 기능이에요.", "disableauth.message2": "이 기능은 Uptime Kuma 앞단에 Cloudflare Access, Authelia 등의 {intendThirdPartyAuth}을 위해 설계되었습니다.",
"where you intend to implement third-party authentication": "Cloudflare Access와 같은 서드파티 인증", "where you intend to implement third-party authentication": "서드 파티 인증을 구현하는 상황",
"Please use this option carefully!": "신중하게 사용하세요!", "Please use this option carefully!": "신중하게 사용하세요!",
"Logout": "로그아웃", "Logout": "로그아웃",
"Leave": "나가기", "Leave": "취소",
"I understand, please disable": "기능에 대해 이해했으니 꺼주세요.", "I understand, please disable": "이해했습니다. 비활성화합니다.",
"Confirm": "확인", "Confirm": "확인",
"Yes": "확인", "Yes": "",
"No": "취소", "No": "아니요",
"Username": "이름", "Username": "사용자명",
"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": "테스트",
@ -120,33 +120,33 @@
"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": "비밀번호 확인",
"Import Backup": "백업 가져오기", "Import Backup": "백업 가져오기",
"Export Backup": "백업 내보내기", "Export Backup": "백업 내보내기",
"Export": "내보내기", "Export": "내보내기",
"Import": "가져오기", "Import": "가져오기",
"respTime": "응답 시간 (ms)", "respTime": "응답 시간 (ms)",
"notAvailableShort": "N/A", "notAvailableShort": "N/A",
"Default enabled": "기본 알림으로 설정", "Default enabled": "기본적으로 활성화",
"Apply on all existing monitors": "기존 모니터에 모두 적용하기", "Apply on all existing monitors": "기존 모니터에 모두 적용",
"Create": "생성하기", "Create": "생성",
"Clear Data": "데이터 삭제", "Clear Data": "데이터 삭제",
"Events": "이벤트", "Events": "이벤트",
"Heartbeats": "하트비트", "Heartbeats": "하트비트",
"Auto Get": "자동 Get", "Auto Get": "Auto Get",
"backupDescription": "모든 모니터링과 알림을 JSON 파일 형식에 저장할 수 있어요.", "backupDescription": "모든 모니터와 알림을 JSON 파일에 백업할 수 있습니다.",
"backupDescription2": "히스토리와 이벤트 데이터는 포함되어 있지 않아요.", "backupDescription2": "히스토리와 이벤트 데이터는 포함되어 있지 않아요.",
"backupDescription3": "알림 토큰과 같은 보안 데이터가 내보내기 파일에 포함되어 있으므로 관리에 주의해주세요.", "backupDescription3": "알림 토큰과 같은 보안 데이터가 내보내기 파일에 포함되어 있으므로 관리에 주의해주세요.",
"alertNoFile": "가져오기를 하기 위해 파일을 선택해주세요.", "alertNoFile": "가져올 파일을 선택하세요.",
"alertWrongFileType": "JSON 파일을 선택해주세요.", "alertWrongFileType": "JSON 파일을 선택세요.",
"Clear all statistics": "모든 통계 삭제", "Clear all statistics": "모든 통계 삭제",
"Skip existing": "기존 건너뛰기", "Skip existing": "기존 항목 건너뛰기",
"Overwrite": "덮어쓰기", "Overwrite": "덮어쓰기",
"Options": "옵션", "Options": "옵션",
"Keep both": "두개 모두 보존", "Keep both": "모두 보존",
"Verify Token": "토큰 검증", "Verify Token": "토큰 검증",
"Setup 2FA": "2단계 인증 설정하기", "Setup 2FA": "2단계 인증 설정",
"Enable 2FA": "2단계 인증 활성화", "Enable 2FA": "2단계 인증 활성화",
"Disable 2FA": "2단계 인증 비활성화", "Disable 2FA": "2단계 인증 비활성화",
"2FA Settings": "2단계 인증 설정", "2FA Settings": "2단계 인증 설정",
@ -154,34 +154,34 @@
"Active": "활성화", "Active": "활성화",
"Inactive": "비활성화", "Inactive": "비활성화",
"Token": "토큰", "Token": "토큰",
"Show URI": "URI 보기", "Show URI": "URI 표시",
"Tags": "태그", "Tags": "태그",
"Add New below or Select...": "아래 새롭게 추가 또는 선택…", "Add New below or Select...": "아래에서 선택하거나 추가…",
"Tag with this name already exist.": "같은 태그 이름이 이미 존재해요.", "Tag with this name already exist.": "동일한 이름의 태그가 이미 존재합니다.",
"Tag with this value already exist.": "같은 값을 가진 태그가 이미 존재해요.", "Tag with this value already exist.": "동일한 값의 태그가 이미 존재합니다.",
"color": "색상", "color": "색상",
"value (optional)": "값 (선택)", "value (optional)": "값 (선택)",
"Gray": "회색", "Gray": "회색",
"Red": "빨간색", "Red": "빨",
"Orange": "주황", "Orange": "주황",
"Green": "초록", "Green": "초록",
"Blue": "파란색", "Blue": "파",
"Indigo": "남색", "Indigo": "인디고",
"Purple": "보라", "Purple": "보라",
"Pink": "핑크", "Pink": "핑크",
"Search...": "검색…", "Search...": "검색…",
"Avg. Ping": "평균 핑", "Avg. Ping": "평균 핑",
"Avg. Response": "평균 응답", "Avg. Response": "평균 응답",
"Entry Page": "첫 페이지", "Entry Page": "첫 페이지",
"statusPageNothing": "아무것도 없어요. 새로운 그룹 또는 모니터링을 추가해주세요.", "statusPageNothing": "아무것도 없습니다. 새 그룹이나 모니터를 추가하세요.",
"No Services": "서비스 없음", "No Services": "서비스 없음",
"All Systems Operational": "모든 시스템 정상", "All Systems Operational": "모든 시스템 작동 중",
"Partially Degraded Service": "일부 시스템 비정상", "Partially Degraded Service": "일부 서비스 불안정",
"Degraded Service": "모든 시스템 비정상", "Degraded Service": "서비스 불안정",
"Add Group": "그룹 추가", "Add Group": "그룹 추가",
"Add a monitor": "모니터 추가", "Add a monitor": "모니터 추가",
"Edit Status Page": "상태 페이지 수정", "Edit Status Page": "상태 페이지 편집",
"Go to Dashboard": "대시보드로 가기", "Go to Dashboard": "대시보드로",
"Status Page": "상태 페이지", "Status Page": "상태 페이지",
"Status Pages": "상태 페이지", "Status Pages": "상태 페이지",
"defaultNotificationName": "내 {notification} 알림 ({number})", "defaultNotificationName": "내 {notification} 알림 ({number})",
@ -198,8 +198,8 @@
"webhook": "Webhook", "webhook": "Webhook",
"Post URL": "Post URL", "Post URL": "Post URL",
"Content Type": "Content Type", "Content Type": "Content Type",
"webhookJsonDesc": "{0}은 Express.js와 같은 최신 HTTP 서버에 적합해요", "webhookJsonDesc": "{0}은(는) Express.js와 같은 모던 HTTP 서버에 적합합니다.",
"webhookFormDataDesc": "{multipart}은 PHP에 적합해요. {decodeFunction}를 기준으로 JSON을 디코딩하면 되어요", "webhookFormDataDesc": "{multipart}는 PHP에 적합합니다. JSON은 {decodeFunction}을 사용해 파싱해야 합니다.",
"smtp": "Email (SMTP)", "smtp": "Email (SMTP)",
"secureOptionNone": "없음 / STARTTLS (25, 587)", "secureOptionNone": "없음 / STARTTLS (25, 587)",
"secureOptionTLS": "TLS (465)", "secureOptionTLS": "TLS (465)",
@ -215,26 +215,26 @@
"Prefix Custom Message": "접두사 메시지", "Prefix Custom Message": "접두사 메시지",
"Hello @everyone is...": "{'@'}everyone 서버 상태 알림이에요…", "Hello @everyone is...": "{'@'}everyone 서버 상태 알림이에요…",
"teams": "Microsoft Teams", "teams": "Microsoft Teams",
"Webhook URL": "웹훅 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",
"Priority": "우선 순위", "Priority": "우선 순위",
"slack": "Slack", "slack": "Slack",
"Icon Emoji": "아이콘 이모지", "Icon Emoji": "아이콘 이모지",
"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칸을 공백으로 두면 기본적으로 Github Project 페이지로 설정해요.", "aboutKumaURL": "Uptime Kuma URL 필드를 공백으로 두면 기본적으로 Github Project 페이지로 설정합니다,",
"emojiCheatSheet": "이모지 목록 시트: {0}", "emojiCheatSheet": "이모지 목록: {0}",
"rocket.chat": "Rocket.chat", "rocket.chat": "Rocket.chat",
"pushover": "Pushover", "pushover": "Pushover",
"pushy": "Pushy", "pushy": "Pushy",
@ -263,9 +263,9 @@
"Example:": "예: {0}", "Example:": "예: {0}",
"Read more:": "더 보기: {0}", "Read more:": "더 보기: {0}",
"Status:": "상태: {0}", "Status:": "상태: {0}",
"Read more": "더 보기", "Read more": "더보기",
"appriseInstalled": "Apprise가 설치되어있어요.", "appriseInstalled": "Apprise가 설치되어 있습니다.",
"appriseNotInstalled": "Apprise가 설치되어있지 않아요. {0}", "appriseNotInstalled": "Apprise가 설치되지 않았습니다. {0}",
"Access Token": "액세스 토큰", "Access Token": "액세스 토큰",
"Channel access token": "채널 액세스 토큰", "Channel access token": "채널 액세스 토큰",
"Line Developers Console": "Line 개발자 콘솔", "Line Developers Console": "Line 개발자 콘솔",
@ -284,10 +284,10 @@
"promosmsTypeSpeed": "SMS SPEED - 시스템에서 가장 높은 우선순위예요. 매우 빠르고 신뢰할 수 있지만 비용이 많이 들어요 (SMS 전체 가격의 약 두 배).", "promosmsTypeSpeed": "SMS SPEED - 시스템에서 가장 높은 우선순위예요. 매우 빠르고 신뢰할 수 있지만 비용이 많이 들어요 (SMS 전체 가격의 약 두 배).",
"promosmsPhoneNumber": "전화 번호 (폴란드 수신자라면 지역번호를 적지 않아도 되어요.)", "promosmsPhoneNumber": "전화 번호 (폴란드 수신자라면 지역번호를 적지 않아도 되어요.)",
"promosmsSMSSender": "SMS 보내는 사람 이름 : 미리 등록된 이름 혹은 기본값 중 하나예요: InfoSMS, SMS Info, MaxSMS, INFO, SMS", "promosmsSMSSender": "SMS 보내는 사람 이름 : 미리 등록된 이름 혹은 기본값 중 하나예요: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Primary Base URL": "기본 URL", "Primary Base URL": "주 베이스 URL",
"Push URL": "Push URL", "Push URL": "Push URL",
"needPushEvery": "이 URL을 {0} 초 마다 호출할 수 있어요.", "needPushEvery": "이 URL을 {0}초 마다 호출할 수 있습니다.",
"pushOptionalParams": "선택적 파라미터: {0}", "pushOptionalParams": "추가 파라미터: {0}",
"emailCustomSubject": "커스텀 주제", "emailCustomSubject": "커스텀 주제",
"clicksendsms": "ClickSend SMS", "clicksendsms": "ClickSend SMS",
"checkPrice": "{0} 가격 확인:", "checkPrice": "{0} 가격 확인:",
@ -297,110 +297,110 @@
"matrixHomeserverURL": "Homeserver URL (http(s):// 와 함께 적어주세요. 그리고 포트 번호는 선택적 입니다.)", "matrixHomeserverURL": "Homeserver URL (http(s):// 와 함께 적어주세요. 그리고 포트 번호는 선택적 입니다.)",
"Internal Room Id": "내부 방 ID", "Internal Room Id": "내부 방 ID",
"matrixDesc1": "Matrix 클라이언트 방 설정의 고급 섹션에서 내부 방 ID를 찾을 수 있어요. 내부 방 ID는 이렇게 생겼답니다: !QMdRCpUIfLwsfjxye6:home.server.", "matrixDesc1": "Matrix 클라이언트 방 설정의 고급 섹션에서 내부 방 ID를 찾을 수 있어요. 내부 방 ID는 이렇게 생겼답니다: !QMdRCpUIfLwsfjxye6:home.server.",
"matrixDesc2": "사용자의 모든 방에 대한 엑세스가 허용될 수 있어서 새로운 사용자를 만들고 원하는 방에만 초대한 후 엑세스 토큰을 사용하는 것이 좋아요. {0} 이 명령어를 통해 엑세스 토큰을 얻을 수 있어요", "matrixDesc2": "개인 Matrix 사용자 계정의 액세스 토큰을 사용하는 것은 계정 전체와 참여 중인 모든 방에 완전한 접근 권한을 부여하게 되므로 권장되지 않습니다. 대신 새로운 사용자를 생성한 후 알림을 받을 방에만 초대하는 것을 권장합니다. 액세스 토큰은 {0} 명령어를 실행하여 얻을 수 있습니다.",
"Method": "메서드", "Method": "Method",
"Body": "Body", "Body": "Body",
"Headers": "헤더", "Headers": "헤더",
"PushUrl": "Push URL", "PushUrl": "Push URL",
"HeadersInvalidFormat": "요청 Headers의 JSON 형식이 올바르지 않아요: ", "HeadersInvalidFormat": "요청 헤더의 JSON 형식이 올바르지 않음: ",
"BodyInvalidFormat": "요청 Body의 JSON 형식이 올바르지 않아요: ", "BodyInvalidFormat": "요청 본문의 JSON 형식이 올바르지 않음: ",
"Monitor History": "모니터 기록", "Monitor History": "모니터 기록",
"clearDataOlderThan": "모니터링 기록을 {0}일 동안 저장해요.", "clearDataOlderThan": "모니터 기록을 {0}일간 저장합니다.",
"PasswordsDoNotMatch": "비밀번호가 일치하지 않아요.", "PasswordsDoNotMatch": "비밀번호가 일치하지 않습니다.",
"records": "records", "records": "레코드",
"One record": "One record", "One record": "One record",
"steamApiKeyDescription": "스팀 게임 서버를 모니터링하려면 Steam Web API 키가 필요해요. API 키는 하단 웹사이트에서 등록할 수 있어요: ", "steamApiKeyDescription": "Steam 게임 서버를 모니터링하려면 Steam Web-API 키가 필요합니다. 여기서 API 키를 등록하세요: ",
"Current User": "현재 사용자", "Current User": "현재 사용자",
"recent": "최근", "recent": "최근",
"Done": "완료", "Done": "완료",
"Info": "정보", "Info": "정보",
"Security": "보안", "Security": "보안",
"Steam API Key": "스팀 API 키", "Steam API Key": "Steam API 키",
"Shrink Database": "데이터베이스 축소", "Shrink Database": "데이터베이스 축소",
"Pick a RR-Type...": "RR-Type을 골라주세요…", "Pick a RR-Type...": "RR-Type 선택…",
"Pick Accepted Status Codes...": "상태 코드를 골라주세요…", "Pick Accepted Status Codes...": "성공 상태 코드 선택…",
"Default": "기본", "Default": "기본",
"HTTP Options": "HTTP 옵션", "HTTP Options": "HTTP 옵션",
"Create Incident": "인시던트 만들기", "Create Incident": "인시던트 생성",
"Title": "제목", "Title": "제목",
"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": "태그 숨기기",
"Description": "설명", "Description": "설명",
"No monitors available.": "모니터링이 없어요.", "No monitors available.": "사용 가능한 모니터가 없습니다.",
"Add one": "추가하기", "Add one": "추가하기",
"No Monitors": "모니터 없음", "No Monitors": "모니터 없음",
"Untitled Group": "이름없는 그룹", "Untitled Group": "제목 없는 그룹",
"Services": "서비스", "Services": "서비스",
"Discard": "취소", "Discard": "취소",
"Cancel": "취소", "Cancel": "닫기",
"Powered by": "Powered by", "Powered by": "Powered by",
"serwersms": "SerwerSMS.pl", "serwersms": "SerwerSMS.pl",
"serwersmsAPIUser": "API Usename (webapi_ 접두사 포함)", "serwersmsAPIUser": "API 사용자명 (webapi_ 접두사 포함)",
"serwersmsAPIPassword": "API 비밀번호", "serwersmsAPIPassword": "API 비밀번호",
"serwersmsPhoneNumber": "휴대전화 번호", "serwersmsPhoneNumber": "휴대 번호",
"serwersmsSenderName": "보내는 사람 이름 (customer portal를 통해 가입된 정보)", "serwersmsSenderName": "SMS 발신자명 (customer portal로 가입된 정보)",
"stackfield": "Stackfield", "stackfield": "Stackfield",
"dnsPortDescription": "DNS 서버 포트, 기본값은 53 이에요. 포트는 언제나 변경할 수 있어요.", "dnsPortDescription": "DNS 서버 포트, 기본값은 53 이에요. 포트는 언제나 변경할 수 있어요.",
"PushByTechulus": "Push by Techulus", "PushByTechulus": "Push by Techulus",
"GoogleChat": "Google Chat (Google Workspace only)", "GoogleChat": "Google Chat (Google Workspace only)",
"topic": "Topic", "topic": "Topic",
"topicExplanation": "모니터링할 MQTT Topic", "topicExplanation": "모니터링할 MQTT 토픽",
"successMessage": "성공 메시지", "successMessage": "성공 메시지",
"successMessageExplanation": "성공으로 간주되는 MQTT 메시지", "successMessageExplanation": "성공으로 간주되는 MQTT 메시지",
"error": "오류", "error": "오류",
"critical": "크리티컬", "critical": "중대",
"Customize": "커스터마이즈", "Customize": "사용자화",
"Custom Footer": "커스텀 Footer", "Custom Footer": "사용자 지정 푸터",
"Custom CSS": "커스텀 CSS", "Custom CSS": "사용자 지정 CSS",
"smtpDkimSettings": "DKIM 설정", "smtpDkimSettings": "DKIM 설정",
"smtpDkimDesc": "사용 방법은 DKIM {0}를 참조하세요.", "smtpDkimDesc": "사용 방법은 Nodemailer DKIM {0}을(를) 참조하세요.",
"documentation": "문서", "documentation": "문서",
"smtpDkimDomain": "도메인 이름", "smtpDkimDomain": "도메인 이름",
"smtpDkimKeySelector": "Key Selector", "smtpDkimKeySelector": "Key Selector",
"smtpDkimPrivateKey": "Private Key", "smtpDkimPrivateKey": "비밀 키",
"smtpDkimHashAlgo": "해시 알고리즘 (선택)", "smtpDkimHashAlgo": "해시 알고리즘 (선택)",
"smtpDkimheaderFieldNames": "서명할 헤더 키 (선택)", "smtpDkimheaderFieldNames": "서명할 헤더 키 (선택)",
"smtpDkimskipFields": "서명하지 않을 헤더 키 (선택)", "smtpDkimskipFields": "서명하지 않을 헤더 키 (선택)",
"wayToGetPagerDutyKey": "Service -> Service Directory -> (서비스 선택) -> Integrations -> Add integration. 에서 찾을 수 있어요. 자세히 알아보려면 {0}에서 \"Events API V2\"를 검색해봐요", "wayToGetPagerDutyKey": "\"Events API V2\"는 Service -> Service Directory -> (서비스 선택) -> Integrations -> Add integration. 에서 찾을 수 있습니다. 자세한 내용 {0}",
"Integration Key": "Integration 키", "Integration Key": "Integration 키",
"Integration URL": "Integration URL", "Integration URL": "Integration URL",
"Auto resolve or acknowledged": "자동 해결 혹은 승인", "Auto resolve or acknowledged": "자동 해결 또는 승인",
"do nothing": "아무것도 하지 않기", "do nothing": "아무것도 하지 않기",
"auto acknowledged": "자동 승인 (acknowledged)", "auto acknowledged": "자동 승인 (acknowledged)",
"auto resolve": "자동 해결 (resolve)", "auto resolve": "자동 해결 (resolve)",
"gorush": "Gorush", "gorush": "Gorush",
"alerta": "Alerta", "alerta": "Alerta",
"alertaApiEndpoint": "API Endpoint", "alertaApiEndpoint": "API 엔드포인트",
"alertaEnvironment": "환경변수", "alertaEnvironment": "환경",
"alertaApiKey": "API 키", "alertaApiKey": "API 키",
"alertaAlertState": "경고 상태", "alertaAlertState": "알림 상태",
"alertaRecoverState": "해결된 상태", "alertaRecoverState": "복구 상태",
"deleteStatusPageMsg": "정말 이 상태 페이지를 삭제할까요?", "deleteStatusPageMsg": "이 상태 페이지를 삭제하시겠습니까?",
"Proxies": "프록시", "Proxies": "프록시",
"default": "Default", "default": "기본",
"enabled": "활성화", "enabled": "활성화",
"setAsDefault": "기본 프록시로 설정", "setAsDefault": "기본로 설정",
"deleteProxyMsg": "정말 이 프록시를 모든 모니터링에서 삭제할까요?", "deleteProxyMsg": "이 프록시를 모든 모니터에서 삭제하시겠습니까?",
"proxyDescription": "프록시가 작동하려면 모니터에 할당되어야 해요.", "proxyDescription": "프록시가 작동하려면 모니터에 할당되어야 합니다.",
"enableProxyDescription": "이 프록시는 활성화될 때까지 영향을 미치지 않아요. 활성화 상태에 따라 모든 모니터에서 프록시를 일시정지할 수 있어요.", "enableProxyDescription": "이 프록시는 활성화될 때까지 모니터의 요청에 미치지 않습니다. 활성화 상태를 통해 모든 모니터에서 프록시를 일시 정지할 수 있습니다.",
"setAsDefaultProxyDescription": "새로 추가하는 모든 모니터링에 이 프록시를 기본적으로 활성화해요. 각 모니터에 대해 별도로 프록시를 비활성화할 수 있어요.", "setAsDefaultProxyDescription": "새 모니터에 이 프록시를 기본적으로 활성화합니다. 개별 모니터에 대해 프록시를 비활성화할 수 있습니다.",
"Certificate Chain": "인증서 체인", "Certificate Chain": "인증서 체인",
"Valid": "유효", "Valid": "유효",
"Invalid": "유효하지 않음", "Invalid": "유효하지 않음",
"AccessKeyId": "AccessKey ID", "AccessKeyId": "AccessKey ID",
"SecretAccessKey": "AccessKey Secret", "SecretAccessKey": "AccessKey Secret",
@ -425,17 +425,17 @@
"Proxy server has authentication": "프록시 서버에 인증 절차가 있음", "Proxy server has authentication": "프록시 서버에 인증 절차가 있음",
"User": "사용자", "User": "사용자",
"Installed": "설치됨", "Installed": "설치됨",
"Not installed": "설치되어 있지 않음", "Not installed": "설치되지 않음",
"Running": "작동 중", "Running": "작동 중",
"Not running": "작동하고 있지 않음", "Not running": "작동 중이 아님",
"Remove Token": "토큰 제", "Remove Token": "토큰 ",
"Start": "시작", "Start": "시작",
"Stop": "정지", "Stop": "정지",
"Uptime Kuma": "Uptime Kuma", "Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "새로운 상태 페이지 만들기", "Add New Status Page": "새 상태 페이지 추가",
"Slug": "주소", "Slug": "Slug",
"Accept characters:": "허용되는 문자열:", "Accept characters:": "허용되는 문자열:",
"startOrEndWithOnly": "{0} 로 시작하거나 끝나야 해요", "startOrEndWithOnly": "{0}로 시작하거나 끝나야 합니다.",
"No consecutive dashes": "연속되는 대시는 허용되지 않아요", "No consecutive dashes": "연속되는 대시는 허용되지 않아요",
"Next": "다음", "Next": "다음",
"The slug is already taken. Please choose another slug.": "이미 존재하는 주소에요. 다른 주소를 사용해 주세요.", "The slug is already taken. Please choose another slug.": "이미 존재하는 주소에요. 다른 주소를 사용해 주세요.",
@ -469,7 +469,7 @@
"onebotGroupMessage": "그룹 메시지", "onebotGroupMessage": "그룹 메시지",
"onebotPrivateMessage": "개인 메시지", "onebotPrivateMessage": "개인 메시지",
"onebotUserOrGroupId": "그룹/사용자 ID", "onebotUserOrGroupId": "그룹/사용자 ID",
"onebotSafetyTips": "을 위해 Access 토큰을 설정하세요", "onebotSafetyTips": "안을 위해 Access 토큰을 설정하세요.",
"PushDeer Key": "PushDeer 키", "PushDeer Key": "PushDeer 키",
"Footer Text": "Footer 문구", "Footer Text": "Footer 문구",
"Show Powered By": "Powered By 문구 표시하기", "Show Powered By": "Powered By 문구 표시하기",
@ -479,9 +479,9 @@
"Certificate Expiry Notification": "인증서 만료 알림", "Certificate Expiry Notification": "인증서 만료 알림",
"API Username": "API 사용자 이름", "API Username": "API 사용자 이름",
"API Key": "API 키", "API Key": "API 키",
"Recipient Number": "받는 사람 번호", "Recipient Number": "수신자 번호",
"From Name/Number": "발신자 이름/번호", "From Name/Number": "발신자 이름/번호",
"Leave blank to use a shared sender number.": "공유 발신 번호를 사용하려면 공백으로 두세요.", "Leave blank to use a shared sender number.": "공유 발신 번호를 사용하려면 공백으로 두세요.",
"Octopush API Version": "Octopush API 버전", "Octopush API Version": "Octopush API 버전",
"Legacy Octopush-DM": "레거시 Octopush-DM", "Legacy Octopush-DM": "레거시 Octopush-DM",
"endpoint": "endpoint", "endpoint": "endpoint",
@ -526,9 +526,9 @@
"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 사용자명과 키를 얻을 수 있습니다.",
"Custom Monitor Type": "커스텀 모니터", "Custom Monitor Type": "커스텀 모니터",
"deleteDockerHostMsg": "정말 이 도커 호스트를 모든 모니터링에서 삭제할까요?", "deleteDockerHostMsg": "이 Docker 호스트를 모든 모니터에서 삭제하시겠습니까?",
"trustProxyDescription": "'X-Forwarded-*' 헤더를 신뢰해요. 올바른 클라이언트 IP를 얻어야하고Uptime Kuma가 Nginx나 Apache 같은 프록시 뒤에 있다면 이 기능을 활성화해야 해요.", "trustProxyDescription": "'X-Forwarded-*' 헤더를 신뢰해요. 올바른 클라이언트 IP를 얻어야하고Uptime Kuma가 Nginx나 Apache 같은 프록시 뒤에 있다면 이 기능을 활성화해야 해요.",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "프로필 이름(왼쪽 아래)을 클릭하고 아래로 스크롤한 다음 토큰 만들기를 클릭하여 장기 액세스 토큰을 만들 수 있어요. ", "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "프로필 이름(왼쪽 아래)을 클릭하고 아래로 스크롤한 다음 토큰 만들기를 클릭하여 장기 액세스 토큰을 만들 수 있어요. ",
"Then choose an action, for example switch the scene to where an RGB light is red.": "그런 다음 동작을 선택해요, 예를 들어 장면을 RGB 조명이 빨간색인 곳으로 전환해요.", "Then choose an action, for example switch the scene to where an RGB light is red.": "그런 다음 동작을 선택해요, 예를 들어 장면을 RGB 조명이 빨간색인 곳으로 전환해요.",
@ -541,12 +541,12 @@
"You can divide numbers with": "다음과 같이 숫자를 구분할 수 있어요:", "You can divide numbers with": "다음과 같이 숫자를 구분할 수 있어요:",
"goAlertInfo": "GoAlert는 온콜 스케줄링, 자동 에스컬레이션 및 알림(SMS 또는 음성 통화와 같은)을 위한 오픈 소스 응용 프로그램이에요. 올바른 사람, 올바른 방법, 적절한 시간에 자동으로 참여하세요! {0}", "goAlertInfo": "GoAlert는 온콜 스케줄링, 자동 에스컬레이션 및 알림(SMS 또는 음성 통화와 같은)을 위한 오픈 소스 응용 프로그램이에요. 올바른 사람, 올바른 방법, 적절한 시간에 자동으로 참여하세요! {0}",
"smseagle": "SMSEagle", "smseagle": "SMSEagle",
"smseagleTo": "휴대전화 번호", "smseagleTo": "휴대 번호",
"smseagleRecipient": "받는 사람 (쉼표로 구분)", "smseagleRecipient": "수신자 (여러 명인 경우 쉼표로 구분)",
"Maintenance": "점검", "Maintenance": "점검",
"statusMaintenance": "점검 중", "statusMaintenance": "점검 중",
"resendEveryXTimes": "{0}번마다 다시 보내요", "resendEveryXTimes": "{0}번마다 재전송",
"resendDisabled": "다시 보내지 않아요", "resendDisabled": "재전송하지 않음",
"loadingError": "데이터를 가져올 수 없어요, 나중에 다시 시도하세요.", "loadingError": "데이터를 가져올 수 없어요, 나중에 다시 시도하세요.",
"plugin": "플러그인", "plugin": "플러그인",
"install": "설치", "install": "설치",
@ -575,14 +575,14 @@
"Bark Group": "Bark 그룹", "Bark Group": "Bark 그룹",
"Bark Sound": "Bark 소리", "Bark Sound": "Bark 소리",
"promosmsAllowLongSMS": "긴 SMS 허용", "promosmsAllowLongSMS": "긴 SMS 허용",
"smseagleGroup": "전화번호부 그룹 이름", "smseagleGroup": "연락처 그룹명 목록",
"smseagleContact": "전화번호부 연락처 이름", "smseagleContact": "연락처 이름 목록",
"smseagleRecipientType": "받는 사람 종류", "smseagleRecipientType": "수신자 종류",
"smseagleToken": "API 세스 토큰", "smseagleToken": "API 세스 토큰",
"smseagleUrl": "SMSEagle 기기 URL", "smseagleUrl": "SMSEagle 기기 URL",
"smseagleEncoding": "유니코드로 보내기", "smseagleEncoding": "유니코드로 보내기",
"smseaglePriority": "메시지 우선 순위 (0-9, 기본값= 0)", "smseaglePriority": "메시지 우선 순위 (0-9, 기본값= 0)",
"ntfy Topic": "ntfy 주제", "ntfy Topic": "ntfy 토픽",
"HomeAssistant": "홈 어시스턴트", "HomeAssistant": "홈 어시스턴트",
"RadiusSecretDescription": "클라이언트와 서버 간의 비밀 키", "RadiusSecretDescription": "클라이언트와 서버 간의 비밀 키",
"RadiusSecret": "Radius 비밀 키", "RadiusSecret": "Radius 비밀 키",
@ -594,7 +594,7 @@
"Request Timeout": "요청 타임아웃", "Request Timeout": "요청 타임아웃",
"Query": "쿼리", "Query": "쿼리",
"settingsCertificateExpiry": "TLS 인증서 만료", "settingsCertificateExpiry": "TLS 인증서 만료",
"certificationExpiryDescription": "HTTPS 모니터링 TLS 인증서가 만료되면 알림을 활성화해요:", "certificationExpiryDescription": "TLS 인증서가 설정된 기간 내에 만료될 경우, HTTPS 모니터가 알림을 전송합니다:",
"Setup Docker Host": "도커 호스트 설정", "Setup Docker Host": "도커 호스트 설정",
"Docker Daemon": "도커 데몬", "Docker Daemon": "도커 데몬",
"socket": "소켓", "socket": "소켓",
@ -623,7 +623,7 @@
"Event data:": "이벤트 데이터:", "Event data:": "이벤트 데이터:",
"Frontend Version": "프론트엔드 버전", "Frontend Version": "프론트엔드 버전",
"Frontend Version do not match backend version!": "프론트엔드 버전이 백엔드 버전과 일치하지 않아요!", "Frontend Version do not match backend version!": "프론트엔드 버전이 백엔드 버전과 일치하지 않아요!",
"confirmDeleteTagMsg": "정말 이 태그를 삭제할까요? 이 태그와 연결된 모니터링은 삭제되지 않아요.", "confirmDeleteTagMsg": "이 태그를 삭제하시겠습니까? 이 태그와 연결된 모니터는 삭제되지 않습니다.",
"infiniteRetention": "무한히 저장하려면 0으로 설정하세요.", "infiniteRetention": "무한히 저장하려면 0으로 설정하세요.",
"backupRecommend": "대신 볼륨 또는 데이터 폴더 (./data/) 를 직접 백업하세요.", "backupRecommend": "대신 볼륨 또는 데이터 폴더 (./data/) 를 직접 백업하세요.",
"Optional": "선택", "Optional": "선택",
@ -670,64 +670,64 @@
"grpcMethodDescription": "메서드 이름은 sayHello, check와 같은 카멜 케이스로 변환되어요.", "grpcMethodDescription": "메서드 이름은 sayHello, check와 같은 카멜 케이스로 변환되어요.",
"deleteMaintenanceMsg": "정말 이 점검을 삭제할까요?", "deleteMaintenanceMsg": "정말 이 점검을 삭제할까요?",
"recurringIntervalMessage": "매일 한 번 실행 | {0}일마다 한 번 실행", "recurringIntervalMessage": "매일 한 번 실행 | {0}일마다 한 번 실행",
"affectedMonitorsDescription": "현재 점검에 영향을 받는 모니터링 선택하기", "affectedMonitorsDescription": "현재 유지보수에 영향을 받는 모니터를 선택하세요.",
"affectedStatusPages": "점검 메시지를 표시할 상태 페이지 선택하기", "affectedStatusPages": "점검 메시지를 표시할 상태 페이지 선택하기",
"Kook": "Kook", "Kook": "Kook",
"atLeastOneMonitor": "최소 1개의 모니터링을 선택하세요", "atLeastOneMonitor": "적어도 1개 이상의 모니터를 선택하세요.",
"wayToGetKookBotToken": "{0} 에서 애플리케이션을 만들고 봇 토큰을 얻어요", "wayToGetKookBotToken": "{0} 에서 애플리케이션을 만들고 봇 토큰을 얻어요",
"Help": "도움말", "Help": "도움말",
"Game": "게임", "Game": "게임",
"General Monitor Type": "일반 모니터", "General Monitor Type": "일반 모니터 유형",
"Passive Monitor Type": "수동 모니터", "Passive Monitor Type": "수동 모니터 유형",
"Specific Monitor Type": "특정 모니터", "Specific Monitor Type": "특정 모니터 유형",
"Monitor": "모니터", "Monitor": "모니터",
"Resend Notification if Down X times consecutively": "X번 중단될 경우 알림 다시 보내기", "Resend Notification if Down X times consecutively": "연속적인 다운으로 판단해 알림을 재전송할 기준 횟수",
"Schedule maintenance": "점검 예약하기", "Schedule maintenance": "유지보수 예약",
"Affected Monitors": "영향을 받는 모니터", "Affected Monitors": "영향을 받는 모니터",
"Pick Affected Monitors...": "영향을 받는 모니터 선택하기…", "Pick Affected Monitors...": "영향을 받는 모니터 선택…",
"Start of maintenance": "점검 시작", "Start of maintenance": "점검 시작",
"All Status Pages": "모든 상태 페이지", "All Status Pages": "모든 상태 페이지",
"Select status pages...": "상태 페이지 선택하기…", "Select status pages...": "상태 페이지 선택…",
"Custom": "커스텀", "Custom": "사용자 지정",
"webhookAdditionalHeadersTitle": "추가 헤더", "webhookAdditionalHeadersTitle": "추가 헤더",
"webhookAdditionalHeadersDesc": "웹훅과 함께 전송될 추가 헤더를 설정해요. 각각의 헤더는 JSON 키/값으로 구성되어야 해요.", "webhookAdditionalHeadersDesc": "Webhook과 함께 전송되는 추가 헤더를 설정합니다. 각각의 헤더는 JSON 키/값으로 이루어져야 합니다.",
"HTTP Headers": "HTTP 헤더", "HTTP Headers": "HTTP 헤더",
"Trust Proxy": "프록시 신뢰", "Trust Proxy": "프록시 신뢰",
"API Keys": "API 키", "API Keys": "API 키",
"markdownSupported": "Markdown 문법이 지원됨", "markdownSupported": "마크다운 문법 사용 가능",
"telegramMessageThreadID": "(선택) 메시지 스레드 ID", "telegramMessageThreadID": "(선택) 메시지 스레드 ID",
"Clone": "복제", "Clone": "복제",
"cloneOf": "{0}의 복제본", "cloneOf": "{0}의 복제본",
"Clone Monitor": "모니터 복제", "Clone Monitor": "모니터 복제",
"telegramProtectContent": "포워딩/저장 보호", "telegramProtectContent": "포워딩/저장 보호",
"telegramProtectContentDescription": "활성화 할경우 텔레그램 봇 메시지는 포워딩 및 저장으로부터 보호됩니다.", "telegramProtectContentDescription": "활성화 할경우 텔레그램 봇 메시지는 포워딩 및 저장으로부터 보호됩니다.",
"telegramSendSilentlyDescription": "조용히 메시지를 보냅니다. 사용자들은 무음으로 알림을 받습니다.", "telegramSendSilentlyDescription": "조용히 메시지를 보냅니다. 사용자들은 무음으로 알림을 받습니다.",
"telegramSendSilently": "무음 알림", "telegramSendSilently": "무음 알림",
"Add New Tag": "태그 추가", "Add New Tag": "태그 추가",
"Edit Tag": "태그 수정", "Edit Tag": "태그 편집",
"Server Address": "서버 주소", "Server Address": "서버 주소",
"Learn More": "자세히 알아보기", "Learn More": "자세히 알아보기",
"Continue": "계속", "Continue": "계속",
"Key Added": "키 추가됨", "Key Added": "키 추가됨",
"No API Keys": "API 키 없음", "No API Keys": "API 키 없음",
"disableAPIKeyMsg": "이 API키를 정말로 비활성화하시겠습니까?", "disableAPIKeyMsg": "이 API 키를 비활성화하시겠습니까?",
"deleteAPIKeyMsg": "이 API키를 정말로 삭제하시겠습니까?", "deleteAPIKeyMsg": "이 API 키를 삭제하시겠습니까?",
"Generate": "생성", "Generate": "생성",
"Body Encoding": "Body 인코딩", "Body Encoding": "본문(Body) 인코딩",
"Expiry": "만료", "Expiry": "만료",
"Expiry date": "만료 날짜", "Expiry date": "만료",
"Don't expire": "만료되지 않음", "Don't expire": "만료되지 않음",
"notificationRegional": "지역별", "notificationRegional": "지역별",
"Google Analytics ID": "Google Analytics ID", "Google Analytics ID": "Google 애널리틱스 ID",
"Add API Key": "API 키 추가", "Add API Key": "API 키 추가",
"apiKeyAddedMsg": "API 키가 추가되었습니다. 다시 표시되지 않을 것이므로 메모해 두세요.", "apiKeyAddedMsg": "API 키가 추가되었습니다. 다시 표시되지 않므로 메모해 두세요.",
"pagertreeCritical": "긴급", "pagertreeCritical": "긴급",
"apiKey-active": "사용 가능", "apiKey-active": "활성",
"lunaseaUserID": "사용자 ID", "lunaseaUserID": "사용자 ID",
"apiKey-expired": "만료됨", "apiKey-expired": "만료됨",
"Expires": "만료", "Expires": "만료",
"twilioAuthToken": "인증 토큰 / API 키 시크릿", "twilioAuthToken": "인증 토큰 / API 키 시크릿",
"twilioFromNumber": "번호에서", "twilioFromNumber": "발신 번호",
"twilioToNumber": "번호에서", "twilioToNumber": "번호에서",
"twilioAccountSID": "계정 SID", "twilioAccountSID": "계정 SID",
"pagertreeUrgency": "긴급", "pagertreeUrgency": "긴급",
@ -739,75 +739,75 @@
"invalidCronExpression": "알수없는 Cron 값입니다: {0}", "invalidCronExpression": "알수없는 Cron 값입니다: {0}",
"Add Another": "다른 항목 추가", "Add Another": "다른 항목 추가",
"apiKey-inactive": "비활성화", "apiKey-inactive": "비활성화",
"pagertreeIntegrationUrl": "Integration 링크", "pagertreeIntegrationUrl": "Integration URL",
"pagertreeLow": "낮음", "pagertreeLow": "낮음",
"pagertreeMedium": "중간", "pagertreeMedium": "중간",
"pagertreeHigh": "높음", "pagertreeHigh": "높음",
"pagertreeResolve": "자동으로 해결하기", "pagertreeResolve": "자동으로 해결",
"pagertreeDoNothing": "아무것도 하지 않음", "pagertreeDoNothing": "아무것도 하지 않음",
"wayToGetPagerTreeIntegrationURL": "PagerTree에서 Uptime Kuma 통합을 생성한 후 Endpoint를 복사합니다. 전체 세부 정보 보기 {0}", "wayToGetPagerTreeIntegrationURL": "PagerTree에서 Uptime Kuma 통합을 생성한 후 엔드포인트를 복사합니다. 세부 정보 보기 {0}",
"lunaseaTarget": "대상", "lunaseaTarget": "대상",
"lunaseaDeviceID": "기기 ID", "lunaseaDeviceID": "디바이스 ID",
"statusPageRefreshIn": "{0} 후 새로고침", "statusPageRefreshIn": "{0} 후 새로고침",
"telegramMessageThreadIDDescription": "포럼의 대상 메시지 쓰레드(주제)에 대한 선택적 고유 식별인, 포럼 관리자 그룹에만 해당", "telegramMessageThreadIDDescription": "포럼의 대상 메시지 쓰레드(주제)에 대한 선택적 고유 식별인, 포럼 관리자 그룹에만 해당",
"pagertreeSilent": "없음", "pagertreeSilent": "없음",
"setupDatabaseChooseDatabase": "어떤 데이터베이스를 사용하시겠습니까?", "setupDatabaseChooseDatabase": "어떤 데이터베이스를 사용하시겠습니까?",
"setupDatabaseEmbeddedMariaDB": "추가 설정은 필요 없습니다. 이 도커 이미지에는 MariaDB가 내장되어 구성되어 있습니다. Uptime Kuma는 Unix Socket을 통해 데이터베이스에 연결합니다.", "setupDatabaseEmbeddedMariaDB": "추가 설정이 필요하지 않습니다. 이 도커 이미지에는 MariaDB가 자동으로 포함 및 구성되어 있으며, Uptime Kuma는 유닉스 소켓을 통해 데이터베이스에 연결합니다.",
"setupDatabaseMariaDB": "외부 MariaDB 데이터베이스에 연결합니다. 데이터베이스 연결 정보를 설정해야 합니다.", "setupDatabaseMariaDB": "외부 MariaDB 데이터베이스에 연결합니다. 데이터베이스 연결 정보를 설정해야 합니다.",
"setupDatabaseSQLite": "소규모 배포에 권장되는 간단한 데이터베이스 파일입니다. v2.0.0 이전에는 Uptime Kuma가 SQLite를 기본 데이터베이스로 사용했습니다.", "setupDatabaseSQLite": "소규모 배포에 권장되는 간단한 데이터베이스 파일입니다. Uptime Kuma는 v2.0.0 이전까지 SQLite를 기본 데이터베이스로 사용했습니다.",
"dbName": "데이터베이스 이름", "dbName": "데이터베이스 이름",
"filterActive": "활성", "filterActive": "활성",
"filterActivePaused": "일시지", "filterActivePaused": "일시지",
"Home": "홈", "Home": "홈",
"Cannot connect to the socket server": "소켓 서버에 연결 할 수 없습니다", "Cannot connect to the socket server": "소켓 서버에 연결할 수 없습니다.",
"Reconnecting...": "재 연결중...", "Reconnecting...": "다시 연결하는 중...",
"Json Query": "JSON 쿼리", "Json Query": "JSON 쿼리",
"settingUpDatabaseMSG": "데이터베이스를 설정하는 중입니다. 시간이 걸릴 수 있으니 기다려 주세요.", "settingUpDatabaseMSG": "데이터베이스를 설정하는 중입니다. 시간이 걸릴 수 있으니 잠시만 기다려 주세요.",
"enableNSCD": "모든 DNS 요청을 캐싱하기 위해 NSCD (Name Service Cache Daemon) 활성화", "enableNSCD": "모든 DNS 요청을 캐싱하기 위해 NSCD (Name Service Cache Daemon) 활성화",
"pushOthers": "기타", "pushOthers": "기타",
"programmingLanguages": "프로그래밍 언어", "programmingLanguages": "프로그래밍 언어",
"Select": "선택", "Select": "선택",
"Edit Maintenance": "점검 수정하기", "Edit Maintenance": "점검 수정하기",
"styleElapsedTime": "하트비트 바 밑의 지난 시간 표시", "styleElapsedTime": "하트비트 바 아래 표시되는 경과 시간",
"styleElapsedTimeShowNoLine": "보이기 (선 없음)", "styleElapsedTimeShowNoLine": "표시 (선 없음)",
"styleElapsedTimeShowWithLine": "보이기 (선 있음)", "styleElapsedTimeShowWithLine": "표시 (선 있음)",
"chromeExecutable": "Chrome/Chromium 실행 파일", "chromeExecutable": "Chrome/Chromium 실행 파일",
"chromeExecutableAutoDetect": "자동 감지", "chromeExecutableAutoDetect": "자동 감지",
"Invert Keyword": "키워드 반전", "Invert Keyword": "키워드 반전",
"Expected Value": "기값", "Expected Value": "기값",
"Add a domain": "도메인 추가", "Add a domain": "도메인 추가",
"Remove domain": "도메인 '{0}' 제거", "Remove domain": "도메인 '{0}' 제거",
"Monitor Group": "모니터 그룹", "Monitor Group": "모니터 그룹",
"Monitor Setting": "{0}의 모니터 설정", "Monitor Setting": "{0}의 모니터 설정",
"now": "지금", "now": "지금",
"time ago": "{0} 전", "time ago": "{0} 전",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "연결하려는 서버의 호스트 이름을 입력하거나 {local_mta}를 사용하려는 경우 {localhost}를 입력합니다", "Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "연결하려는 서버의 호스트 이름을 입력하거나, {local_mta}를 사용하려는 경우 {localhost}를 입력합니다.",
"-year": "-연도", "-year": "",
"Json Query Expression": "Json 쿼리 표현식", "Json Query Expression": "Json 쿼리 표현식",
"Host URL": "호스트 URL", "Host URL": "호스트 URL",
"locally configured mail transfer agent": "로컬 구성된 메일 전송 에이전트", "locally configured mail transfer agent": "로컬 구성된 메일 전송 에이전트",
"ignoreTLSErrorGeneral": "연결에 TLS/SSL 오류 무시하기", "ignoreTLSErrorGeneral": "연결 중 TLS/SSL 오류 무시",
"ignoredTLSError": "TLS/SSL 오류가 무시되었습니다", "ignoredTLSError": "TLS/SSL 오류가 무시되었습니다",
"liquidIntroduction": "템플릿 생성은 Liquid 템플릿 언어를 통해 이루어집니다. 사용 지침은 {0}을 참조하세요. 사용 가능한 변수는 다음과 같습니다:", "liquidIntroduction": "템플릿은 Liquid 템플릿 언어를 통해 생성됩니다. 사용법은 {0}을 참조하세요. 사용 가능한 변수는 다음과 같습니다:",
"templateMsg": "알림 메시지", "templateMsg": "알림 메시지",
"templateLimitedToUpDownCertNotifications": "업/다운/인증서 만료 알림에만 사용 가능", "templateLimitedToUpDownCertNotifications": "온라인/오프라인/인증서 만료 알림에만 사용 가능",
"templateLimitedToUpDownNotifications": "UP/DOWN 알림에만 사용 가능", "templateLimitedToUpDownNotifications": "온라인/오프라인 알림에만 사용 가능",
"webhookBodyPresetOption": "프리셋 - {0}", "webhookBodyPresetOption": "사전 설정 - {0}",
"successKeyword": "성공 키워드", "successKeyword": "성공 키워드",
"successKeywordExplanation": "성공으로 간주되는 MQTT 키워드", "successKeywordExplanation": "성공으로 간주 MQTT 키워드",
"Reset Token": "토큰 초기화", "Reset Token": "토큰 초기화",
"Check/Uncheck": "체크/체크 해제", "Check/Uncheck": "체크/체크 해제",
"pushViewCode": "푸시 모니터는 어떻게 사용하나요? (코드 보기)", "pushViewCode": "푸시 모니터는 어떻게 사용하나요? (코드 보기)",
"Search monitored sites": "모니터링중인 사이트 검색", "Search monitored sites": "모니터링 중인 사이트 검색",
"templateHeartbeatJSON": "heartbeat를 설명하는 오브젝트", "templateHeartbeatJSON": "하트비트를 설명하는 오브젝트",
"shrinkDatabaseDescriptionSqlite": "SQLite 데이터베이스에서 {vacuum} 명령을 실행해요. {auto_vacuum}이 이미 활성화되어 있지만, {auto_vacuum}은 {vacuum}이 하는 것처럼 데이터베이스를 조각 모음 하거나 페이지를 다시 압축하지는 않아요.", "shrinkDatabaseDescriptionSqlite": "SQLite 데이터베이스에 대해 {vacuum}을(를) 트리거합니다. {auto_vacuum}이 이미 활성화되어 있지만, 이는 데이터베이스를 조각 모음하거나 {vacuum} 명령어처럼 개별 데이터베이스 페이지를 다시 정리하지는 않습니다.",
"statusPageSpecialSlugDesc": "특별한 주소 {0}: 아무런 주소도 입력되지 않으면 이 페이지가 보여요", "statusPageSpecialSlugDesc": "특별한 주소 {0}: 아무런 주소도 입력되지 않으면 이 페이지가 보여요",
"Add a new expiry notification day": "새 만료 알림 날짜 추가", "Add a new expiry notification day": "새 만료 알림 날짜 추가",
"Refresh Interval Description": "이 상태 페이지는 {0}초마다 완전 새로고침(F5) 돼요", "Refresh Interval Description": "이 상태 페이지는 {0}초마다 완전 새로고침(F5) 돼요",
"telegramServerUrlDescription": "텔레그램 봇 API의 제한을 해제하거나, 차단된 지역(중국, 이란 등)에서 액세스하려면 {0}을 클릭하세요. 기본값: {1}", "telegramServerUrlDescription": "텔레그램 봇 API의 제한을 해제하거나, 차단된 지역(중국, 이란 등)에서 액세스하려면 {0}을 클릭하세요. 기본값: {1}",
"chromeExecutableDescription": "Docker 사용자의 경우, Chromium이 아직 설치되지 않았다면 이를 설치하고 테스트 결과를 표시하는 데 몇 분이 걸릴 수 있어요. 1GB의 디스크 공간을 사용해요.", "chromeExecutableDescription": "Docker 사용자의 경우, Chromium이 아직 설치되지 않았다면 이를 설치하고 테스트 결과를 표시하는 데 몇 분이 걸릴 수 있어요. 1GB의 디스크 공간을 사용해요.",
"templateMonitorJSON": "monitor를 설명하는 오브젝트", "templateMonitorJSON": "모니터를 설명하는 오브젝트",
"webhookBodyCustomOption": "커스텀 Body", "webhookBodyCustomOption": "사용자 지정 본문 (Body)",
"telegramServerUrl": "(선택) 서버 URL", "telegramServerUrl": "(선택) 서버 URL",
"and": "그리고", "and": "그리고",
"emailCustomisableContent": "사용자 지정 가능한 콘텐츠", "emailCustomisableContent": "사용자 지정 가능한 콘텐츠",
@ -815,15 +815,15 @@
"leave blank for default subject": "기본값을 사용하려면 비워두세요", "leave blank for default subject": "기본값을 사용하려면 비워두세요",
"emailCustomBody": "커스텀 Body", "emailCustomBody": "커스텀 Body",
"leave blank for default body": "기본값을 사용하려면 비워두세요", "leave blank for default body": "기본값을 사용하려면 비워두세요",
"templateServiceName": "서비스 이름", "templateServiceName": "서비스",
"templateHostnameOrURL": "호스트명 또는 URL", "templateHostnameOrURL": "호스트명 또는 URL",
"templateStatus": "상태", "templateStatus": "상태",
"selectedMonitorCount": "선택됨: {0}", "selectedMonitorCount": "선택됨: {0}",
"Remove the expiry notification": "만료 알림 날짜 제거", "Remove the expiry notification": "만료 알림 날짜 제거",
"Refresh Interval": "새로고침 주기", "Refresh Interval": "새로고침 주기",
"noDockerHostMsg": "사용할 수 없습니다. 먼저 도커 호스트를 설정하세요.", "noDockerHostMsg": "사용할 수 없습니다. 먼저 도커 호스트를 설정하세요.",
"DockerHostRequired": "이 모니터링을 위한 도커 호스트를 설정해 주세요.", "DockerHostRequired": "이 모니터를 위한 Docker 호스트를 설정해 주세요.",
"tailscalePingWarning": "Tailscale Ping 모니터링을 사용하려면 Docker 없이 Uptime Kuma를 설치하고 서버에 Tailscale 클라이언트도 설치해야 합니다.", "tailscalePingWarning": "Tailscale Ping 모니터를 사용하려면 Docker를 사용하지 않고 Uptime Kuma를 설치해야 하며, 서버에 Tailscale 클라이언트도 설치해야 합니다.",
"telegramUseTemplate": "커스텀 메시지 템플릿 사용", "telegramUseTemplate": "커스텀 메시지 템플릿 사용",
"telegramUseTemplateDescription": "활성화하면 메시지를 보낼 때 커스텀 템플릿을 사용해요.", "telegramUseTemplateDescription": "활성화하면 메시지를 보낼 때 커스텀 템플릿을 사용해요.",
"telegramTemplateFormatDescription": "텔레그램은 메시지에 다양한 마크업 언어를 사용할 수 있어요. 자세한 내용은 텔레그램 {0}을 참조하세요.", "telegramTemplateFormatDescription": "텔레그램은 메시지에 다양한 마크업 언어를 사용할 수 있어요. 자세한 내용은 텔레그램 {0}을 참조하세요.",
@ -834,5 +834,15 @@
"Select message type": "메시지 유형 선택", "Select message type": "메시지 유형 선택",
"Send to channel": "채널로 전송", "Send to channel": "채널로 전송",
"Create new forum post": "새 포럼 게시물 만들기", "Create new forum post": "새 포럼 게시물 만들기",
"Your User ID": "사용자 ID" "Your User ID": "사용자 ID",
"emailTemplateMonitorJSON": "모니터를 설명하는 객체",
"postToExistingThread": "기존 스레드/포럼 게시물에 게시",
"forumPostName": "포럼 게시물 이름",
"threadForumPostID": "스레드 / 포럼 게시물 ID",
"e.g. {discordThreadID}": "예: {discordThreadID}",
"whatHappensAtForumPost": "새 포럼 게시물을 만드세요. 기존 게시물에는 메시지가 게시되지 않습니다. 기존 게시물에 게시하려면 \"{option}\"을 사용하세요",
"wayToGetDiscordThreadId": "스레드/포럼 게시물 ID를 얻는 것은 채널 ID를 얻는 것과 비슷합니다. ID를 얻는 방법에 대해 자세히 알아보세요. {0}",
"Channel access token (Long-lived)": "채널 액세스 토큰(장기)",
"invertKeywordDescription": "키워드가 존재하지 않는지 살펴보세요.",
"emailTemplateLimitedToUpDownNotification": "UP/DOWN 하트비트에만 사용 가능, 그렇지 않으면 null"
} }

View file

@ -1108,5 +1108,12 @@
"templateStatus": "status", "templateStatus": "status",
"telegramUseTemplate": "Gebruik aangepaste bericht sjabloon", "telegramUseTemplate": "Gebruik aangepaste bericht sjabloon",
"telegramTemplateFormatDescription": "Telegram staat het gebruik van verschillende opmaaktalen voor berichten toe, zie Telegram {0} voor specifieke details.", "telegramTemplateFormatDescription": "Telegram staat het gebruik van verschillende opmaaktalen voor berichten toe, zie Telegram {0} voor specifieke details.",
"telegramUseTemplateDescription": "Indien ingeschakeld, wordt het bericht verzonden met een aangepaste sjabloon." "telegramUseTemplateDescription": "Indien ingeschakeld, wordt het bericht verzonden met een aangepaste sjabloon.",
"Font Twemoji by Twitter licensed under": "Lettertype Twemoji van Twitter gelicentieerd onder",
"the smsplanet documentation": "de smsplanet documentatie",
"Phone numbers": "Telefoonnummers",
"Sender name": "Naam afzender",
"smsplanetNeedToApproveName": "Moet worden goedgekeurd in het clientpaneel",
"smsplanetApiToken": "Token voor de SMSPlanet API",
"smsplanetApiDocs": "Gedetailleerde informatie over het verkrijgen van API-tokens vindt u op {the_smsplanet_documentation}."
} }

View file

@ -1117,5 +1117,12 @@
"templateStatus": "status", "templateStatus": "status",
"telegramUseTemplate": "Użyj niestandardowego szablonu wiadomości", "telegramUseTemplate": "Użyj niestandardowego szablonu wiadomości",
"telegramUseTemplateDescription": "Jeśli opcja ta jest włączona, wiadomość zostanie wysłana przy użyciu niestandardowego szablonu.", "telegramUseTemplateDescription": "Jeśli opcja ta jest włączona, wiadomość zostanie wysłana przy użyciu niestandardowego szablonu.",
"telegramTemplateFormatDescription": "Telegram pozwala na używanie różnych języków znaczników dla wiadomości, zobacz Telegram {0}, aby uzyskać szczegółowe informacje." "telegramTemplateFormatDescription": "Telegram pozwala na używanie różnych języków znaczników dla wiadomości, zobacz Telegram {0}, aby uzyskać szczegółowe informacje.",
"Font Twemoji by Twitter licensed under": "Czcionka Twemoji autorstwa Twitter na licencji",
"smsplanetApiToken": "Token dla API SMSPlanet",
"smsplanetApiDocs": "Szczegółowe informacje na temat uzyskiwania tokenów API można znaleźć w {the_smsplanet_documentation}.",
"the smsplanet documentation": "dokumentacja smsplanet",
"Phone numbers": "Numery telefonów",
"Sender name": "Nazwa nadawcy",
"smsplanetNeedToApproveName": "Wymaga zatwierdzenia w panelu klienta"
} }

View file

@ -978,7 +978,7 @@
"Add Remote Browser": "Adicionar Navegador Remoto", "Add Remote Browser": "Adicionar Navegador Remoto",
"New Group": "Novo Grupo", "New Group": "Novo Grupo",
"Group Name": "Nome do Grupo", "Group Name": "Nome do Grupo",
"OAuth2: Client Credentials": "OAuth2: Client Credentials", "OAuth2: Client Credentials": "OAuth2: Credenciais do Cliente",
"Authentication Method": "Método de Autenticação", "Authentication Method": "Método de Autenticação",
"Authorization Header": "Header de Autorização", "Authorization Header": "Header de Autorização",
"ignoredTLSError": "Erros TLS/SSL foram ignorados", "ignoredTLSError": "Erros TLS/SSL foram ignorados",
@ -1082,5 +1082,11 @@
"telegramServerUrl": "(Opcional) URL do Servidor", "telegramServerUrl": "(Opcional) URL do Servidor",
"Message Template": "Modelo de Mensagem", "Message Template": "Modelo de Mensagem",
"Template Format": "Formato do Modelo", "Template Format": "Formato do Modelo",
"Font Twemoji by Twitter licensed under": "Fonte Twemoji do Twitter licenciada sob" "Font Twemoji by Twitter licensed under": "Fonte Twemoji do Twitter licenciada sob",
"the smsplanet documentation": "a documentação do smsplanet",
"Phone numbers": "Números de telefone",
"Sender name": "Nome do remetente",
"smsplanetNeedToApproveName": "Precisa ser aprovado no painel do cliente",
"smsplanetApiToken": "Token para a API SMSPlanet",
"smsplanetApiDocs": "Informações detalhadas sobre a obtenção de tokens de API podem ser encontradas em {the_smsplanet_documentation}."
} }

View file

@ -1,10 +1,10 @@
{ {
"languageName": "Русский", "languageName": "Английский",
"checkEverySecond": "Проверка каждые {0} секунд", "checkEverySecond": "Проверка каждые {0} секунд",
"retriesDescription": "Максимальное количество попыток перед пометкой сервиса, как недоступного, и отправкой уведомления", "retriesDescription": "Максимальное число попыток перед тем, как сервис будет помечен как неработающий и будет отправлено уведомление",
"ignoreTLSError": "Игнорировать ошибки TLS/SSL для HTTPS сайтов", "ignoreTLSError": "Игнорировать ошибки TLS/SSL для сайтов с HTTPS",
"upsideDownModeDescription": "Инверсия статуса. Если сервис доступен, он будет отмечен как недоступный.", "upsideDownModeDescription": "Инвертировать статус. Если сервис доступен — он считается НЕРАБОТАЮЩИМ.",
"maxRedirectDescription": "Максимальное количество перенаправлений. Поставьте 0, чтобы отключить перенаправления.", "maxRedirectDescription": "Максимальное число перенаправлений. Установите 0, чтобы отключить перенаправления.",
"acceptedStatusCodesDescription": "Выберите коды статусов для определения доступности сервиса.", "acceptedStatusCodesDescription": "Выберите коды статусов для определения доступности сервиса.",
"passwordNotMatchMsg": "Введённые пароли не совпадают.", "passwordNotMatchMsg": "Введённые пароли не совпадают.",
"notificationDescription": "Необходимо привязать уведомления к монитору чтобы они функционировали.", "notificationDescription": "Необходимо привязать уведомления к монитору чтобы они функционировали.",
@ -26,8 +26,8 @@
"Check Update On GitHub": "Проверить наличие обновления в GitHub", "Check Update On GitHub": "Проверить наличие обновления в GitHub",
"List": "Список", "List": "Список",
"Add": "Добавить", "Add": "Добавить",
"Add New Monitor": "Добавить Новый Сенсор", "Add New Monitor": "Новый монитор",
"Quick Stats": "Статистика", "Quick Stats": "Сводка",
"Up": "Доступен", "Up": "Доступен",
"Down": "Не доступен", "Down": "Не доступен",
"Pending": "В ожидании", "Pending": "В ожидании",
@ -43,7 +43,7 @@
"Delete": "Удалить", "Delete": "Удалить",
"Current": "Текущий", "Current": "Текущий",
"Uptime": "Время безотказной работы", "Uptime": "Время безотказной работы",
"Cert Exp.": "Сертификат ист.", "Cert Exp.": "Срок SSL",
"day": "день | дней", "day": "день | дней",
"-day": "-дней", "-day": "-дней",
"hour": "час", "hour": "час",
@ -56,7 +56,7 @@
"URL": "URL-ссылка", "URL": "URL-ссылка",
"Hostname": "Имя хоста", "Hostname": "Имя хоста",
"Port": "Порт", "Port": "Порт",
"Heartbeat Interval": "Частота опроса", "Heartbeat Interval": "Интервал опроса",
"Retries": "Попыток", "Retries": "Попыток",
"Advanced": "Дополнительно", "Advanced": "Дополнительно",
"Upside Down Mode": "Режим инверсии статуса", "Upside Down Mode": "Режим инверсии статуса",
@ -208,10 +208,10 @@
"pushbullet": "Pushbullet", "pushbullet": "Pushbullet",
"line": "Line Messenger", "line": "Line Messenger",
"mattermost": "Mattermost", "mattermost": "Mattermost",
"Primary Base URL": "Основной URL", "Primary Base URL": "Основной URL, по которому доступен Uptime Kuma",
"Push URL": "URL-ссылка push уведомлений", "Push URL": "URL-ссылка push уведомлений",
"needPushEvery": "К этому URL необходимо обращаться каждые {0} секунд.", "needPushEvery": "К этому URL необходимо обращаться каждые {0} секунд.",
"pushOptionalParams": "Опциональные параметры: {0}", "pushOptionalParams": "Необязательные параметры: {0}",
"defaultNotificationName": "Уведомления {notification} ({number})", "defaultNotificationName": "Уведомления {notification} ({number})",
"here": "здесь", "here": "здесь",
"Required": "Обязательно", "Required": "Обязательно",
@ -689,7 +689,7 @@
"Guild ID": "Идентификатор гильдии", "Guild ID": "Идентификатор гильдии",
"Kook": "Kook", "Kook": "Kook",
"wayToGetKookBotToken": "Создайте приложение и получите токен бота по адресу {0}", "wayToGetKookBotToken": "Создайте приложение и получите токен бота по адресу {0}",
"Resend Notification if Down X times consecutively": "Повторная отправка уведомления при неудачном запросе X раз", "Resend Notification if Down X times consecutively": "Повторно отправлять уведомление, если сбой произошёл X раз подряд",
"telegramProtectContent": "Запретить пересылку/сохранение", "telegramProtectContent": "Запретить пересылку/сохранение",
"telegramProtectContentDescription": "Если включено, сообщения бота в Telegram будут запрещены для пересылки и сохранения.", "telegramProtectContentDescription": "Если включено, сообщения бота в Telegram будут запрещены для пересылки и сохранения.",
"telegramSendSilently": "Отправить без звука", "telegramSendSilently": "Отправить без звука",
@ -786,7 +786,7 @@
"Badge Label Suffix": "Суффикс надписи для значка", "Badge Label Suffix": "Суффикс надписи для значка",
"Edit Maintenance": "Редактировать техобслуживание", "Edit Maintenance": "Редактировать техобслуживание",
"Reconnecting...": "Переподключение...", "Reconnecting...": "Переподключение...",
"Cannot connect to the socket server": "Невозможно подключиться к серверу", "Cannot connect to the socket server": "Не удаётся подключиться к сокет-серверу",
"Badge Warn Color": "Цвет значка для предупреждения", "Badge Warn Color": "Цвет значка для предупреждения",
"Badge Warn Days": "Значок для \"дней предупреждения\"", "Badge Warn Days": "Значок для \"дней предупреждения\"",
"Badge Down Days": "Значок для \"дней недоступности\"", "Badge Down Days": "Значок для \"дней недоступности\"",
@ -829,7 +829,7 @@
"PushDeer Server": "Сервер PushDeer", "PushDeer Server": "Сервер PushDeer",
"pushDeerServerDescription": "Оставьте пустым для использования официального сервера", "pushDeerServerDescription": "Оставьте пустым для использования официального сервера",
"showCertificateExpiry": "Показывать истекающий сертификат", "showCertificateExpiry": "Показывать истекающий сертификат",
"Request Timeout": "Тайм-Аут запроса", "Request Timeout": "Таймаут ожидания",
"timeoutAfter": "Тайм-Аут через {0} секунд", "timeoutAfter": "Тайм-Аут через {0} секунд",
"Select": "Выбрать", "Select": "Выбрать",
"selectedMonitorCount": "Выбрано: {0}", "selectedMonitorCount": "Выбрано: {0}",
@ -850,12 +850,12 @@
"nostrRelaysHelp": "Один URL-адрес ретрансляции в каждой строке", "nostrRelaysHelp": "Один URL-адрес ретрансляции в каждой строке",
"enableNSCD": "Включить NSCD (Name Service Cache Daemon) для кэширования всех DNS-запросов", "enableNSCD": "Включить NSCD (Name Service Cache Daemon) для кэширования всех DNS-запросов",
"Saved.": "Сохранено.", "Saved.": "Сохранено.",
"setupDatabaseChooseDatabase": "Какую базу данных Вы бы хотели использовать?", "setupDatabaseChooseDatabase": "Какую базу данных вы хотите использовать?",
"setupDatabaseEmbeddedMariaDB": "Вам не нужно ничего настраивать. В этот докер-образ автоматически встроена и настроена MariaDB. Uptime Kuma будет подключаться к этой базе данных через unix-сокет.", "setupDatabaseEmbeddedMariaDB": "Ничего настраивать не нужно. Этот образ Docker уже содержит встроенную и настроенную MariaDB. Uptime Kuma будет подключаться к базе данных через Unix-сокет.",
"setupDatabaseSQLite": "Простой файл базы данных, рекомендуемый для небольших развертываний. До версии 2.0.0 Uptime Kuma использовал SQLite в качестве базы данных по умолчанию.", "setupDatabaseSQLite": "Простой файл базы данных, рекомендуется для небольших установок. До версии 2.0.0 Uptime Kuma использовал SQLite в качестве базы данных по умолчанию.",
"setupDatabaseMariaDB": "Подключитесь к внешней базе данных MariaDB. Необходимо задать информацию о подключении к базе данных.", "setupDatabaseMariaDB": "Подключение к внешней базе данных MariaDB. Необходимо указать информацию для подключения.",
"dbName": "Имя базы данных", "dbName": "Имя базы данных",
"pushViewCode": "Как использовать монитор Push? (Посмотреть код)", "pushViewCode": "Как настроить Push-монитор? (Показать код)",
"programmingLanguages": "Языки программирования", "programmingLanguages": "Языки программирования",
"Bark API Version": "Версия Bark API", "Bark API Version": "Версия Bark API",
"monitorToastMessagesDescription": "Уведомления для мониторов исчезают через заданное время в секундах. Значение -1 отключает тайм-аут. Значение 0 отключает уведомления.", "monitorToastMessagesDescription": "Уведомления для мониторов исчезают через заданное время в секундах. Значение -1 отключает тайм-аут. Значение 0 отключает уведомления.",
@ -914,7 +914,7 @@
"Add a Remote Browser": "Добавить удаленный браузер", "Add a Remote Browser": "Добавить удаленный браузер",
"Remote Browser not found!": "Удаленный браузер не найден!", "Remote Browser not found!": "Удаленный браузер не найден!",
"remoteBrowsersDescription": "Удаленные браузеры — альтернатива локальному запуску Chromium. Установите такой сервис, как browserless.io, или подключитесь к своему собственному", "remoteBrowsersDescription": "Удаленные браузеры — альтернатива локальному запуску Chromium. Установите такой сервис, как browserless.io, или подключитесь к своему собственному",
"settingUpDatabaseMSG": "Настраиваем базу данных. Это может занять некоторое время, пожалуйста подождите.", "settingUpDatabaseMSG": "Настройка базы данных. Это может занять некоторое время, пожалуйста, подождите.",
"setup a new monitor group": "настроить новую группу мониторов", "setup a new monitor group": "настроить новую группу мониторов",
"openModalTo": "открыть модальное окно {0}", "openModalTo": "открыть модальное окно {0}",
"Add a domain": "Добавить домен", "Add a domain": "Добавить домен",
@ -929,8 +929,8 @@
"Mention group": "Упомянуть {group}", "Mention group": "Упомянуть {group}",
"Your User ID": "Ваш идентификатор пользователя", "Your User ID": "Ваш идентификатор пользователя",
"Host URL": "URL Хоста", "Host URL": "URL Хоста",
"locally configured mail transfer agent": "Настроенный локально агент передачи почты", "locally configured mail transfer agent": "локальный почтовый агент",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Введите {Hostname} сервера, к которому вы хотите подключиться, либо {localhost}, если вы собираетесь использовать {local_mta}", "Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Укажите имя хоста сервера, к которому хотите подключиться, или {localhost}, если планируете использовать {local_mta}",
"wayToGetHeiiOnCallDetails": "Как получить ID триггера и ключи API , рассказывается в {documentation}", "wayToGetHeiiOnCallDetails": "Как получить ID триггера и ключи API , рассказывается в {documentation}",
"gtxMessagingApiKeyHint": "Вы можете найти свой ключ API на странице: Мои учетные записи маршрутизации > Показать информацию об учетной записи > Учетные данные API > REST API (v2.x)", "gtxMessagingApiKeyHint": "Вы можете найти свой ключ API на странице: Мои учетные записи маршрутизации > Показать информацию об учетной записи > Учетные данные API > REST API (v2.x)",
"From Phone Number / Transmission Path Originating Address (TPOA)": "Номер телефона / Адрес источника пути передачи (АИПП)", "From Phone Number / Transmission Path Originating Address (TPOA)": "Номер телефона / Адрес источника пути передачи (АИПП)",
@ -978,7 +978,7 @@
"Refresh Interval Description": "Страница статуса будет полностью обновлена каждые {0} секунд", "Refresh Interval Description": "Страница статуса будет полностью обновлена каждые {0} секунд",
"and": "и", "and": "и",
"e.g. {discordThreadID}": "например {discordThreadID}", "e.g. {discordThreadID}": "например {discordThreadID}",
"ignoredTLSError": "Ошибки TLS/SSL проигнорированы", "ignoredTLSError": "TLS/SSL ошибки не учитываются",
"Debug": "Отладка", "Debug": "Отладка",
"Copy": "Скопировать", "Copy": "Скопировать",
"CopyToClipboardError": "Не удалось скопировать: {error}", "CopyToClipboardError": "Не удалось скопировать: {error}",
@ -1110,20 +1110,27 @@
"templateServiceName": "имя сервиса", "templateServiceName": "имя сервиса",
"templateHostnameOrURL": "hostname или URL", "templateHostnameOrURL": "hostname или URL",
"templateStatus": "статус", "templateStatus": "статус",
"telegramServerUrlDescription": "Чтобы поднять ограничения API API Telegram или получить доступ к заблокированным районам (Китай, Иран и т.д.). Для получения дополнительной информации нажмите {0}. По умолчанию: {1}", "telegramServerUrlDescription": "Чтобы обойти ограничения API бота Telegram или получить доступ в заблокированных регионах (например, в Китае или Иране), нажмите {0} для получения подробной информации. Значение по умолчанию: {1}",
"wayToGetWahaApiKey": "Ключ API - это значение переменной среды WHATSAPP_API_KEY, которое вы использовали для запуска WAHA.", "wayToGetWahaApiKey": "Ключ API — это значение переменной окружения WHATSAPP_API_KEY, которое вы использовали для запуска WAHA.",
"wayToGetWahaSession": "Из этой сессии WAHA отправляет уведомления на удостоверение личности чата. Вы можете найти его на приборной панели Waha.", "wayToGetWahaSession": "Из этой сессии WAHA отправляет уведомления на удостоверение личности чата. Вы можете найти его на приборной панели Waha.",
"wayToWriteWahaChatId": "Номер телефона с международным префиксом, но без знака плюс в начале ({0}), идентификатор контакта ({1}) или идентификатора группы ({2}). Уведомления отправляются на этот идентификатор чата от сеанса Waha.", "wayToWriteWahaChatId": "Номер телефона с международным префиксом, но без знака плюс в начале ({0}), идентификатор контакта ({1}) или идентификатора группы ({2}). Уведомления отправляются на этот идентификатор чата от сеанса Waha.",
"wahaSession": "Сессия", "wahaSession": "Сессия",
"wahaChatId": "Идентификатор чата (номер телефона / идентификатор контакта / идентификатор группы)", "wahaChatId": "Идентификатор чата (номер телефона / идентификатор контакта / идентификатор группы)",
"wayToGetWahaApiUrl": "Ваш экземпляр WAHA URL.", "wayToGetWahaApiUrl": "Ваш экземпляр WAHA URL.",
"YZJ Webhook URL": "YZJ Вебхук URL", "YZJ Webhook URL": "URL вебхука YZJ",
"YZJ Robot Token": "YZJ Токен Робота", "YZJ Robot Token": "YZJ Токен Робота",
"telegramServerUrl": "(Необязательно) URL Сервера", "telegramServerUrl": "(Необязательно) URL Сервера",
"telegramUseTemplate": "Используйте пользовательский шаблон сообщения", "telegramUseTemplate": "Используйте пользовательский шаблон сообщения",
"telegramUseTemplateDescription": "Если включено, сообщение будет отправлено с помощью пользовательского шаблона.", "telegramUseTemplateDescription": "Если включено, сообщение будет отправлено с помощью пользовательского шаблона.",
"telegramTemplateFormatDescription": "Telegram позволяет использовать различные языки разметки для сообщений, см. Telegram {0} для конкретных деталей.", "telegramTemplateFormatDescription": "Telegram позволяет использовать различные языки разметки в сообщениях. Подробности смотрите в документации Telegram — {0}.",
"Plain Text": "Простой текст", "Plain Text": "Простой текст",
"Message Template": "Шаблон сообщения", "Message Template": "Шаблон сообщения",
"Template Format": "Формат шаблона" "Template Format": "Формат шаблона",
"Font Twemoji by Twitter licensed under": "Шрифт Twemoji от Twitter лицензирован на условиях",
"smsplanetApiToken": "Токен для API SMSPlanet",
"smsplanetApiDocs": "Подробную информацию о получении API-токенов можно найти в {the_smsplanet_documentation}.",
"the smsplanet documentation": "документация SMSPlanet",
"Phone numbers": "Номера телефонов",
"Sender name": "Имя отправителя",
"smsplanetNeedToApproveName": "Требуется одобрение в панели клиента"
} }

View file

@ -28,20 +28,20 @@
"confirmDisableTwoFAMsg": "คุณแน่ใจหรือไม่ที่จะปิดใช้งาน 2FA?", "confirmDisableTwoFAMsg": "คุณแน่ใจหรือไม่ที่จะปิดใช้งาน 2FA?",
"Settings": "การตั้งค่า", "Settings": "การตั้งค่า",
"Dashboard": "แผงควบคุม", "Dashboard": "แผงควบคุม",
"New Update": "อัพเดทใหม่", "New Update": "อัปเดตใหม่",
"Language": "ภาษา", "Language": "ภาษา",
"Appearance": "หน้าตา", "Appearance": "ลักษณะการแสดงผล",
"Theme": "ธีม", "Theme": "ธีม",
"General": "ทั่วไป", "General": "ทั่วไป",
"Primary Base URL": "URL หลัก", "Primary Base URL": "URL หลัก",
"Version": "เวอร์ชัน", "Version": "เวอร์ชัน",
"Check Update On GitHub": "ตรวจสอบการอัปเดตบน GitHub", "Check Update On GitHub": "ตรวจสอบการอัปเดตบน GitHub",
"List": "รายการ", "List": "รายการ",
"Add": "เพิ่ม", "Add": "เพิ่ม",
"Add New Monitor": "เพิ่มมอนิเตอร์ใหม่", "Add New Monitor": "เพิ่มมอนิเตอร์ใหม่",
"Quick Stats": "สถิติอย่างย่อ", "Quick Stats": "สรุปสถานะ",
"Up": "ใช้งานได้", "Up": "ทำงานปกติ",
"Down": "ไม่สามารถใช้งานได้", "Down": "ทำงานล้มเหลว",
"Pending": "รอดำเนินการ", "Pending": "รอดำเนินการ",
"Unknown": "ไม่ทราบ", "Unknown": "ไม่ทราบ",
"Pause": "หยุดชั่วคราว", "Pause": "หยุดชั่วคราว",
@ -103,7 +103,7 @@
"Enable Auth": "เปิดใช้งานการตรวจสอบสิทธิ์", "Enable Auth": "เปิดใช้งานการตรวจสอบสิทธิ์",
"disableauth.message1": "คุณต้องการที่จะ {disableAuth}?", "disableauth.message1": "คุณต้องการที่จะ {disableAuth}?",
"disable authentication": "ปิดใช้งานระบบรับรองความถูกต้องใช่หรือไม่", "disable authentication": "ปิดใช้งานระบบรับรองความถูกต้องใช่หรือไม่",
"disableauth.message2": "ระบบนี้ถูกออกแบบมาเพื่อการใช้งานกับระบบรับรองความถูกต้องของบุคคลที่สามเช่น Cloudflare Access, Authelia หรือวิธีการอื่นๆ", "disableauth.message2": "ถูกออกแบบมาสำหรับกรณีที่มี {intendThirdPartyAuth} อยู่หน้าระบบ Uptime Kuma เช่น Cloudflare Access, Authelia หรือกลไกการตรวจสอบสิทธิ์อื่น ๆ",
"Please use this option carefully!": "โปรดใช้ความระมัดระวังในการเลือกใช้งานระบบนี้ !", "Please use this option carefully!": "โปรดใช้ความระมัดระวังในการเลือกใช้งานระบบนี้ !",
"Logout": "ออกจากระบบ", "Logout": "ออกจากระบบ",
"Leave": "ออก", "Leave": "ออก",
@ -300,7 +300,7 @@
"Internal Room Id": "รหัสห้องภายใน", "Internal Room Id": "รหัสห้องภายใน",
"matrixDesc1": "คุณค้นหารหัสห้องภายในได้โดยดูในส่วนขั้นสูงของการตั้งค่าห้องในไคลเอ็นต์ Matrix มันควรจะมีลักษณะเช่น !PMdRCpsIfLwsfjIye6:kiznick.server.", "matrixDesc1": "คุณค้นหารหัสห้องภายในได้โดยดูในส่วนขั้นสูงของการตั้งค่าห้องในไคลเอ็นต์ Matrix มันควรจะมีลักษณะเช่น !PMdRCpsIfLwsfjIye6:kiznick.server.",
"matrixDesc2": "ขอแนะนำเป็นอย่างยิ่งให้คุณสร้างผู้ใช้ใหม่และอย่าใช้โทเค็นการเข้าถึงของผู้ใช้ Matrix ของคุณเอง เนื่องจากจะทำให้สามารถเข้าถึงบัญชีของคุณและห้องทั้งหมดที่คุณเข้าร่วม ให้สร้างผู้ใช้ใหม่และเชิญเฉพาะห้องที่คุณต้องการรับการแจ้งเตือนแทน คุณสามารถรับโทเค็นเพื่อการเข้าถึงได้โดยเรียกใช้ {0}", "matrixDesc2": "ขอแนะนำเป็นอย่างยิ่งให้คุณสร้างผู้ใช้ใหม่และอย่าใช้โทเค็นการเข้าถึงของผู้ใช้ Matrix ของคุณเอง เนื่องจากจะทำให้สามารถเข้าถึงบัญชีของคุณและห้องทั้งหมดที่คุณเข้าร่วม ให้สร้างผู้ใช้ใหม่และเชิญเฉพาะห้องที่คุณต้องการรับการแจ้งเตือนแทน คุณสามารถรับโทเค็นเพื่อการเข้าถึงได้โดยเรียกใช้ {0}",
"Method": "วิธี", "Method": "เมทอด",
"Body": "เนื้อหา", "Body": "เนื้อหา",
"Headers": "ส่วนหัว", "Headers": "ส่วนหัว",
"PushUrl": "Push URL", "PushUrl": "Push URL",
@ -380,7 +380,7 @@
"alertaApiKey": "กุญแจ API", "alertaApiKey": "กุญแจ API",
"alertaAlertState": "แจ้งเตือนสถานะ", "alertaAlertState": "แจ้งเตือนสถานะ",
"alertaRecoverState": "กู้คืนสถานะ", "alertaRecoverState": "กู้คืนสถานะ",
"deleteStatusPageMsg": "คุณแน่ใจหรือไม่ว่าต้องการลบหน้าสถานะนี้", "deleteStatusPageMsg": "คุณแน่ใจหรือไม่ว่าต้องการลบหน้าสถานะนี้?",
"Proxies": "พร็อกซี", "Proxies": "พร็อกซี",
"default": "ค่าเริ่มต้น", "default": "ค่าเริ่มต้น",
"enabled": "เปิดใช้งานแล้ว", "enabled": "เปิดใช้งานแล้ว",
@ -462,12 +462,12 @@
"PushDeer Key": "กุญแจ PushDeer", "PushDeer Key": "กุญแจ PushDeer",
"Footer Text": "ข้อความส่วนท้าย", "Footer Text": "ข้อความส่วนท้าย",
"Show Powered By": "แสดงข้อความ \"ขับเคลื่อนโดย\"", "Show Powered By": "แสดงข้อความ \"ขับเคลื่อนโดย\"",
"Domain Names": "Domain Names", "Domain Names": "ชื่อโดเมน",
"signedInDisp": "เข้าใช้งานในฐานะ {0}", "signedInDisp": "เข้าใช้งานในฐานะ {0}",
"signedInDispDisabled": "ปิดการยืนยันตัวตน", "signedInDispDisabled": "ปิดการยืนยันตัวตน",
"Certificate Expiry Notification": "แจ้งเตือนใบรับรองหมดอายุ", "Certificate Expiry Notification": "แจ้งเตือนใบรับรองหมดอายุ",
"API Username": "API Username", "API Username": "ชื่อผู้ใช้ของ API",
"API Key": "API Key", "API Key": "API คีย์",
"Recipient Number": "หมายเลขผู้รับ", "Recipient Number": "หมายเลขผู้รับ",
"From Name/Number": "จาก ชื่อ / หมายเลข", "From Name/Number": "จาก ชื่อ / หมายเลข",
"Leave blank to use a shared sender number.": "ไม่ต้องกรอกเพื่อใช้ชื่อผู้ส่งร่วมกัน", "Leave blank to use a shared sender number.": "ไม่ต้องกรอกเพื่อใช้ชื่อผู้ส่งร่วมกัน",
@ -477,32 +477,32 @@
"octopushAPIKey": "\"API key\" จากข้อมูลยืนยันตัวตน HTTP API ในแผงควบคุม", "octopushAPIKey": "\"API key\" จากข้อมูลยืนยันตัวตน HTTP API ในแผงควบคุม",
"octopushLogin": "\"Login\" จากข้อมูลยืนยันตัวตน HTTP API ในแผงควบคุม", "octopushLogin": "\"Login\" จากข้อมูลยืนยันตัวตน HTTP API ในแผงควบคุม",
"promosmsLogin": "API Login Name", "promosmsLogin": "API Login Name",
"promosmsPassword": "API Password", "promosmsPassword": "รหัสผ่าน API",
"pushoversounds pushover": "Pushover (default)", "pushoversounds pushover": "Pushover (default)",
"pushoversounds bike": "Bike", "pushoversounds bike": "จักรยาน",
"pushoversounds bugle": "Bugle", "pushoversounds bugle": "บักเกิล",
"pushoversounds cashregister": "Cash Register", "pushoversounds cashregister": "เครื่องคิดเงิน",
"pushoversounds classical": "Classical", "pushoversounds classical": "Classical",
"pushoversounds cosmic": "Cosmic", "pushoversounds cosmic": "คอสมิก",
"pushoversounds falling": "Falling", "pushoversounds falling": "ตก",
"pushoversounds gamelan": "Gamelan", "pushoversounds gamelan": "ระนาด",
"pushoversounds incoming": "Incoming", "pushoversounds incoming": "กำลังมา",
"pushoversounds intermission": "Intermission", "pushoversounds intermission": "ช่วงพัก",
"pushoversounds magic": "Magic", "pushoversounds magic": "แมจิก",
"pushoversounds mechanical": "Mechanical", "pushoversounds mechanical": "เครื่องกล",
"pushoversounds pianobar": "Piano Bar", "pushoversounds pianobar": "เปียโนบาร์",
"pushoversounds siren": "Siren", "pushoversounds siren": "ไซเรน",
"pushoversounds spacealarm": "Space Alarm", "pushoversounds spacealarm": "สัญญาณเตือนอวกาศ",
"pushoversounds tugboat": "Tug Boat", "pushoversounds tugboat": "เรือโยง",
"pushoversounds alien": "Alien Alarm (long)", "pushoversounds alien": "แจ้งเตือน เอเลี่ยน (ยาว)",
"pushoversounds climb": "Climb (long)", "pushoversounds climb": "ไต่เขา (ยาว)",
"pushoversounds persistent": "Persistent (long)", "pushoversounds persistent": "Persistent (long)",
"pushoversounds echo": "Pushover Echo (long)", "pushoversounds echo": "Pushover Echo (long)",
"pushoversounds updown": "Up Down (long)", "pushoversounds updown": "ขึ้นลง (ยาว)",
"pushoversounds vibrate": "Vibrate Only", "pushoversounds vibrate": "สั่นอย่างเดียว",
"pushoversounds none": "None (silent)", "pushoversounds none": "ไม่มี (เงียบ)",
"pushyAPIKey": "Secret API Key", "pushyAPIKey": "Secret API Key",
"pushyToken": "Device token", "pushyToken": "โทเคน ของอุปกรณ์",
"Show update if available": "แสดงการอัปเดตถ้ามี", "Show update if available": "แสดงการอัปเดตถ้ามี",
"Also check beta release": "ตรวจสอบรุ่นเบต้า", "Also check beta release": "ตรวจสอบรุ่นเบต้า",
"Using a Reverse Proxy?": "ใช้ Reverse Proxy อยู่ใช่มั้ย?", "Using a Reverse Proxy?": "ใช้ Reverse Proxy อยู่ใช่มั้ย?",
@ -534,7 +534,7 @@
"Bark Sound": "เสียงประกาศ", "Bark Sound": "เสียงประกาศ",
"Authentication": "การตรวจสอบสิทธิ์", "Authentication": "การตรวจสอบสิทธิ์",
"HTTP Headers": "HTTP Headers", "HTTP Headers": "HTTP Headers",
"Trust Proxy": "Trust Proxy", "Trust Proxy": "เชื่อถือพร็อกซี",
"HomeAssistant": "Home Assistant", "HomeAssistant": "Home Assistant",
"RadiusSecret": "Radius Secret", "RadiusSecret": "Radius Secret",
"RadiusSecretDescription": "แบ่งปันคีย์ลับระหว่างผู้ใช้งานและเซิร์ฟเวอร์", "RadiusSecretDescription": "แบ่งปันคีย์ลับระหว่างผู้ใช้งานและเซิร์ฟเวอร์",
@ -553,9 +553,9 @@
"socket": "Socket", "socket": "Socket",
"tcp": "TCP / HTTP", "tcp": "TCP / HTTP",
"Docker Container": "Docker Container", "Docker Container": "Docker Container",
"Container Name / ID": "Container Name / ID", "Container Name / ID": "ชื่อ / ไอดี ของคอนเทนเนอร์",
"Docker Host": "Docker Host", "Docker Host": "โฮสต์ของ Docker",
"Docker Hosts": "Docker Hosts", "Docker Hosts": "โฮสต์ของ Docker",
"ntfy Topic": "หัวข้อ ntfy", "ntfy Topic": "หัวข้อ ntfy",
"Domain": "โดเมน", "Domain": "โดเมน",
"Workstation": "Workstation", "Workstation": "Workstation",
@ -576,7 +576,7 @@
"Frontend Version": "เวอร์ชั่น Frontend", "Frontend Version": "เวอร์ชั่น Frontend",
"Frontend Version do not match backend version!": "เวอร์ชั่น Frontend ไม่ตรงกับ Backend !", "Frontend Version do not match backend version!": "เวอร์ชั่น Frontend ไม่ตรงกับ Backend !",
"webhookAdditionalHeadersTitle": "Header เพิ่มเติม", "webhookAdditionalHeadersTitle": "Header เพิ่มเติม",
"webhookAdditionalHeadersDesc": "กำหนด Header ที่จะส่งไปหร้อมกับ Webhook", "webhookAdditionalHeadersDesc": "กำหนด Header ที่จะส่งไปหร้อมกับ Webhook โดยแต่ละ header ควรระบุในรูปแบบ key/value แบบ JSON",
"Start of maintenance": "เริ่มการซ่อมบำรุง", "Start of maintenance": "เริ่มการซ่อมบำรุง",
"All Status Pages": "หน้าสถานะทั้งหมด", "All Status Pages": "หน้าสถานะทั้งหมด",
"Custom": "กำหนดเอง", "Custom": "กำหนดเอง",
@ -638,7 +638,7 @@
"lastDay3": "วันที่ 3 สุดท้ายของเดือน", "lastDay3": "วันที่ 3 สุดท้ายของเดือน",
"lastDay4": "วันที่ 4 สุดท้ายของเดือน", "lastDay4": "วันที่ 4 สุดท้ายของเดือน",
"No Maintenance": "ไม่มีการบำรุงรักษา", "No Maintenance": "ไม่มีการบำรุงรักษา",
"pauseMaintenanceMsg": "แน่ใจไหมว่าต้องการหยุดชั่วคราว", "pauseMaintenanceMsg": "แน่ใจไหมว่าต้องการหยุดชั่วคราว?",
"Display Timezone": "แสดงเขตเวลา", "Display Timezone": "แสดงเขตเวลา",
"statusPageMaintenanceEndDate": "จบ", "statusPageMaintenanceEndDate": "จบ",
"Server Timezone": "เขตเวลาเซิร์ฟเวอร์", "Server Timezone": "เขตเวลาเซิร์ฟเวอร์",
@ -647,7 +647,7 @@
"telegramProtectContentDescription": "หากเปิดใช้งาน ข้อความบอทใน Telegram จะได้รับการปกป้องจากการส่งต่อและการบันทึก", "telegramProtectContentDescription": "หากเปิดใช้งาน ข้อความบอทใน Telegram จะได้รับการปกป้องจากการส่งต่อและการบันทึก",
"dnsCacheDescription": "อาจจะทำงานไม่ได้กับ IPv6, ปิดใช้งานถ้าเจอปัญหา", "dnsCacheDescription": "อาจจะทำงานไม่ได้กับ IPv6, ปิดใช้งานถ้าเจอปัญหา",
"IconUrl": "URL ไอคอน", "IconUrl": "URL ไอคอน",
"Enable DNS Cache": "เปิดใช้งาน DNS Cache", "Enable DNS Cache": "(เลิกใช้แล้ว) เปิดใช้งานแคช DNS สำหรับตัวตรวจสอบ HTTP(s)",
"Enable": "เปิดใช้งาน", "Enable": "เปิดใช้งาน",
"Disable": "ปิดใช้งาน", "Disable": "ปิดใช้งาน",
"Single Maintenance Window": "หน้าการปรับปรุงเดี่ยว", "Single Maintenance Window": "หน้าการปรับปรุงเดี่ยว",
@ -676,12 +676,12 @@
"timeoutAfter": "หมดเวลาหลังจาก {0} วินาที", "timeoutAfter": "หมดเวลาหลังจาก {0} วินาที",
"Select": "เลือก", "Select": "เลือก",
"Expected Value": "ค่าที่คาดหวัง", "Expected Value": "ค่าที่คาดหวัง",
"setupDatabaseChooseDatabase": "ฐานข้อมูลไหนที่ต้องการใช้งาน?", "setupDatabaseChooseDatabase": "คุณต้องการใช้ฐานข้อมูลใด?",
"setupDatabaseEmbeddedMariaDB": "คุณไม่จำเป็นต้องทำอะไร Docker image จะสร้างและตั่งค่า MariaDB ให้โดยอัตโนมัติ Uptime Kuma จะเชื่อมต่อกับฐานข้อมูลนี้ด้วย unix socket", "setupDatabaseEmbeddedMariaDB": "คุณไม่จำเป็นต้องทำอะไร Docker image จะสร้างและตั่งค่า MariaDB ให้โดยอัตโนมัติ Uptime Kuma จะเชื่อมต่อกับฐานข้อมูลนี้ด้วย unix socket",
"setupDatabaseMariaDB": "เชื่อมต่อไปยัง MariaDB ภายนอก คุณจำเป็นจะต้องตั่งค่าการเชื่อมต่อฐานข้อมูล", "setupDatabaseMariaDB": "เชื่อมต่อไปยัง MariaDB ภายนอก คุณจำเป็นจะต้องตั่งค่าการเชื่อมต่อฐานข้อมูล",
"setupDatabaseSQLite": "ไฟล์ฐานข้อมูลอย่างง่าย แนะนำสำหรับการปรับใช้ขนาดเล็ก ก่อนเวอร์ชัน 2.0.0 Uptime Kuma ใช้ SQLite เป็นฐานข้อมูลเริ่มต้น", "setupDatabaseSQLite": "ไฟล์ฐานข้อมูลอย่างง่าย แนะนำสำหรับการปรับใช้ขนาดเล็ก ก่อนเวอร์ชัน 2.0.0 Uptime Kuma ใช้ SQLite เป็นฐานข้อมูลเริ่มต้น",
"dbName": "ชื่อฐานข้อมูล", "dbName": "ชื่อฐานข้อมูล",
"Passive Monitor Type": "ชนิดมอนิเตอร์แบบพาสซีฟ", "Passive Monitor Type": "ประเภทมอนิเตอร์แบบพาสซีฟ",
"documentationOf": "{0} คู่มือ", "documentationOf": "{0} คู่มือ",
"successDeleted": "ลบสำเร็จ.", "successDeleted": "ลบสำเร็จ.",
"Command": "คำสั่ง", "Command": "คำสั่ง",
@ -703,12 +703,289 @@
"ignoreTLSErrorGeneral": "ละเว้นข้อผิดพลาด TLS/SSL สำหรับการเชื่อมต่อ", "ignoreTLSErrorGeneral": "ละเว้นข้อผิดพลาด TLS/SSL สำหรับการเชื่อมต่อ",
"programmingLanguages": "ภาษาโปรแกรมมิ่ง", "programmingLanguages": "ภาษาโปรแกรมมิ่ง",
"Invert Keyword": "คำสำคัญ", "Invert Keyword": "คำสำคัญ",
"settingUpDatabaseMSG": "กำลังตั้งค่าฐานข้อมูลอาจใช้เวลาสักครู่ โปรดอดทนรอ", "settingUpDatabaseMSG": "การตั้งค่าฐานข้อมูล อาจต้องใช้เวลาสักระยะหนึ่ง โปรดอดใจรอ",
"time ago": "{0} ที่ผ่านมา", "time ago": "{0} ที่ผ่านมา",
"-year": "-ปี", "-year": "-ปี",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "ป้อนชื่อโฮสต์ของเซิร์ฟเวอร์ที่คุณต้องการเชื่อมต่อหรือ {localhost} หากคุณต้องการใช้ {local_mta}", "Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "ป้อนชื่อโฮสต์ของเซิร์ฟเวอร์ที่คุณต้องการเชื่อมต่อหรือ {localhost} หากคุณต้องการใช้ {local_mta}",
"Request Timeout": "หมดเวลาการเชื่อมต่อ", "Request Timeout": "หมดเวลาการเชื่อมต่อ",
"ignoredTLSError": "ข้อผิดพลาด TLS/SSL ถูกละเว้น", "ignoredTLSError": "ข้อผิดพลาด TLS/SSL ถูกละเว้น",
"pushOthers": "อื่น ๆ", "pushOthers": "อื่น ๆ",
"pushViewCode": "วิธีใช้งาน Push monitor (ดูโค้ด)" "pushViewCode": "วิธีใช้งาน Push monitor (ดูโค้ด)",
"templateServiceName": "ชื่อบริการ",
"templateHostnameOrURL": "ชื่อโฮสต์หรือ URL",
"templateStatus": "สถานะ",
"webhookBodyCustomOption": "เนื้อหากำหนดเอง",
"Reset Token": "รีเซ็ตโทเคน",
"apiKeyAddedMsg": "คีย์ API ของคุณถูกเพิ่มเรียบร้อยแล้ว โปรดจดบันทึกไว้ เนื่องจากจะไม่แสดงอีกครั้ง",
"wayToGetSevenIOApiKey": "ไปที่แดชบอร์ดที่ app.seven.io > develope > api key > ปุ่มเพิ่มสีเขียว",
"filterActivePaused": "หยุดชั่วคราว",
"Search monitored sites": "ค้นหาเว็บไซต์ที่ตรวจสอบ",
"liquidIntroduction": "การใช้เทมเพลตสามารถทำได้ผ่านภาษาการสร้างเทมเพลต Liquid โปรดดูที่ {0} สำหรับคำแนะนำในการใช้งาน ตัวแปรที่ใช้ได้มีดังนี้:",
"templateLimitedToUpDownCertNotifications": "ใช้ได้เฉพาะสำหรับการแจ้งเตือนสถานะ UP/DOWN/การหมดอายุของใบรับรอง",
"selectedMonitorCount": "ที่เลือกไว้: {0}",
"statusPageSpecialSlugDesc": "Slug พิเศษ {0}: หน้านี้จะแสดงเมื่อไม่มีการระบุ slug",
"Add a new expiry notification day": "เพิ่มวันแจ้งเตือนการหมดอายุใหม่",
"templateMonitorJSON": "ออบเจ็กต์ที่อธิบายเกี่ยวกับตัวตรวจสอบ",
"templateLimitedToUpDownNotifications": "ใช้ได้เฉพาะสำหรับการแจ้งเตือนสถานะ UP/DOWN",
"webhookBodyPresetOption": "ค่าที่ตั้งไว้ล่วงหน้า - {0}",
"Check/Uncheck": "เลือก/ไม่เลือก",
"Learn More": "เรียนรู้เพิ่มเติม",
"Add API Key": "เพิ่มคีย์ API",
"templateMsg": "ข้อความการแจ้งเตือน",
"Json Query Expression": "นิพจน์สำหรับดึงข้อมูล JSON",
"locally configured mail transfer agent": "ตัวส่งอีเมลในเครื่อง",
"filterActive": "กำลังทำงาน",
"successKeyword": "คำสำเร็จ (Success Keyword)",
"smseagleContact": "ชื่อผู้ติดต่อในสมุดโทรศัพท์",
"smspartnerApiurl": "คุณสามารถหาคีย์ API ของคุณได้ในแดชบอร์ดที่ {0}",
"smspartnerPhoneNumber": "หมายเลขโทรศัพท์",
"smspartnerSenderName": "ชื่อผู้ส่ง SMS",
"Remove the expiry notification": "ลบวันแจ้งเตือนการหมดอายุ",
"Refresh Interval": "ช่วงเวลารีเฟรช",
"Refresh Interval Description": "หน้าสถานะจะทำการรีเฟรชเว็บไซต์ทั้งหมดทุก ๆ {0} วินาที",
"noDockerHostMsg": "ไม่พร้อมใช้งาน กรุณาตั้งค่า Docker Host ก่อน",
"tailscalePingWarning": "เพื่อที่จะใช้ตัวตรวจสอบ Tailscale Ping คุณต้องติดตั้ง Uptime Kuma โดยไม่ใช้ Docker และติดตั้ง Tailscale client บนเซิร์ฟเวอร์ของคุณด้วย",
"telegramUseTemplate": "ใช้เทมเพลต ข้อความที่กำหนดเอง",
"telegramUseTemplateDescription": "หากเปิดใช้งาน ข้อความจะถูกส่งโดยใช้เทมเพลตที่กำหนดเอง",
"telegramTemplateFormatDescription": "Telegram อนุญาตให้ใช้ภาษามาร์กอัปต่าง ๆ กับข้อความ ดูรายละเอียดเพิ่มเติมได้ที่ Telegram {0}",
"telegramServerUrl": "(ไม่บังคับ) URL ของเซิร์ฟเวอร์",
"telegramServerUrlDescription": "เพื่อยกระดับข้อจำกัดของ API ของ Telegram หรือให้เข้าถึงพื้นที่ที่ถูกบล็อก (จีน, อิหร่าน, เป็นต้น) สำหรับข้อมูลเพิ่มเติมคลิก {0}. ค่าเริ่มต้น: {1}",
"enableNSCD": "เปิดใช้งาน NSCD (Name Service Cache Daemon) สำหรับการแคชคำขอ DNS ทั้งหมด",
"emailCustomisableContent": "เนื้อหาที่ปรับแต่งได้",
"smtpLiquidIntroduction": "สองฟิลด์ต่อไปนี้สามารถใช้เทมเพลตผ่านภาษาการสร้างเทมเพลต Liquid โปรดดูที่ {0} สำหรับคำแนะนำในการใช้งาน ตัวแปรที่ใช้ได้มีดังนี้:",
"emailTemplateMsg": "ข้อความของการแจ้งเตือน",
"postToExistingThread": "โพสต์ไปยังเธรด / โพสต์ฟอรัมที่มีอยู่แล้ว",
"whatHappensAtForumPost": "สร้างโพสต์ในฟอรัมใหม่ จะไม่โพสต์ข้อความในโพสต์เดิม หากต้องการโพสต์ในโพสต์เดิมให้ใช้ “{option}”",
"wayToGetDiscordThreadId": "การรับค่า ID ของเธรดหรือโพสต์ในฟอรัมจะคล้ายกับการรับ Channel ID อ่านเพิ่มเติมเกี่ยวกับวิธีการรับ ID ได้ที่ {0}",
"infiniteRetention": "ตั้งค่าเป็น 0 เพื่อการเก็บข้อมูลตลอดไป",
"confirmDeleteTagMsg": "คุณแน่ใจหรือว่าต้องการลบแท็กนี้? มอนิเตอร์ที่ใช้กับแท็กนี้จะไม่ได้ถูกลบ",
"affectedMonitorsDescription": "เลือกมอนิเตอร์ที่ได้รับผลกระทบจากการซ่อมบำรุงปัจจุบัน",
"affectedStatusPages": "แสดงข้อความการซ่อมบำรุงนี้บนหน้าสถานะที่เลือก",
"wayToGetKookBotToken": "สร้างแอปพลิเคชันและรับโทเค็นบอตของคุณที่ {0}",
"wayToGetKookGuildID": "เปิดโหมด Developer ในการตั้งค่าของ Kook แล้วคลิกขวาที่กิลด์เพื่อรับ ID ของมัน",
"Strategy": "กลยุทธ์",
"Economy": "เศรษฐกิจ",
"You can divide numbers with": "คุณสามารถหารตัวเลขได้ด้วย",
"Notify Channel": "ช่องทางการแจ้งเตือน",
"setup a new monitor group": "ตั้งค่ากลุ่มการมอนิเตอร์หม่",
"smseagleGroup": "ชื่อกลุ่มสมุดโทรศัพท์",
"smseagleEncoding": "ส่งเป็น Unicode",
"smseaglePriority": "ลำดับความสำคัญของข้อความ (0-9, ค่าเริ่มต้น = 0)",
"smspartnerPhoneNumberHelptext": "หมายเลขต้องอยู่ในรูปแบบสากล {0}, {1} และหากมีหลายหมายเลขต้องคั่นด้วย {2}",
"smspartnerSenderNameInfo": "ต้องอยู่ระหว่าง 3 ถึง 11 ตัวอักษรปกติ",
"Custom Monitor Type": "ประเภทการมอนิเตอร์ แบบกำหนดเอง",
"Add Another": "เพิ่มอีกหนึ่ง",
"Expires": "หมดอายุ",
"disableAPIKeyMsg": "คุณแน่ใจหรือไม่ว่าจะปิดการใช้งาน API คีย์นี้?",
"ntfyAuthenticationMethod": "วิธีการยืนยันตัวตน",
"ntfyPriorityHelptextAllEvents": "ทุกกิจกรรมจะถูกส่งด้วยลำดับความสำคัญสูงสุด",
"ntfyPriorityHelptextAllExceptDown": "เหตุการณ์ทั้งหมดจะถูกส่งด้วยลำดับความสำคัญนี้ ยกเว้นเหตุการณ์ {0} ซึ่งมีลำดับความสำคัญ {1}",
"Show Clickable Link Description": "หากทำเครื่องหมายไว้ ทุกคนที่มีสิทธิ์เข้าถึงหน้าสถานะนี้จะสามารถเข้าถึง URL ของมอนิเตอร์ได้",
"monitorToastMessagesDescription": "การแจ้งเตือนแบบ Toast สำหรับการตรวจสอบจะหายไปหลังจากเวลาที่กำหนด (เป็นวินาที) หากตั้งค่าเป็น -1 ระบบจะไม่จำกัดเวลาแสดงผล หากตั้งค่าเป็น 0 จะปิดการแสดงการแจ้งเตือนแบบ Toast ทั้งหมด",
"wayToGetFlashDutyKey": "คุณสามารถไปที่ Channel -> (เลือก Channel) -> Integrations -> Add a new integration' page, add 'Uptime Kuma' to get push address, copy the Integration Key in the address. สำหรับข้อมูลเพิ่มเติม โปรดไปที่",
"cacheBusterParamDescription": "พารามิเตอร์ที่สร้างขึ้นแบบสุ่มเพื่อหลีกเลี่ยงการใช้แคช",
"gamedigGuessPortDescription": "พอร์ตที่ใช้โดย Valve Server Query Protocol อาจแตกต่างจากพอร์ตไคลเอนต์ ลองใช้วิธีนี้หากมอนิเตอร์ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ของคุณได้",
"bitrix24SupportUserID": "กรอกรหัสผู้ใช้ของคุณลงใน Bitrix24 คุณสามารถค้นหารหัสได้จากลิงก์โดยไปที่โปรไฟล์ของผู้ใช้",
"successBackupRestored": "กู้คืนข้อมูลสำรองสำเร็จแล้ว",
"Remote Browser not found!": "ไม่พบ Remote Browse!",
"remoteBrowsersDescription": "Remote Browsers เป็นทางเลือกหนึ่งแทนการเรียกใช้ Chromium บนเครื่องของคุณโดยตรง โดยสามารถตั้งค่าใช้งานร่วมกับบริการ เช่น browserless.io หรือเชื่อมต่อกับ",
"deleteRemoteBrowserMessage": "คุณแน่ใจหรือไม่ว่าต้องการลบ Remote Browser นี้สำหรับมอนิเตอร์ทั้งหมด?",
"mongodbCommandDescription": "รันคำสั่ง MongoDB กับฐานข้อมูล สำหรับข้อมูลเกี่ยวกับคำสั่งที่มีอยู่ โปรดดูที่ {documentation}",
"goAlertInfo": "GoAlert is a An open source application for on-call scheduling, automated escalations and notifications (like SMS or voice calls). Automatically engage the right person, the right way, and at the right time! {0}",
"aboutNotifyChannel": "การแจ้งเตือนไปยังช่องทางจะทำให้เกิดการแจ้งเตือนบนเดสก์ท็อปหรือมือถือของสมาชิกทุกคนในช่องนั้น ไม่ว่าสถานะของพวกเขาจะเป็น “ใช้งาน” หรือ “ไม่อยู่” ก็ตาม",
"DockerHostRequired": "กรุณาตั้งค่า Docker Host สำหรับมอนิเตอร์นี้",
"Select message type": "เลือกประเภทข้อความ",
"dataRetentionTimeError": "ระยะเวลาเก็บข้อมูลต้องเป็น 0 หรือมากกว่า",
"promosmsAllowLongSMS": "อนุญาตให้ส่ง SMS ยาว",
"apiKey-active": "ใช้งานอยู่",
"and": "และ",
"chromeExecutable": "ไฟล์ที่ใช้รัน Chrome/Chromium",
"Maintenance Time Window of a Day": "ช่วงเวลาการซ่อมบำรุงของวัน",
"Effective Date Range": "ช่วงวันที่มีผล (ไม่บังคับ)",
"leave blank for default subject": "เว้นว่างไว้สำหรับหัวข้อเริ่มต้น",
"emailCustomBody": "เนื้อหากำหนดเอง",
"leave blank for default body": "เว้นว่างไว้สำหรับเนื้อหาพื้นฐาน",
"emailTemplateMonitorJSON": "อ็อบเจ็กต์ที่อธิบายเกี่ยวกับมอนิเตอร์",
"Send to channel": "ส่งไปยังช่องทาง",
"Create new forum post": "สร้างโพสต์ฟอรัมใหม่",
"forumPostName": "ชื่อโพสต์ในฟอรัม",
"threadForumPostID": "รหัสเธรด / โพสต์ในฟอรัม",
"e.g. {discordThreadID}": "เช่น {discordThreadID}",
"Channel access token (Long-lived)": "Channel access token (Long-lived)",
"Your User ID": "ไอดีผู้ใช้ของคุณ",
"deleteMaintenanceMsg": "คุณแน่ใจหรือว่าต้องการลบการซ่อมบำรุงนี้?",
"atLeastOneMonitor": "เลือกมอนิเตอร์ที่ได้รับผลกระทบอย่างน้อยหนึ่งมอนิเตอร์",
"invertKeywordDescription": "ค้นหาคำสำคัญที่ไม่มีอยู่ แทนที่จะมีอยู่",
"Guild ID": "กิลด์ ID",
"Proto Service Name": "ชื่อบริการ Proto",
"Proto Method": "Proto เมทอด",
"Proto Content": "เนื้อหา Proto",
"Lowcost": "ต้นทุนต่ำ",
"high": "สูง",
"SMSManager API Docs": "เอกสาร API ของ SMSManager ",
"Gateway Type": "ประเภทเกตเวย์",
"Base URL": "URL หลัก",
"pushoverMessageTtl": "ข้อความ TTL (วินาที)",
"Free Mobile User Identifier": "Free Mobile User Identifier",
"Free Mobile API Key": "Free Mobile API Key",
"SendKey": "SendKey",
"goAlertIntegrationKeyInfo": "Get generic API integration key for the service in this format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" usually the value of token parameter of copied URL.",
"Mentioning": "การกล่าวถึง",
"Don't mention people": "อย่ากล่าวถึงบุคคล",
"Mention group": "กล่าวถึง {group}",
"Bark API Version": "เวอร์ชัน Bark API",
"openModalTo": "เปิดโมดัลไปยัง {0}",
"Add a domain": "เพิ่มโดเมน",
"Remove domain": "ลบโดเมน {0}",
"smseagleTo": "หมายเลขโทรศัพท์",
"smseagleRecipientType": "ประเภทผู้รับ",
"smseagleRecipient": "ผู้รับ (หากมีหลายคนให้แยกด้วยเครื่องหมายจุลภาค)",
"smseagleUrl": "URL ของอุปกรณ์ SMSEagle ของคุณ",
"Server URL should not contain the nfty topic": "URL ของเซิร์ฟเวอร์ไม่ควรมีหัวข้อ “nfty”",
"smseagleToken": "API Access token",
"pushDeerServerDescription": "เว้นว่างไว้เพื่อใช้เซิร์ฟเวอร์อย่างเป็นทางการ",
"Edit Tag": "แก้ไขแท็ก",
"Server Address": "ที่อยู่เซิร์ฟเวอร์",
"Expiry": "หมดอายุ",
"Expiry date": "วันที่หมดอายุ",
"Don't expire": "ไม่หมดอายุ",
"Continue": "ดำเนินการต่อ",
"Key Added": "เพิ่มคีย์แล้ว",
"No API Keys": "ไม่มี API คีย์",
"apiKey-expired": "หมดอายุแล้ว",
"apiKey-inactive": "ไม่ได้ใช้งาน",
"deleteAPIKeyMsg": "คุณแน่ใจหรือไม่ว่าต้องการลบ API คีย์นี้?",
"Generate": "สร้าง",
"pagertreeUrgency": "ด่วน",
"pagertreeLow": "ต่ำ",
"pagertreeMedium": "ปานกลาง",
"pagertreeHigh": "สูง",
"lunaseaTarget": "เป้าหมาย",
"lunaseaDeviceID": "ไอดี ของอุปกรณ์",
"lunaseaUserID": "ไอดี ของผู้ใช้",
"ntfyUsernameAndPassword": "ชื่อผู้ใช้ และ รหัสผ่าน",
"twilioAccountSID": "บัญชี SID",
"twilioApiKey": "API คีย์ (ไม่บังคับ)",
"twilioFromNumber": "จากหมายเลข",
"twilioToNumber": "ถึงหมายเลข",
"Monitor Setting": "การตั้งค่ามอนิเตอร์ของ {0}",
"PushDeer Server": "เซิร์ฟเวอร์ PushDeer",
"Google Analytics ID": "ไอดี Google Analytics",
"API Keys": "API คีย์",
"pagertreeIntegrationUrl": "URL สำหรับการเชื่อมต่อระบบ (Integration URL)",
"twilioAuthToken": "Auth Token / Api Key Secret",
"Group": "กลุ่ม",
"Monitor Group": "กลุ่มมอนิเตอร์",
"monitorToastMessagesLabel": "การแจ้งเตือนมอนิเตอร์แบบ Toast",
"Press Enter to add broker": "กด Enter เพื่อเพิ่มโบรกเกอร์",
"Mechanism": "กลไก",
"Pick a SASL Mechanism...": "เลือกกลไก SASL…",
"noGroupMonitorMsg": "ไม่พร้อมใช้งาน โปรดสร้างกลุ่มมอนิเตอร์ก่อน",
"Close": "ปิด",
"FlashDuty Severity": "ความรุนแรง",
"Show Clickable Link": "แสดงลิงก์ที่คลิกได้",
"Open Badge Generator": "เปิดเครื่องมือสร้าง Badge",
"Badge Generator": "เครื่องมือสร้าง Badge ของ {0}",
"Badge Type": "ประเภท Badge",
"Badge Duration (in hours)": "ระยะเวลา Badge (เป็นชั่วโมง)",
"Badge Label": "ป้ายกำกับ Badge",
"nostrRelays": "รีเลย์ Nostr",
"nostrRelaysHelp": "URL รีเลย์หนึ่งรายการต่อบรรทัด",
"cacheBusterParam": "เพิ่มพารามิเตอร์ {0}",
"gamedigGuessPort": "Gamedig: เดาพอร์ต",
"Message format": "รูปแบบข้อความ",
"wayToGetBitrix24Webhook": "คุณสามารถสร้างเว็บฮุกได้โดยทำตามขั้นตอนที่ {0}",
"authIncorrectCreds": "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง",
"2faAlreadyEnabled": "2FA ได้รับการเปิดใช้งานแล้ว",
"2faEnabled": "เปิดใช้งาน 2FA แล้ว",
"2faDisabled": "2FA ถูกปิดการใช้งาน",
"successAdded": "เพิ่มเรียบร้อยแล้ว",
"successResumed": "ดำเนินการต่อสำเร็จแล้ว",
"successPaused": "หยุดชั่วคราวสำเร็จแล้ว",
"successEnabled": "เปิดใช้งานสำเร็จแล้ว",
"tagNotFound": "ไม่พบแท็ก",
"foundChromiumVersion": "พบ Chromium/Chrome เวอร์ชัน: {0}",
"Add a Remote Browser": "เพิ่ม Remote Browser",
"self-hosted container": "คอนเทนเนอร์ที่โฮสต์เอง (self-hosted container)",
"remoteBrowserToggle": "โดยปกติแล้ว Chromium จะทำงานภายในคอนเทนเนอร์ของ Uptime Kuma คุณสามารถใช้ remote browser ได้โดยเปิดสวิตช์นี้",
"useRemoteBrowser": "ใช้ Remote Browser",
"aboutSlackUsername": "เปลี่ยนชื่อที่แสดงของผู้ส่งข้อความ หากคุณต้องการกล่าวถึงใคร ให้รวมไว้ในชื่อที่เป็นมิตรแทน",
"grpcMethodDescription": "ชื่อเมธอดจะถูกแปลงเป็นรูปแบบ camelCase เช่น sayHello, check, เป็นต้น",
"Enable TLS": "เปิดใช้งาน TLS",
"pagertreeSilent": "เงียบ",
"enableGRPCTls": "อนุญาตให้ส่งคำขอแบบ gRPC ด้วยการเชื่อมต่อ TLS",
"Sender name": "ชื่อผู้ส่ง",
"smsplanetNeedToApproveName": "ต้องได้รับการอนุมัติในแผงควบคุมของไคลเอนต์",
"Phone numbers": "หมายเลขโทรศัพท์",
"Badge Prefix": "คำนำหน้าค่าของ Badge",
"Badge Suffix": "คำตามหลังค่าของ Badge",
"Badge Label Color": "สีของป้ายข้อความ (Label) บน Badge",
"Badge Color": "สีของ Badge",
"Badge Preview": "ตัวอย่างการแสดง Badge (Preview)",
"Badge Label Suffix": "คำต่อท้ายข้อความ (Label) บน Badge",
"Badge Label Prefix": "คำนำหน้าข้อความ (Label) บน Badge",
"Badge Up Color": "สีของ Badge เมื่อสถานะเป็นปกติ (Up)",
"Badge Down Color": "สีของ Badge เมื่อสถานะเป็นออฟไลน์ (Down)",
"Badge Pending Color": "สีของ Badge ขณะรอการประมวลผล",
"Badge Maintenance Color": "สีของ Badge ในระหว่างการบำรุงรักษา",
"Badge Warn Color": "สีของ Badge เมื่อสถานะเตือน",
"Badge Warn Days": "จำนวนวันที่ Badge แสดงสถานะเตือน",
"Badge Down Days": "จำนวนวันที่ Badge แสดงสถานะออฟไลน์",
"Badge Style": "สไตล์ของ Badge",
"Badge value (For Testing only.)": "ค่าของ Badge (สำหรับการทดสอบเท่านั้น)",
"Badge URL": "URL ของ Badge",
"rabbitmqNodesRequired": "โปรดตั้งค่าโหนดสำหรับมอนิเตอร์นี้",
"Font Twemoji by Twitter licensed under": "ฟอนต์ Twemoji โดย Twitter ที่มีลิขสิทธิ์ภายใต้",
"the smsplanet documentation": "เอกสารของ smsplanet",
"smsplanetApiToken": "โทเค็นสำหรับ API ของ SMSPlanet",
"smsplanetApiDocs": "ข้อมูลรายละเอียดเกี่ยวกับการขอ API tokens สามารถดูได้ใน {the_smsplanet_documentation}",
"Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.": "การแจ้งเตือนที่มีความสำคัญตามเวลา จะถูกส่งทันที แม้ว่าจะอยู่ในโหมดไม่รบกวน (Do Not Disturb) ก็ตาม",
"Clear": "กระจ่าง",
"equals": "เท่ากับ",
"Go back to home page.": "กลับไปหน้าหลัก",
"conditionValuePlaceholder": "ค่า",
"not starts with": "ไม่ได้เริ่มต้นด้วย",
"Notification Channel": "ช่องทางการรับแจ้งเตือน",
"Alphanumerical string and hyphens only": "ใช้ได้เฉพาะตัวอักษรและตัวเลข (a-z, A-Z, 0-9) และขีดกลาง (-) เท่านั้น",
"Message Template": "ข้อความของเทมเพลต",
"Plain Text": "ข้อความธรรมดา",
"wayToWriteWahaChatId": "หมายเลขโทรศัพท์ที่มีรหัสประเทศ แต่ไม่มีเครื่องหมายบวกที่เริ่มต้น ({0}), หมายเลขติดต่อ ({1}) หรือ หมายเลขกลุ่ม ({2}) การแจ้งเตือนจะถูกส่งไปยัง Chat ID นี้จาก WAHA Session",
"not equals": "ไม่เท่ากับ",
"No tags found.": "ไม่พบแท็ก",
"Conditions": "เงื่อนไข",
"conditionAdd": "เพิ่มเงื่อนไข",
"conditionDelete": "ลบเงื่อนไข",
"conditionAddGroup": "เพิ่มกลุ่ม",
"conditionDeleteGroup": "ลบกลุ่ม",
"contains": "ประกอบด้วย",
"not contains": "ไม่มี",
"Template Format": "รูปแบบของเทมเพลต",
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.": "ระบุรหัสผู้ส่งข้อความแบบข้อความหรือลำดับหมายเลขโทรศัพท์ในรูปแบบ E.164 หากคุณต้องการรับการตอบกลับ",
"The phone number of the recipient in E.164 format.": "หมายเลขโทรศัพท์ของผู้รับในรูปแบบ E.164",
"Can be found on:": "สามารถดูได้ที่: {0}",
"From": "จาก",
"Time Sensitive (iOS Only)": "การแจ้งเตือนที่มีความสำคัญตามเวลา (เฉพาะ iOS)",
"Custom sound to override default notification sound": "เสียงที่กำหนดเองเพื่อแทนที่เสียงการแจ้งเตือนเริ่มต้น",
"Arcade": "อาร์เคด",
"Correct": "ถูกต้อง",
"Fail": "ล้มเหลว",
"Harp": "พิณ",
"Reveal": "เปิดเผย",
"Bubble": "ฟอง",
"Flute": "ขลุ่ย",
"Scifi": "ไซไฟ",
"Sound": "เสียง",
"starts with": "เริ่มต้นด้วย",
"ends with": "ลงท้ายด้วย",
"not ends with": "ไม่ลงท้ายด้วย",
"greater than or equal to": "มากกว่าหรือเท่ากับ",
"record": "บันทึก",
"less than": "น้อยกว่า",
"greater than": "มากกว่า",
"less than or equal to": "น้อยกว่าหรือเท่ากับ",
"Pop": "พอป",
"Elevator": "ลิฟต์",
"Doorbell": "ออด",
"Money": "เงิน",
"Guitar": "กีตาร์"
} }

View file

@ -1124,5 +1124,11 @@
"wayToWriteWahaChatId": "Номер телефону з міжнародним префіксом, але без знака плюс на початку ({0}), ID контакту ({1}) або ID групи ({2}). На цей ID чату надсилаються сповіщення з сеансу WAHA.", "wayToWriteWahaChatId": "Номер телефону з міжнародним префіксом, але без знака плюс на початку ({0}), ID контакту ({1}) або ID групи ({2}). На цей ID чату надсилаються сповіщення з сеансу WAHA.",
"telegramServerUrl": "(Необов'язково) URL сервера", "telegramServerUrl": "(Необов'язково) URL сервера",
"telegramServerUrlDescription": "Щоб зняти обмеження з Telegram bot api або отримати доступ у заблокованих регіонах (Китай, Іран тощо). Для отримання додаткової інформації натисніть {0}. За замовчуванням: {1}", "telegramServerUrlDescription": "Щоб зняти обмеження з Telegram bot api або отримати доступ у заблокованих регіонах (Китай, Іран тощо). Для отримання додаткової інформації натисніть {0}. За замовчуванням: {1}",
"Font Twemoji by Twitter licensed under": "Шрифт Twemoji від Twitter ліцензований під" "Font Twemoji by Twitter licensed under": "Шрифт Twemoji від Twitter ліцензований під",
"the smsplanet documentation": "документації smsplanet",
"Phone numbers": "Номери телефонів",
"Sender name": "Ім'я відправника",
"smsplanetNeedToApproveName": "Потребує схвалення в клієнтській панелі",
"smsplanetApiToken": "Токен для API SMSPlanet",
"smsplanetApiDocs": "Детальну інформацію про отримання токенів API можна знайти в {the_smsplanet_documentation}."
} }

View file

@ -1120,5 +1120,11 @@
"YZJ Robot Token": "YZJ 机器人令牌", "YZJ Robot Token": "YZJ 机器人令牌",
"telegramServerUrl": "(可选) 服务器 Url", "telegramServerUrl": "(可选) 服务器 Url",
"telegramServerUrlDescription": "用以解除 Telegram 的机器人 API 限制或在封锁区域(中国、伊朗等)获得访问权限。获取更多信息,请点击 {0}。默认值:{1}", "telegramServerUrlDescription": "用以解除 Telegram 的机器人 API 限制或在封锁区域(中国、伊朗等)获得访问权限。获取更多信息,请点击 {0}。默认值:{1}",
"Font Twemoji by Twitter licensed under": "由 Twitter 制作的 Twemoji 字体根据此许可证授权" "Font Twemoji by Twitter licensed under": "由 Twitter 制作的 Twemoji 字体根据此许可证授权",
"smsplanetApiDocs": "有关获取 API token 的详细信息,请参阅 {the_smsplanet_documentation}。",
"smsplanetNeedToApproveName": "需要在客户端面板进行确认",
"Sender name": "发件人名称",
"Phone numbers": "手机号码",
"the smsplanet documentation": "smsplanet 文档",
"smsplanetApiToken": "SMSPlanet API 的 Token"
} }

View file

@ -586,7 +586,7 @@
"Domain": "網域", "Domain": "網域",
"Workstation": "工作站", "Workstation": "工作站",
"disableCloudflaredNoAuthMsg": "您處於無驗證模式。無須輸入密碼。", "disableCloudflaredNoAuthMsg": "您處於無驗證模式。無須輸入密碼。",
"trustProxyDescription": "信任 'X-Forwarded-*' 標頭。如果您想要取得正確的客戶端 IP且您的 Uptime Kuma 架設於 Nginx 或 Apache 後方,您應該啟用此選項。", "trustProxyDescription": "信任“X-Forwarded-*”標頭。如果您想要取得正確的用戶端 IP而您的 Uptime Kuma 位於 Nginx 或 Apache 等代理程式後面,則應該啟用此功能。",
"wayToGetLineNotifyToken": "您可以從 {0} 取得存取權杖", "wayToGetLineNotifyToken": "您可以從 {0} 取得存取權杖",
"Examples": "範例", "Examples": "範例",
"Home Assistant URL": "Home Assistant 網址", "Home Assistant URL": "Home Assistant 網址",
@ -606,7 +606,7 @@
"goAlertInfo": "GoAlert 是用於待命排程、升級自動化,以及通知 (如簡訊或語音通話) 的開源應用程式。自動在正確的時間、用洽當的方法、聯絡合適的人! {0}", "goAlertInfo": "GoAlert 是用於待命排程、升級自動化,以及通知 (如簡訊或語音通話) 的開源應用程式。自動在正確的時間、用洽當的方法、聯絡合適的人! {0}",
"goAlertIntegrationKeyInfo": "取得服務的通用 API 整合金鑰,格式為 \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\"。通常是已複製的網址的權杖參數值。", "goAlertIntegrationKeyInfo": "取得服務的通用 API 整合金鑰,格式為 \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\"。通常是已複製的網址的權杖參數值。",
"goAlert": "GoAlert", "goAlert": "GoAlert",
"backupOutdatedWarning": "即將棄用:由於專案新增了大量新功能,且備份功能未被妥善維護,故此功能無法產生或復原完整備份。", "backupOutdatedWarning": "已棄用:由於添加了許多功能且此備份功能有點無人維護,因此它無法產生或還原完整的備份。",
"backupRecommend": "請直接備份磁碟區或 ./data/ 資料夾。", "backupRecommend": "請直接備份磁碟區或 ./data/ 資料夾。",
"Optional": "選填", "Optional": "選填",
"squadcast": "Squadcast", "squadcast": "Squadcast",
@ -716,7 +716,7 @@
"apiKey-expired": "已過期", "apiKey-expired": "已過期",
"Reconnecting...": "重新連線...", "Reconnecting...": "重新連線...",
"Expiry date": "過期時間", "Expiry date": "過期時間",
"Don't expire": "過期", "Don't expire": "不過期",
"Continue": "繼續", "Continue": "繼續",
"Add Another": "新增作者", "Add Another": "新增作者",
"Add API Key": "新增 API 金鑰", "Add API Key": "新增 API 金鑰",
@ -738,10 +738,10 @@
"Learn More": "閱讀更多", "Learn More": "閱讀更多",
"pushoverMessageTtl": "Message TTL (秒)", "pushoverMessageTtl": "Message TTL (秒)",
"apiKeyAddedMsg": "您的 API 金鑰已建立。金鑰不會再次顯示,請將它放在安全的地方。", "apiKeyAddedMsg": "您的 API 金鑰已建立。金鑰不會再次顯示,請將它放在安全的地方。",
"No API Keys": "無 API 金鑰", "No API Keys": "沒有 API Keys",
"apiKey-active": "活躍", "apiKey-active": "啟用",
"Expires": "過期", "Expires": "過期",
"disableAPIKeyMsg": "您確定要停用這個 API 金鑰?", "disableAPIKeyMsg": "確定要禁用這個 API key?",
"Monitor Setting": "{0} 的監視器設定", "Monitor Setting": "{0} 的監視器設定",
"Guild ID": "公會 ID", "Guild ID": "公會 ID",
"chromeExecutableDescription": "如果您使用 Docker 且未安裝 Chromium可能要花數分鐘安裝後才能顯示測試結果。安裝會使用 1GB 的硬碟空間。", "chromeExecutableDescription": "如果您使用 Docker 且未安裝 Chromium可能要花數分鐘安裝後才能顯示測試結果。安裝會使用 1GB 的硬碟空間。",
@ -1085,5 +1085,31 @@
"conditionValuePlaceholder": "值", "conditionValuePlaceholder": "值",
"Separate multiple email addresses with commas": "用逗號分隔多個電子郵件地址", "Separate multiple email addresses with commas": "用逗號分隔多個電子郵件地址",
"record": "記錄", "record": "記錄",
"New Group": "新分組" "New Group": "新分組",
"Font Twemoji by Twitter licensed under": "已經由 Twitter 的 Twemoji 授權",
"Phone numbers": "手機號碼",
"smsplanetApiToken": "SMSPlanet 的 Token",
"smsplanetApiDocs": "有關取得 API token的詳細信息請參閱 {the_smsplanet_documentation}。",
"the smsplanet documentation": "smsplanet 說明文件",
"Sender name": "發送者名稱",
"smsplanetNeedToApproveName": "需在用戶介面獲得允許",
"templateServiceName": "伺服器名稱",
"templateHostnameOrURL": "主機名稱或 URL",
"templateStatus": "狀態",
"telegramUseTemplate": "使用自訂訊息模板",
"telegramTemplateFormatDescription": "Telegram 允許使用不同的標記語言來傳送訊息,有關具體詳情請參閱 Telegram {0}。",
"telegramUseTemplateDescription": "如果啟用則使用自訂模板發送訊息。",
"Plain Text": "純文字",
"Message Template": "訊息模板",
"wayToGetWahaApiUrl": "你的 WAHA 主機 URL。",
"wayToGetWahaApiKey": "API Key 是你在 WAHA 使用的環境變數 WHATSAPP_API_KEY 的值。",
"wayToGetWahaSession": "從這個 WAHA 會話發送通知到聊天ID。你可以從 WAHA 儀錶板找到它。",
"Template Format": "模板格式",
"wahaSession": "WAHA 會話",
"wahaChatId": "聊天室ID (手機號碼/聯絡人 ID/群組 ID)",
"wayToWriteWahaChatId": "帶有國際前綴但不含開頭加號 ({0})、聯絡人 ID ({1}) 或群組 ID ({2}) 的電話號碼。通知從 WAHA 會話傳送到此聊天 ID。",
"YZJ Webhook URL": "YZJ 的 Webhook URL",
"YZJ Robot Token": "YZJ 的機器人 token",
"telegramServerUrl": "(可選) 伺服器 URL",
"telegramServerUrlDescription": "解除 Telegram 的機器人 API 限製或在被封鎖區域(中國、伊朗等)取得存取權限。欲了解更多信息,請點擊{0}。預設值:{1}"
} }

View file

@ -16,7 +16,7 @@
<span class="fs-4 title">{{ $t("Uptime Kuma") }}</span> <span class="fs-4 title">{{ $t("Uptime Kuma") }}</span>
</router-link> </router-link>
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/uptime-kuma/releases" class="btn btn-info me-3"> <a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/uptime-kuma/releases" class="btn btn-primary me-3">
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("New Update") }} <font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("New Update") }}
</a> </a>

View file

@ -9,7 +9,8 @@
<div>{{ monitor.id }}</div> <div>{{ monitor.id }}</div>
</div> </div>
</h1> </h1>
<p v-if="monitor.description">{{ monitor.description }}</p> <!-- eslint-disable-next-line vue/no-v-html-->
<p v-if="monitor.description" v-html="descriptionHTML"></p>
<div class="d-flex"> <div class="d-flex">
<div class="tags"> <div class="tags">
<Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" /> <Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" />
@ -285,6 +286,8 @@ import Tag from "../components/Tag.vue";
import CertificateInfo from "../components/CertificateInfo.vue"; import CertificateInfo from "../components/CertificateInfo.vue";
import { getMonitorRelativeURL } from "../util.ts"; import { getMonitorRelativeURL } from "../util.ts";
import { URL } from "whatwg-url"; import { URL } from "whatwg-url";
import DOMPurify from "dompurify";
import { marked } from "marked";
import { getResBaseURL } from "../util-frontend"; import { getResBaseURL } from "../util-frontend";
import { highlight, languages } from "prismjs/components/prism-core"; import { highlight, languages } from "prismjs/components/prism-core";
import "prismjs/components/prism-clike"; import "prismjs/components/prism-clike";
@ -399,6 +402,14 @@ export default {
screenshotURL() { screenshotURL() {
return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime; return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime;
},
descriptionHTML() {
if (this.monitor.description != null) {
return DOMPurify.sanitize(marked(this.monitor.description));
} else {
return "";
}
} }
}, },

View file

@ -223,12 +223,12 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="mb-2">{{ $t("startDateTime") }}</div> <div class="mb-2">{{ $t("startDateTime") }}</div>
<input v-model="maintenance.dateRange[0]" type="datetime-local" class="form-control" :required="maintenance.strategy === 'single'"> <input v-model="maintenance.dateRange[0]" type="datetime-local" max="9999-12-31T23:59" class="form-control" :required="maintenance.strategy === 'single'">
</div> </div>
<div class="col"> <div class="col">
<div class="mb-2">{{ $t("endDateTime") }}</div> <div class="mb-2">{{ $t("endDateTime") }}</div>
<input v-model="maintenance.dateRange[1]" type="datetime-local" class="form-control" :required="maintenance.strategy === 'single'"> <input v-model="maintenance.dateRange[1]" type="datetime-local" max="9999-12-31T23:59" class="form-control" :required="maintenance.strategy === 'single'">
</div> </div>
</div> </div>
</div> </div>

View file

@ -24,6 +24,9 @@
<option value="ping"> <option value="ping">
Ping Ping
</option> </option>
<option value="smtp">
SMTP
</option>
<option value="snmp"> <option value="snmp">
SNMP SNMP
</option> </option>
@ -52,6 +55,9 @@
<option value="push"> <option value="push">
Push Push
</option> </option>
<option value="manual">
{{ $t("Manual") }}
</option>
</optgroup> </optgroup>
<optgroup :label="$t('Specific Monitor Type')"> <optgroup :label="$t('Specific Monitor Type')">
@ -112,6 +118,18 @@
<input id="name" v-model="monitor.name" type="text" class="form-control" data-testid="friendly-name-input" :placeholder="defaultFriendlyName"> <input id="name" v-model="monitor.name" type="text" class="form-control" data-testid="friendly-name-input" :placeholder="defaultFriendlyName">
</div> </div>
<!-- Manual Status switcher -->
<div v-if="monitor.type === 'manual'" class="mb-3">
<div class="btn-group w-100 mb-3">
<button class="btn btn-success" @click="monitor.manual_status = 1">
<i class="fas fa-check"></i> {{ $t("Up") }}
</button>
<button class="btn btn-danger" @click="monitor.manual_status = 0">
<i class="fas fa-times"></i> {{ $t("Down") }}
</button>
</div>
</div>
<!-- URL --> <!-- URL -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3">
<label for="url" class="form-label">{{ $t("URL") }}</label> <label for="url" class="form-label">{{ $t("URL") }}</label>
@ -281,8 +299,8 @@
</template> </template>
<!-- Hostname --> <!-- Hostname -->
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / SNMP only --> <!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / SNMP / SMTP only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'snmp'" class="my-3"> <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'smtp' || monitor.type === 'snmp'" class="my-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label> <label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input <input
id="hostname" id="hostname"
@ -297,7 +315,7 @@
<!-- Port --> <!-- Port -->
<!-- For TCP Port / Steam / MQTT / Radius Type / SNMP --> <!-- For TCP Port / Steam / MQTT / Radius Type / SNMP -->
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'snmp'" class="my-3"> <div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'smtp' || monitor.type === 'snmp'" class="my-3">
<label for="port" class="form-label">{{ $t("Port") }}</label> <label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1"> <input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div> </div>
@ -329,6 +347,18 @@
</select> </select>
</div> </div>
<div v-if="monitor.type === 'smtp'" class="my-3">
<label for="smtp_security" class="form-label">{{ $t("SMTP Security") }}</label>
<select id="smtp_security" v-model="monitor.smtpSecurity" class="form-select">
<option value="secure">SMTPS</option>
<option value="nostarttls">Ignore STARTTLS</option>
<option value="starttls">Use STARTTLS</option>
</select>
<div class="form-text">
{{ $t("smtpHelpText") }}
</div>
</div>
<!-- Json Query --> <!-- Json Query -->
<!-- For Json Query / SNMP --> <!-- For Json Query / SNMP -->
<div v-if="monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3"> <div v-if="monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3">
@ -595,10 +625,14 @@
<input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required :min="minInterval" step="1"> <input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required :min="minInterval" step="1">
</div> </div>
<!-- Timeout: HTTP / Keyword / SNMP only --> <!-- Timeout: HTTP / JSON query / Keyword / Ping / RabbitMQ / SNMP only -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'snmp' || monitor.type === 'rabbitmq'" class="my-3"> <div v-if="monitor.type === 'http' || monitor.type === 'json-query' || monitor.type === 'keyword' || monitor.type === 'ping' || monitor.type === 'rabbitmq' || monitor.type === 'snmp'" class="my-3">
<label for="timeout" class="form-label">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label> <label for="timeout" class="form-label">
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1"> {{ monitor.type === 'ping' ? $t("pingGlobalTimeoutLabel") : $t("Request Timeout") }}
<span v-if="monitor.type !== 'ping'">({{ $t("timeoutAfter", [monitor.timeout || clampTimeout(monitor.interval)]) }})</span>
</label>
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" :min="timeoutMin" :max="timeoutMax" :step="timeoutStep" required>
<div v-if="monitor.type === 'ping'" class="form-text">{{ $t("pingGlobalTimeoutDescription") }}</div>
</div> </div>
<div class="my-3"> <div class="my-3">
@ -660,10 +694,39 @@
</div> </div>
</div> </div>
<!-- Ping packet size --> <!-- Max Packets / Count -->
<div v-if="monitor.type === 'ping'" class="my-3">
<label for="ping-count" class="form-label">{{ $t("pingCountLabel") }}</label>
<input id="ping-count" v-model="monitor.ping_count" type="number" class="form-control" required min="1" max="100" step="1">
<div class="form-text">
{{ $t("pingCountDescription") }}
</div>
</div>
<!-- Numeric Output -->
<div v-if="monitor.type === 'ping'" class="my-3 form-check">
<input id="ping_numeric" v-model="monitor.ping_numeric" type="checkbox" class="form-check-input" :checked="monitor.ping_numeric">
<label class="form-check-label" for="ping_numeric">
{{ $t("pingNumericLabel") }}
</label>
<div class="form-text">
{{ $t("pingNumericDescription") }}
</div>
</div>
<!-- Packet size -->
<div v-if="monitor.type === 'ping'" class="my-3"> <div v-if="monitor.type === 'ping'" class="my-3">
<label for="packet-size" class="form-label">{{ $t("Packet Size") }}</label> <label for="packet-size" class="form-label">{{ $t("Packet Size") }}</label>
<input id="packet-size" v-model="monitor.packetSize" type="number" class="form-control" required min="1" max="65500" step="1"> <input id="packet-size" v-model="monitor.packetSize" type="number" class="form-control" required min="1" :max="65500" step="1">
</div>
<!-- per-request timeout -->
<div v-if="monitor.type === 'ping'" class="my-3">
<label for="ping_per_request_timeout" class="form-label">{{ $t("pingPerRequestTimeoutLabel") }}</label>
<input id="ping_per_request_timeout" v-model="monitor.ping_per_request_timeout" type="number" class="form-control" required min="0" max="300" step="1">
<div class="form-text">
{{ $t("pingPerRequestTimeoutDescription") }}
</div>
</div> </div>
<!-- HTTP / Keyword only --> <!-- HTTP / Keyword only -->
@ -697,6 +760,20 @@
{{ $t("acceptedStatusCodesDescription") }} {{ $t("acceptedStatusCodesDescription") }}
</div> </div>
</div> </div>
<div class="my-3">
<label for="ipFamily" class="form-label">{{ $t("Ip Family") }}</label>
<select id="ipFamily" v-model="monitor.ipFamily" class="form-select">
<option :value="null">{{ $t("auto-select") }}</option>
<option value="ipv4">IPv4</option>
<option value="ipv6">IPv6</option>
</select>
<i18n-t v-if="monitor.ipFamily == null" keypath="ipFamilyDescriptionAutoSelect" tag="div" class="form-text">
<template #happyEyeballs>
<a href="https://en.wikipedia.org/wiki/Happy_Eyeballs" target="_blank">{{ $t("Happy Eyeballs algorithm") }}</a>
</template>
</i18n-t>
</div>
</template> </template>
<!-- Parent Monitor --> <!-- Parent Monitor -->
@ -1060,7 +1137,13 @@ import DockerHostDialog from "../components/DockerHostDialog.vue";
import RemoteBrowserDialog from "../components/RemoteBrowserDialog.vue"; import RemoteBrowserDialog from "../components/RemoteBrowserDialog.vue";
import ProxyDialog from "../components/ProxyDialog.vue"; import ProxyDialog from "../components/ProxyDialog.vue";
import TagsManager from "../components/TagsManager.vue"; import TagsManager from "../components/TagsManager.vue";
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, sleep } from "../util.ts"; import {
genSecret,
isDev,
MAX_INTERVAL_SECOND,
MIN_INTERVAL_SECOND,
sleep,
} from "../util.ts";
import { hostNameRegexPattern } from "../util-frontend"; import { hostNameRegexPattern } from "../util-frontend";
import HiddenInput from "../components/HiddenInput.vue"; import HiddenInput from "../components/HiddenInput.vue";
import EditMonitorConditions from "../components/EditMonitorConditions.vue"; import EditMonitorConditions from "../components/EditMonitorConditions.vue";
@ -1075,6 +1158,7 @@ const monitorDefaults = {
parent: null, parent: null,
url: "https://", url: "https://",
method: "GET", method: "GET",
ipFamily: null,
interval: 60, interval: 60,
retryInterval: 60, retryInterval: 60,
resendInterval: 0, resendInterval: 0,
@ -1082,7 +1166,6 @@ const monitorDefaults = {
notificationIDList: {}, notificationIDList: {},
ignoreTls: false, ignoreTls: false,
upsideDown: false, upsideDown: false,
packetSize: 56,
expiryNotification: false, expiryNotification: false,
maxredirects: 10, maxredirects: 10,
accepted_statuscodes: [ "200-299" ], accepted_statuscodes: [ "200-299" ],
@ -1157,6 +1240,29 @@ export default {
}, },
computed: { computed: {
timeoutStep() {
return this.monitor.type === "ping" ? 1 : 0.1;
},
timeoutMin() {
return this.monitor.type === "ping" ? 1 : 0;
},
timeoutMax() {
return this.monitor.type === "ping" ? 60 : undefined;
},
timeoutLabel() {
return this.monitor.type === "ping" ? this.$t("pingTimeoutLabel") : this.$t("Request Timeout");
},
timeoutDescription() {
if (this.monitor.type === "ping") {
return this.$t("pingTimeoutDescription");
}
return "";
},
defaultFriendlyName() { defaultFriendlyName() {
if (this.monitor.hostname) { if (this.monitor.hostname) {
return this.monitor.hostname; return this.monitor.hostname;
@ -1194,6 +1300,7 @@ export default {
} }
return this.$t(name); return this.$t(name);
}, },
remoteBrowsersOptions() { remoteBrowsersOptions() {
return this.$root.remoteBrowserList.map(browser => { return this.$root.remoteBrowserList.map(browser => {
return { return {
@ -1202,6 +1309,7 @@ export default {
}; };
}); });
}, },
remoteBrowsersToggle: { remoteBrowsersToggle: {
get() { get() {
return this.remoteBrowsersEnabled || this.monitor.remote_browser != null; return this.remoteBrowsersEnabled || this.monitor.remote_browser != null;
@ -1219,6 +1327,7 @@ export default {
} }
} }
}, },
isAdd() { isAdd() {
return this.$route.path === "/add"; return this.$route.path === "/add";
}, },
@ -1269,6 +1378,7 @@ message HealthCheckResponse {
} }
` ]); ` ]);
}, },
bodyPlaceholder() { bodyPlaceholder() {
if (this.monitor && this.monitor.httpBodyEncoding && this.monitor.httpBodyEncoding === "xml") { if (this.monitor && this.monitor.httpBodyEncoding && this.monitor.httpBodyEncoding === "xml") {
return this.$t("Example:", [ ` return this.$t("Example:", [ `
@ -1434,9 +1544,25 @@ message HealthCheckResponse {
}, },
"monitor.timeout"(value, oldValue) { "monitor.timeout"(value, oldValue) {
// keep timeout within 80% range if (this.monitor.type === "ping") {
if (value && value !== oldValue) { this.finishUpdateInterval();
this.monitor.timeout = this.clampTimeout(value); } else {
// keep timeout within 80% range
if (value && value !== oldValue) {
this.monitor.timeout = this.clampTimeout(value);
}
}
},
"monitor.ping_count"() {
if (this.monitor.type === "ping") {
this.finishUpdateInterval();
}
},
"monitor.ping_per_request_timeout"() {
if (this.monitor.type === "ping") {
this.finishUpdateInterval();
} }
}, },
@ -1465,8 +1591,10 @@ message HealthCheckResponse {
// Set a default timeout if the monitor type has changed or if it's a new monitor // Set a default timeout if the monitor type has changed or if it's a new monitor
if (oldType || this.isAdd) { if (oldType || this.isAdd) {
if (this.monitor.type === "snmp") { if (this.monitor.type === "snmp") {
// snmp is not expected to be executed via the internet => we can choose a lower default timeout // snmp is not expected to be executed via the internet => we can choose a lower default timeout
this.monitor.timeout = 5; this.monitor.timeout = 5;
} else if (this.monitor.type === "ping") {
this.monitor.timeout = 10;
} else { } else {
this.monitor.timeout = 48; this.monitor.timeout = 48;
} }
@ -1583,7 +1711,11 @@ message HealthCheckResponse {
if (this.isAdd) { if (this.isAdd) {
this.monitor = { this.monitor = {
...monitorDefaults ...monitorDefaults,
ping_count: 3,
ping_numeric: true,
packetSize: 56,
ping_per_request_timeout: 2,
}; };
if (this.$root.proxyList && !this.monitor.proxyId) { if (this.$root.proxyList && !this.monitor.proxyId) {
@ -1646,7 +1778,12 @@ message HealthCheckResponse {
} }
// Handling for monitors that are missing/zeroed timeout // Handling for monitors that are missing/zeroed timeout
if (!this.monitor.timeout) { if (!this.monitor.timeout) {
this.monitor.timeout = ~~(this.monitor.interval * 8) / 10; if (this.monitor.type === "ping") {
// set to default
this.monitor.timeout = 10;
} else {
this.monitor.timeout = ~~(this.monitor.interval * 8) / 10;
}
} }
} else { } else {
this.$root.toastError(res.msg); this.$root.toastError(res.msg);
@ -1863,11 +2000,48 @@ message HealthCheckResponse {
return Number.isFinite(clamped) ? clamped : maxTimeout; return Number.isFinite(clamped) ? clamped : maxTimeout;
}, },
calculatePingInterval() {
// If monitor.type is not "ping", simply return the configured interval
if (this.monitor.type !== "ping") {
return this.monitor.interval;
}
// Calculate the maximum theoretical time needed if every ping request times out
const theoreticalTotal = this.monitor.ping_count * this.monitor.ping_per_request_timeout;
// The global timeout (aka deadline) forces ping to terminate, so the effective limit
// is the smaller value between deadline and theoreticalTotal
const effectiveLimit = Math.min(this.monitor.timeout, theoreticalTotal);
// Add a 10% margin to the effective limit to ensure proper handling
const adjustedLimit = Math.ceil(effectiveLimit * 1.1);
// If the calculated limit is lower than the minimum allowed interval, use the minimum interval
if (adjustedLimit < this.minInterval) {
return this.minInterval;
}
return adjustedLimit;
},
finishUpdateInterval() { finishUpdateInterval() {
// Update timeout if it is greater than the clamp timeout if (this.monitor.type === "ping") {
let clampedValue = this.clampTimeout(this.monitor.interval); // Calculate the minimum required interval based on ping configuration
if (this.monitor.timeout > clampedValue) { const calculatedPingInterval = this.calculatePingInterval();
this.monitor.timeout = clampedValue;
// If the configured interval is too small, adjust it to the minimum required value
if (this.monitor.interval < calculatedPingInterval) {
this.monitor.interval = calculatedPingInterval;
// Notify the user that the interval has been automatically adjusted
toast.info(this.$t("pingIntervalAdjustedInfo"));
}
} else {
// Update timeout if it is greater than the clamp timeout
let clampedValue = this.clampTimeout(this.monitor.interval);
if (this.monitor.timeout > clampedValue) {
this.monitor.timeout = clampedValue;
}
} }
}, },

View file

@ -157,12 +157,12 @@
<!-- Admin functions --> <!-- Admin functions -->
<div v-if="hasToken" class="mb-4"> <div v-if="hasToken" class="mb-4">
<div v-if="!enableEditMode"> <div v-if="!enableEditMode">
<button class="btn btn-info me-2" data-testid="edit-button" @click="edit"> <button class="btn btn-primary me-2" data-testid="edit-button" @click="edit">
<font-awesome-icon icon="edit" /> <font-awesome-icon icon="edit" />
{{ $t("Edit Status Page") }} {{ $t("Edit Status Page") }}
</button> </button>
<a href="/manage-status-page" class="btn btn-info"> <a href="/manage-status-page" class="btn btn-primary">
<font-awesome-icon icon="tachometer-alt" /> <font-awesome-icon icon="tachometer-alt" />
{{ $t("Go to Dashboard") }} {{ $t("Go to Dashboard") }}
</a> </a>

View file

@ -8,19 +8,15 @@
// Backend uses the compiled file util.js // Backend uses the compiled file util.js
// Frontend uses util.ts // Frontend uses util.ts
*/ */
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _a; var _a;
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0; exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_MAX = exports.PING_PER_REQUEST_TIMEOUT_MIN = exports.PING_COUNT_DEFAULT = exports.PING_COUNT_MAX = exports.PING_COUNT_MIN = exports.PING_GLOBAL_TIMEOUT_DEFAULT = exports.PING_GLOBAL_TIMEOUT_MAX = exports.PING_GLOBAL_TIMEOUT_MIN = exports.PING_PACKET_SIZE_DEFAULT = exports.PING_PACKET_SIZE_MAX = exports.PING_PACKET_SIZE_MIN = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0;
exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = void 0; exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = void 0;
exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = void 0; const dayjs_1 = require("dayjs");
const dayjs_1 = __importDefault(require("dayjs"));
const dayjs = require("dayjs");
const jsonata = require("jsonata"); const jsonata = require("jsonata");
exports.isDev = process.env.NODE_ENV === "development"; exports.isDev = process.env.NODE_ENV === "development";
exports.isNode = typeof process !== "undefined" && ((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node); exports.isNode = typeof process !== "undefined" && ((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node);
const dayjs = (exports.isNode) ? require("dayjs") : dayjs_1.default;
exports.appName = "Uptime Kuma"; exports.appName = "Uptime Kuma";
exports.DOWN = 0; exports.DOWN = 0;
exports.UP = 1; exports.UP = 1;
@ -35,6 +31,18 @@ exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm"; exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
exports.MAX_INTERVAL_SECOND = 2073600; exports.MAX_INTERVAL_SECOND = 2073600;
exports.MIN_INTERVAL_SECOND = 20; exports.MIN_INTERVAL_SECOND = 20;
exports.PING_PACKET_SIZE_MIN = 1;
exports.PING_PACKET_SIZE_MAX = 65500;
exports.PING_PACKET_SIZE_DEFAULT = 56;
exports.PING_GLOBAL_TIMEOUT_MIN = 1;
exports.PING_GLOBAL_TIMEOUT_MAX = 300;
exports.PING_GLOBAL_TIMEOUT_DEFAULT = 10;
exports.PING_COUNT_MIN = 1;
exports.PING_COUNT_MAX = 100;
exports.PING_COUNT_DEFAULT = 1;
exports.PING_PER_REQUEST_TIMEOUT_MIN = 1;
exports.PING_PER_REQUEST_TIMEOUT_MAX = 60;
exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
exports.CONSOLE_STYLE_Reset = "\x1b[0m"; exports.CONSOLE_STYLE_Reset = "\x1b[0m";
exports.CONSOLE_STYLE_Bright = "\x1b[1m"; exports.CONSOLE_STYLE_Bright = "\x1b[1m";
exports.CONSOLE_STYLE_Dim = "\x1b[2m"; exports.CONSOLE_STYLE_Dim = "\x1b[2m";
@ -66,7 +74,6 @@ exports.CONSOLE_STYLE_BgMagenta = "\x1b[45m";
exports.CONSOLE_STYLE_BgCyan = "\x1b[46m"; exports.CONSOLE_STYLE_BgCyan = "\x1b[46m";
exports.CONSOLE_STYLE_BgWhite = "\x1b[47m"; exports.CONSOLE_STYLE_BgWhite = "\x1b[47m";
exports.CONSOLE_STYLE_BgGray = "\x1b[100m"; exports.CONSOLE_STYLE_BgGray = "\x1b[100m";
const consoleModuleColors = [ const consoleModuleColors = [
exports.CONSOLE_STYLE_FgCyan, exports.CONSOLE_STYLE_FgCyan,
exports.CONSOLE_STYLE_FgGreen, exports.CONSOLE_STYLE_FgGreen,
@ -159,11 +166,11 @@ class Logger {
module = module.toUpperCase(); module = module.toUpperCase();
level = level.toUpperCase(); level = level.toUpperCase();
let now; let now;
if (dayjs_1.default.tz) { if (dayjs.tz) {
now = dayjs_1.default.tz(new Date()).format(); now = dayjs.tz(new Date()).format();
} }
else { else {
now = (0, dayjs_1.default)().format(); now = dayjs().format();
} }
const levelColor = consoleLevelColors[level]; const levelColor = consoleLevelColors[level];
const moduleColor = consoleModuleColors[intHash(module, consoleModuleColors.length)]; const moduleColor = consoleModuleColors[intHash(module, consoleModuleColors.length)];
@ -264,11 +271,11 @@ function polyfill() {
exports.polyfill = polyfill; exports.polyfill = polyfill;
class TimeLogger { class TimeLogger {
constructor() { constructor() {
this.startTime = (0, dayjs_1.default)().valueOf(); this.startTime = dayjs().valueOf();
} }
print(name) { print(name) {
if (exports.isDev && process.env.TIMELOGGER === "1") { if (exports.isDev && process.env.TIMELOGGER === "1") {
console.log(name + ": " + ((0, dayjs_1.default)().valueOf() - this.startTime) + "ms"); console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
} }
} }
} }
@ -380,19 +387,19 @@ function parseTimeFromTimeObject(obj) {
} }
exports.parseTimeFromTimeObject = parseTimeFromTimeObject; exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
function isoToUTCDateTime(input) { function isoToUTCDateTime(input) {
return (0, dayjs_1.default)(input).utc().format(exports.SQL_DATETIME_FORMAT); return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
} }
exports.isoToUTCDateTime = isoToUTCDateTime; exports.isoToUTCDateTime = isoToUTCDateTime;
function utcToISODateTime(input) { function utcToISODateTime(input) {
return dayjs_1.default.utc(input).toISOString(); return dayjs.utc(input).toISOString();
} }
exports.utcToISODateTime = utcToISODateTime; exports.utcToISODateTime = utcToISODateTime;
function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) { function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
return dayjs_1.default.utc(input).local().format(format); return dayjs.utc(input).local().format(format);
} }
exports.utcToLocal = utcToLocal; exports.utcToLocal = utcToLocal;
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) { function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
return (0, dayjs_1.default)(input).utc().format(format); return dayjs(input).utc().format(format);
} }
exports.localToUTC = localToUTC; exports.localToUTC = localToUTC;
function intHash(str, length = 10) { function intHash(str, length = 10) {

View file

@ -9,9 +9,9 @@
// Frontend uses util.ts // Frontend uses util.ts
*/ */
import dayjs from "dayjs"; import dayjsFrontend from "dayjs";
// For loading dayjs plugins, don't remove event though it is not used in this file // For dayjs plugins' type checking, don't remove event though it is not used in this file
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import * as timezone from "dayjs/plugin/timezone"; import * as timezone from "dayjs/plugin/timezone";
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -21,6 +21,13 @@ import * as jsonata from "jsonata";
export const isDev = process.env.NODE_ENV === "development"; export const isDev = process.env.NODE_ENV === "development";
export const isNode = typeof process !== "undefined" && process?.versions?.node; export const isNode = typeof process !== "undefined" && process?.versions?.node;
/**
* Smarter dayjs import that supports both frontend and backend
* @returns {dayjs.Dayjs} dayjs instance
*/
const dayjs = (isNode) ? require("dayjs") : dayjsFrontend;
export const appName = "Uptime Kuma"; export const appName = "Uptime Kuma";
export const DOWN = 0; export const DOWN = 0;
export const UP = 1; export const UP = 1;
@ -39,6 +46,26 @@ export const SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
export const MAX_INTERVAL_SECOND = 2073600; // 24 days export const MAX_INTERVAL_SECOND = 2073600; // 24 days
export const MIN_INTERVAL_SECOND = 20; // 20 seconds export const MIN_INTERVAL_SECOND = 20; // 20 seconds
// Packet Size limits
export const PING_PACKET_SIZE_MIN = 1;
export const PING_PACKET_SIZE_MAX = 65500;
export const PING_PACKET_SIZE_DEFAULT = 56;
// Global timeout (aka deadline) limits in seconds
export const PING_GLOBAL_TIMEOUT_MIN = 1;
export const PING_GLOBAL_TIMEOUT_MAX = 300;
export const PING_GLOBAL_TIMEOUT_DEFAULT = 10;
// Ping count limits
export const PING_COUNT_MIN = 1;
export const PING_COUNT_MAX = 100;
export const PING_COUNT_DEFAULT = 1;
// per-request timeout (aka timeout) limits in seconds
export const PING_PER_REQUEST_TIMEOUT_MIN = 1;
export const PING_PER_REQUEST_TIMEOUT_MAX = 60;
export const PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
// Console colors // Console colors
// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color // https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
export const CONSOLE_STYLE_Reset = "\x1b[0m"; export const CONSOLE_STYLE_Reset = "\x1b[0m";

View file

@ -8,10 +8,14 @@ test.describe("Status Page", () => {
}); });
test("create and edit", async ({ page }, testInfo) => { test("create and edit", async ({ page }, testInfo) => {
test.setTimeout(60000); // Keep the timeout increase for stability
// Monitor // Monitor
const monitorName = "Monitor for Status Page"; const monitorName = "Monitor for Status Page";
const tagName = "Client"; const tagName = "Client";
const tagValue = "Acme Inc"; const tagValue = "Acme Inc";
const tagName2 = "Project"; // Add second tag name
const tagValue2 = "Phoenix"; // Add second tag value
const monitorUrl = "https://www.example.com/status"; const monitorUrl = "https://www.example.com/status";
const monitorCustomUrl = "https://www.example.com"; const monitorCustomUrl = "https://www.example.com";
@ -33,12 +37,26 @@ test.describe("Status Page", () => {
await page.getByTestId("monitor-type-select").selectOption("http"); await page.getByTestId("monitor-type-select").selectOption("http");
await page.getByTestId("friendly-name-input").fill(monitorName); await page.getByTestId("friendly-name-input").fill(monitorName);
await page.getByTestId("url-input").fill(monitorUrl); await page.getByTestId("url-input").fill(monitorUrl);
// Modified tag section to add multiple tags
await page.getByTestId("add-tag-button").click(); await page.getByTestId("add-tag-button").click();
await page.getByTestId("tag-name-input").fill(tagName); await page.getByTestId("tag-name-input").fill(tagName);
await page.getByTestId("tag-value-input").fill(tagValue); await page.getByTestId("tag-value-input").fill(tagValue);
await page.getByTestId("tag-color-select").click(); // Vue-Multiselect component await page.getByTestId("tag-color-select").click(); // Vue-Multiselect component
await page.getByTestId("tag-color-select").getByRole("option", { name: "Orange" }).click(); await page.getByTestId("tag-color-select").getByRole("option", { name: "Orange" }).click();
await page.getByTestId("tag-submit-button").click();
// Add another tag instead of submitting directly
await page.getByRole("button", { name: "Add Another Tag" }).click();
// Add second tag
await page.getByTestId("tag-name-input").fill(tagName2);
await page.getByTestId("tag-value-input").fill(tagValue2);
await page.getByTestId("tag-color-select").click();
await page.getByTestId("tag-color-select").getByRole("option", { name: "Blue" }).click();
// Submit both tags
await page.getByTestId("add-tags-final-button").click();
await page.getByTestId("save-button").click(); await page.getByTestId("save-button").click();
await page.waitForURL("/dashboard/*"); // wait for the monitor to be created await page.waitForURL("/dashboard/*"); // wait for the monitor to be created
@ -61,8 +79,6 @@ test.describe("Status Page", () => {
await page.getByTestId("show-certificate-expiry-checkbox").uncheck(); await page.getByTestId("show-certificate-expiry-checkbox").uncheck();
await page.getByTestId("google-analytics-input").fill(googleAnalyticsId); await page.getByTestId("google-analytics-input").fill(googleAnalyticsId);
await page.getByTestId("custom-css-input").getByTestId("textarea").fill(customCss); // Prism await page.getByTestId("custom-css-input").getByTestId("textarea").fill(customCss); // Prism
await expect(page.getByTestId("description-editable")).toHaveText(descriptionText);
await expect(page.getByTestId("custom-footer-editable")).toHaveText(footerText);
// Add an incident // Add an incident
await page.getByTestId("create-incident-button").click(); await page.getByTestId("create-incident-button").click();
@ -98,9 +114,7 @@ test.describe("Status Page", () => {
await expect(page.getByTestId("incident")).toHaveCount(1); await expect(page.getByTestId("incident")).toHaveCount(1);
await expect(page.getByTestId("incident-title")).toContainText(incidentTitle); await expect(page.getByTestId("incident-title")).toContainText(incidentTitle);
await expect(page.getByTestId("incident-content")).toContainText(incidentContent); await expect(page.getByTestId("incident-content")).toContainText(incidentContent);
await expect(page.getByTestId("description")).toContainText(descriptionText);
await expect(page.getByTestId("group-name")).toContainText(groupName); await expect(page.getByTestId("group-name")).toContainText(groupName);
await expect(page.getByTestId("footer-text")).toContainText(footerText);
await expect(page.getByTestId("powered-by")).toHaveCount(0); await expect(page.getByTestId("powered-by")).toHaveCount(0);
await expect(page.getByTestId("monitor-name")).toHaveAttribute("href", monitorCustomUrl); await expect(page.getByTestId("monitor-name")).toHaveAttribute("href", monitorCustomUrl);
@ -111,6 +125,11 @@ test.describe("Status Page", () => {
expect(updateCountdown).toBeLessThanOrEqual(refreshInterval + 10); expect(updateCountdown).toBeLessThanOrEqual(refreshInterval + 10);
await expect(page.locator("body")).toHaveClass(theme); await expect(page.locator("body")).toHaveClass(theme);
// Add Google Analytics ID to head and verify
await page.waitForFunction(() => {
return document.head.innerHTML.includes("https://www.googletagmanager.com/gtag/js?id=");
}, { timeout: 5000 });
expect(await page.locator("head").innerHTML()).toContain(googleAnalyticsId); expect(await page.locator("head").innerHTML()).toContain(googleAnalyticsId);
const backgroundColor = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor); const backgroundColor = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor);
@ -129,7 +148,10 @@ test.describe("Status Page", () => {
await expect(page.getByTestId("edit-sidebar")).toHaveCount(0); await expect(page.getByTestId("edit-sidebar")).toHaveCount(0);
await expect(page.getByTestId("powered-by")).toContainText("Powered by"); await expect(page.getByTestId("powered-by")).toContainText("Powered by");
await expect(page.getByTestId("monitor-tag")).toContainText(tagValue);
// Modified tag verification to check both tags
await expect(page.getByTestId("monitor-tag").filter({ hasText: tagValue })).toBeVisible();
await expect(page.getByTestId("monitor-tag").filter({ hasText: tagValue2 })).toBeVisible();
await screenshot(testInfo, page); await screenshot(testInfo, page);
}); });

9
tsconfig-backend.json Normal file
View file

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"esModuleInterop": false
},
"files": [
"./src/util.ts"
]
}

View file

@ -16,6 +16,6 @@
"esModuleInterop": true "esModuleInterop": true
}, },
"files": [ "files": [
"./src/util.ts"
] ]
} }