Merge branch 'master' into 2.0.X

# Conflicts:
#	package-lock.json
#	server/database.js
#	server/util-server.js
pull/3529/head^2
Louis Lam 9 months ago
commit 3e0f5f4231

@ -18,6 +18,7 @@ README.md
.vscode
.eslint*
.stylelint*
/.devcontainer
/.github
yarn.lock
app.json
@ -35,6 +36,7 @@ tsconfig.json
/extra/healthcheck
extra/exe-builder
### .gitignore content (commented rules are duplicated)
#node_modules

@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
node: [ 14, 18 ]
node: [ 14, 20 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
@ -50,7 +50,7 @@ jobs:
strategy:
matrix:
os: [ ARMv7 ]
node: [ 14.21.3, 18.16.1 ]
node: [ 14.21.3, 20.5.0 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:

@ -10,6 +10,7 @@
"color-function-notation": "legacy",
"shorthand-property-no-redundant-values": null,
"color-hex-length": null,
"declaration-block-no-redundant-longhand-properties": null
"declaration-block-no-redundant-longhand-properties": null,
"at-rule-no-unknown": null
}
}

@ -34,19 +34,19 @@ Yes or no, it depends on what you will try to do. Since I don't want to waste yo
Here are some references:
✅ Usually Accept:
### ✅ Usually accepted:
- Bug fix
- Security fix
- Adding notification providers
- Adding new language files (You should go to https://weblate.kuma.pet for existing languages)
- Adding new language files (see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md))
- Adding new language keys: `$t("...")`
⚠️ Discussion First
### ⚠️ Discussion required:
- Large pull requests
- New features
❌ Won't Merge
- A dedicated pr for translating existing languages (You can now translate on https://weblate.kuma.pet)
### ❌ Won't be merged:
- A dedicated pr for translating existing languages (see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md))
- Do not pass the auto test
- Any breaking changes
- Duplicated pull requests
@ -106,11 +106,11 @@ I personally do not like something that requires so many configurations before y
## Tools
- Node.js >= 14
- NPM >= 8.5
- Git
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
- A SQLite GUI tool (SQLite Expert Personal is suggested)
- [`Node.js`](https://nodejs.org/) >= 14
- [`npm`](https://www.npmjs.com/) >= 8.5
- [`git`](https://git-scm.com/)
- IDE that supports [`ESLint`](https://eslint.org/) and EditorConfig (I am using [`IntelliJ IDEA`](https://www.jetbrains.com/idea/))
- A SQLite GUI tool (f.ex. [`SQLite Expert Personal`](https://www.sqliteexpert.com/download.html) or [`DBeaver Community`](https://dbeaver.io/download/))
## Install Dependencies for Development
@ -218,7 +218,17 @@ If for maybe security reasons, a library must be updated. Then you must need to
## Translations
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
Please add **all** the strings which are translatable to `src/lang/en.json` (If translation keys are ommited, they can not be translated).
**Don't include any other languages in your inital Pull-Request** (even if this is your mother tounge), to avoid merge-conflicts between weblate and `master`.
The translations can then (after merging a PR into `master`) be translated by awesome people donating their language-skills.
If you want to help by translating Uptime Kuma into your language, please visit the [instructions on how to translate using weblate](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
## Spelling & Grammar
Feel free to correct the grammar in the documentation or code.
My mother language is not english and my grammar is not that great.
## Wiki

@ -1,16 +1,16 @@
<div align="center" width="100%">
<img src="./public/icon.svg" width="128" alt="" />
</div>
# Uptime Kuma
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Open%20Collective%20Backers&color=brightgreen" /></a>
[![GitHub Sponsors](https://img.shields.io/github/sponsors/louislam?label=GitHub%20Sponsors)](https://github.com/sponsors/louislam) <a href="https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/">
<img src="https://weblate.kuma.pet/widgets/uptime-kuma/-/svg-badge.svg" alt="Translation status" />
</a>
<div align="center" width="100%">
<img src="./public/icon.svg" width="128" alt="" />
</div>
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
<img src="https://user-images.githubusercontent.com/1336778/212262296-e6205815-ad62-488c-83ec-a5b0d0689f7c.jpg" width="700" alt="" />
## 🥔 Live Demo
@ -184,7 +184,10 @@ If you want to report a bug or request a new feature, feel free to open a [new i
### Translations
If you want to translate Uptime Kuma into your language, please visit [Weblate Readme](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
Feel free to correct my grammar in this README, source code, or wiki, as my mother language is not English and my grammar is not that great.
## Spelling & Grammar
Feel free to correct the grammar in the documentation or code.
My mother language is not english and my grammar is not that great.
### Create Pull Requests
If you want to modify Uptime Kuma, please read this guide and follow the rules here: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md

@ -4,8 +4,4 @@ if (process.env.TEST_FRONTEND) {
config.presets = [ "@babel/preset-env" ];
}
if (process.env.TEST_BACKEND) {
config.plugins = [ "babel-plugin-rewire" ];
}
module.exports = config;

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

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

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

@ -0,0 +1,19 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD oauth_client_id TEXT default null;
ALTER TABLE monitor
ADD oauth_client_secret TEXT default null;
ALTER TABLE monitor
ADD oauth_token_url TEXT default null;
ALTER TABLE monitor
ADD oauth_scopes TEXT default null;
ALTER TABLE monitor
ADD oauth_auth_method TEXT default null;
COMMIT;

@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.22.1",
"version": "1.23.0-beta.1",
"license": "MIT",
"repository": {
"type": "git",
@ -99,6 +99,7 @@
"http-proxy-agent": "~5.0.0",
"https-proxy-agent": "~5.0.1",
"iconv-lite": "~0.6.3",
"isomorphic-ws": "^5.0.0",
"jsesc": "~3.0.2",
"jsonata": "^2.0.3",
"jsonwebtoken": "~9.0.0",
@ -115,7 +116,9 @@
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.6.5",
"nostr-tools": "^1.13.1",
"notp": "~2.0.3",
"openid-client": "^5.4.2",
"password-hash": "~1.2.2",
"pg": "~8.8.0",
"pg-connection-string": "~2.5.0",
@ -132,7 +135,8 @@
"socks-proxy-agent": "6.1.1",
"tar": "~6.1.11",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2"
"thirty-two": "~1.0.2",
"ws": "^8.13.0"
},
"devDependencies": {
"@actions/github": "~5.0.1",
@ -149,7 +153,6 @@
"@vue/compiler-sfc": "~3.3.4",
"@vuepic/vue-datepicker": "~3.4.8",
"aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0",
"bootstrap": "5.1.3",
"chart.js": "~4.2.1",
"chartjs-adapter-dayjs-4": "~1.0.4",

@ -28,6 +28,8 @@ class Database {
static sqlitePath;
static dockerTLSDir;
/**
* @type {boolean}
*/
@ -79,6 +81,10 @@ class Database {
"patch-add-invert-keyword.sql": true,
"patch-added-json-query.sql": true,
"patch-added-kafka-producer.sql": true,
"patch-add-certificate-expiry-status-page.sql": true,
"patch-monitor-oauth-cc.sql": true,
"patch-add-timeout-monitor.sql": true,
"patch-add-gamedig-given-port.sql": true,
};
/**
@ -101,23 +107,28 @@ class Database {
// Data Directory (must be end with "/")
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
Database.sqlitePath = Database.dataDir + "kuma.db";
Database.sqlitePath = path.join(Database.dataDir, "kuma.db");
if (! fs.existsSync(Database.dataDir)) {
fs.mkdirSync(Database.dataDir, { recursive: true });
}
Database.uploadDir = Database.dataDir + "upload/";
Database.uploadDir = path.join(Database.dataDir, "upload/");
if (! fs.existsSync(Database.uploadDir)) {
fs.mkdirSync(Database.uploadDir, { recursive: true });
}
// Create screenshot dir
Database.screenshotDir = Database.dataDir + "screenshots/";
Database.screenshotDir = path.join(Database.dataDir, "screenshots/");
if (! fs.existsSync(Database.screenshotDir)) {
fs.mkdirSync(Database.screenshotDir, { recursive: true });
}
Database.dockerTLSDir = path.join(Database.dataDir, "docker-tls/");
if (! fs.existsSync(Database.dockerTLSDir)) {
fs.mkdirSync(Database.dockerTLSDir, { recursive: true });
}
log.info("db", `Data Dir: ${Database.dataDir}`);
}

@ -2,8 +2,16 @@ const axios = require("axios");
const { R } = require("redbean-node");
const version = require("../package.json").version;
const https = require("https");
const fs = require("fs");
const path = require("path");
const Database = require("./database");
class DockerHost {
static CertificateFileNameCA = "ca.pem";
static CertificateFileNameCert = "cert.pem";
static CertificateFileNameKey = "key.pem";
/**
* Save a docker host
* @param {Object} dockerHost Docker host to save
@ -66,10 +74,6 @@ class DockerHost {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version
},
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: false,
}),
};
if (dockerHost.dockerType === "socket") {
@ -77,6 +81,7 @@ class DockerHost {
} else if (dockerHost.dockerType === "tcp") {
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
}
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
let res = await axios.request(options);
@ -111,6 +116,53 @@ class DockerHost {
}
return url;
}
/**
* Returns HTTPS agent options with client side TLS parameters if certificate files
* for the given host are available under a predefined directory path.
*
* The base path where certificates are looked for can be set with the
* 'DOCKER_TLS_DIR_PATH' environmental variable or defaults to 'data/docker-tls/'.
*
* If a directory in this path exists with a name matching the FQDN of the docker host
* (e.g. the FQDN of 'https://example.com:2376' is 'example.com' so the directory
* 'data/docker-tls/example.com/' would be searched for certificate files),
* then 'ca.pem', 'key.pem' and 'cert.pem' files are included in the agent options.
* File names can also be overridden via 'DOCKER_TLS_FILE_NAME_(CA|KEY|CERT)'.
*
* @param {String} dockerType i.e. "tcp" or "socket"
* @param {String} url The docker host URL rewritten to https://
* @return {Object}
* */
static getHttpsAgentOptions(dockerType, url) {
let baseOptions = {
maxCachedSessions: 0,
rejectUnauthorized: true
};
let certOptions = {};
let dirName = (new URL(url)).hostname;
let caPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCA);
let certPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCert);
let keyPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameKey);
if (dockerType === "tcp" && fs.existsSync(caPath) && fs.existsSync(certPath) && fs.existsSync(keyPath)) {
let ca = fs.readFileSync(caPath);
let key = fs.readFileSync(keyPath);
let cert = fs.readFileSync(certPath);
certOptions = {
ca,
key,
cert
};
}
return {
...baseOptions,
...certOptions
};
}
}
module.exports = {

@ -9,12 +9,12 @@ class Group extends BeanModel {
* @param {boolean} [showTags=false] Should the JSON include monitor tags
* @returns {Object}
*/
async toPublicJSON(showTags = false) {
async toPublicJSON(showTags = false, certExpiry = false) {
let monitorBeanList = await this.getMonitorList();
let monitorList = [];
for (let bean of monitorBeanList) {
monitorList.push(await bean.toPublicJSON(showTags));
monitorList.push(await bean.toPublicJSON(showTags, certExpiry));
}
return {

@ -6,7 +6,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVA
SQL_DATETIME_FORMAT
} = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, mongodbPing, kafkaProducerAsync
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials,
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
@ -38,11 +38,12 @@ class Monitor extends BeanModel {
* Only show necessary data to public
* @returns {Object}
*/
async toPublicJSON(showTags = false) {
async toPublicJSON(showTags = false, certExpiry = false) {
let obj = {
id: this.id,
name: this.name,
sendUrl: this.sendUrl,
type: this.type,
};
if (this.sendUrl) {
@ -52,6 +53,13 @@ class Monitor extends BeanModel {
if (showTags) {
obj.tags = await this.getTags();
}
if (certExpiry && this.type === "http") {
const { certExpiryDaysRemaining, validCert } = await this.getCertExpiry(this.id);
obj.certExpiryDaysRemaining = certExpiryDaysRemaining;
obj.validCert = validCert;
}
return obj;
}
@ -95,6 +103,7 @@ class Monitor extends BeanModel {
active: await this.isActive(),
forceInactive: !await Monitor.isParentActive(this.id),
type: this.type,
timeout: this.timeout,
interval: this.interval,
retryInterval: this.retryInterval,
resendInterval: this.resendInterval,
@ -127,6 +136,7 @@ class Monitor extends BeanModel {
radiusCalledStationId: this.radiusCalledStationId,
radiusCallingStationId: this.radiusCallingStationId,
game: this.game,
gamedigGivenPortOnly: this.getGameDigGivenPortOnly(),
httpBodyEncoding: this.httpBodyEncoding,
jsonPath: this.jsonPath,
expectedValue: this.expectedValue,
@ -147,6 +157,11 @@ class Monitor extends BeanModel {
grpcMetadata: this.grpcMetadata,
basic_auth_user: this.basic_auth_user,
basic_auth_pass: this.basic_auth_pass,
oauth_client_id: this.oauth_client_id,
oauth_client_secret: this.oauth_client_secret,
oauth_token_url: this.oauth_token_url,
oauth_scopes: this.oauth_scopes,
oauth_auth_method: this.oauth_auth_method,
pushToken: this.pushToken,
databaseConnectionString: this.databaseConnectionString,
radiusUsername: this.radiusUsername,
@ -185,6 +200,31 @@ class Monitor extends BeanModel {
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ? ORDER BY tag.name", [ this.id ]);
}
/**
* Gets certificate expiry for this monitor
* @param {number} monitorID ID of monitor to send
* @returns {Promise<LooseObject<any>>}
*/
async getCertExpiry(monitorID) {
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
monitorID,
]);
let tlsInfo;
if (tlsInfoBean) {
tlsInfo = JSON.parse(tlsInfoBean?.info_json);
if (tlsInfo?.valid && tlsInfo?.certInfo?.daysRemaining) {
return {
certExpiryDaysRemaining: tlsInfo.certInfo.daysRemaining,
validCert: true
};
}
}
return {
certExpiryDaysRemaining: "",
validCert: false
};
}
/**
* Encode user and password to Base64 encoding
* for HTTP "basic" auth, as per RFC-7617
@ -242,6 +282,10 @@ class Monitor extends BeanModel {
return JSON.parse(this.accepted_statuscodes_json);
}
getGameDigGivenPortOnly() {
return Boolean(this.gamedigGivenPortOnly);
}
/**
* Start monitor
* @param {Server} io Socket server instance
@ -314,7 +358,10 @@ class Monitor extends BeanModel {
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
// Only change state if the monitor is in worse conditions then the ones before
if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
// lastBeat.status could be null
if (!lastBeat) {
bean.status = PENDING;
} else if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
bean.status = lastBeat.status;
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
bean.status = lastBeat.status;
@ -342,6 +389,24 @@ class Monitor extends BeanModel {
};
}
// OIDC: Basic client credential flow.
// Additional grants might be implemented in the future
let oauth2AuthHeader = {};
if (this.auth_method === "oauth2-cc") {
try {
if (this.oauthAccessToken === undefined || new Date(this.oauthAccessToken.expires_at * 1000) <= new Date()) {
log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new one`);
this.oauthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method);
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken.expires_at * 1000)}`);
}
oauth2AuthHeader = {
"Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token,
};
} catch (e) {
throw new Error("The oauth config is invalid. " + e.message);
}
}
const httpsAgentOptions = {
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
@ -370,12 +435,13 @@ class Monitor extends BeanModel {
const options = {
url: this.url,
method: (this.method || "get").toLowerCase(),
timeout: this.interval * 1000 * 0.8,
timeout: this.timeout * 1000,
headers: {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"User-Agent": "Uptime-Kuma/" + version,
...(contentType ? { "Content-Type": contentType } : {}),
...(basicAuthHeader),
...(oauth2AuthHeader),
...(this.headers ? JSON.parse(this.headers) : {})
},
maxRedirects: this.maxredirects,
@ -589,7 +655,7 @@ class Monitor extends BeanModel {
}
let res = await axios.get(steamApiUrl, {
timeout: this.interval * 1000 * 0.8,
timeout: this.timeout * 1000,
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
@ -627,7 +693,7 @@ class Monitor extends BeanModel {
type: this.game,
host: this.hostname,
port: this.port,
givenPortOnly: true,
givenPortOnly: this.getGameDigGivenPortOnly(),
});
bean.msg = state.name;
@ -661,6 +727,9 @@ class Monitor extends BeanModel {
options.socketPath = dockerHost._dockerDaemon;
} else if (dockerHost._dockerType === "tcp") {
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
options.httpsAgent = CacheableDnsHttpAgent.getHttpsAgent(
DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
);
}
log.debug("monitor", `[${this.name}] Axios Request`);
@ -760,29 +829,19 @@ class Monitor extends BeanModel {
port = this.port;
}
try {
const resp = await radius(
this.hostname,
this.radiusUsername,
this.radiusPassword,
this.radiusCalledStationId,
this.radiusCallingStationId,
this.radiusSecret,
port,
this.interval * 1000 * 0.8,
);
if (resp.code) {
bean.msg = resp.code;
}
bean.status = UP;
} catch (error) {
bean.status = DOWN;
if (error.response?.code) {
bean.msg = error.response.code;
} else {
bean.msg = error.message;
}
}
const resp = await radius(
this.hostname,
this.radiusUsername,
this.radiusPassword,
this.radiusCalledStationId,
this.radiusCallingStationId,
this.radiusSecret,
port,
this.interval * 1000 * 0.4,
);
bean.msg = resp.code;
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "redis") {
let startTime = dayjs().valueOf();

@ -90,6 +90,8 @@ class StatusPage extends BeanModel {
* @param {StatusPage} statusPage
*/
static async getStatusPageData(statusPage) {
const config = await statusPage.toPublicJSON();
// Incident
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
statusPage.id,
@ -110,13 +112,13 @@ class StatusPage extends BeanModel {
]);
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags);
let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
publicGroupList.push(monitorGroup);
}
// Response
return {
config: await statusPage.toPublicJSON(),
config,
incident,
publicGroupList,
maintenanceList,
@ -234,6 +236,7 @@ class StatusPage extends BeanModel {
footerText: this.footer_text,
showPoweredBy: !!this.show_powered_by,
googleAnalyticsId: this.google_analytics_tag_id,
showCertificateExpiry: !!this.show_certificate_expiry,
};
}
@ -255,6 +258,7 @@ class StatusPage extends BeanModel {
footerText: this.footer_text,
showPoweredBy: !!this.show_powered_by,
googleAnalyticsId: this.google_analytics_tag_id,
showCertificateExpiry: !!this.show_certificate_expiry,
};
}

@ -0,0 +1,98 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
const successMessage = "Sent Successfully.";
class FlashDuty extends NotificationProvider {
name = "FlashDuty";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
try {
if (heartbeatJSON == null) {
const title = "Uptime Kuma Alert";
const monitor = {
type: "ping",
url: msg,
name: "https://flashcat.cloud"
};
return this.postNotification(notification, title, msg, monitor);
}
if (heartbeatJSON.status === UP) {
const title = "Uptime Kuma Monitor ✅ Up";
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "Ok");
}
if (heartbeatJSON.status === DOWN) {
const title = "Uptime Kuma Monitor 🔴 Down";
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, notification.flashdutySeverity);
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
/**
* Generate a monitor url from the monitors infomation
* @param {Object} monitorInfo Monitor details
* @returns {string|undefined}
*/
genMonitorUrl(monitorInfo) {
if (monitorInfo.type === "port" && monitorInfo.port) {
return monitorInfo.hostname + ":" + monitorInfo.port;
}
if (monitorInfo.hostname != null) {
return monitorInfo.hostname;
}
return monitorInfo.url;
}
/**
* Send the message
* @param {BeanModel} notification Message title
* @param {string} title Message
* @param {string} body Message
* @param {Object} monitorInfo Monitor details
* @param {string} eventStatus Monitor status (Info, Warning, Critical, Ok)
* @returns {string}
*/
async postNotification(notification, title, body, monitorInfo, eventStatus) {
const options = {
method: "POST",
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
headers: { "Content-Type": "application/json" },
data: {
description: `[${title}] [${monitorInfo.name}] ${body}`,
title,
event_status: eventStatus || "Info",
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7),
labels: monitorInfo?.tags?.reduce((acc, item) => ({ ...acc,
[item.name]: item.value
}), { resource: this.genMonitorUrl(monitorInfo) }),
}
};
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorInfo) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
}
let result = await axios.request(options);
if (result.status == null) {
throw new Error("FlashDuty notification failed with invalid response!");
}
if (result.status < 200 || result.status >= 300) {
throw new Error("FlashDuty notification failed with status code " + result.status);
}
if (result.statusText != null) {
return "FlashDuty notification succeed: " + result.statusText;
}
return successMessage;
}
}
module.exports = FlashDuty;

@ -0,0 +1,119 @@
const { log } = require("../../src/util");
const NotificationProvider = require("./notification-provider");
const {
relayInit,
getPublicKey,
getEventHash,
getSignature,
nip04,
nip19
} = require("nostr-tools");
// polyfills for node versions
const semver = require("semver");
const nodeVersion = process.version;
if (semver.lt(nodeVersion, "16.0.0")) {
log.warn("monitor", "Node <= 16 is unsupported for nostr, sorry :(");
} else if (semver.lt(nodeVersion, "18.0.0")) {
// polyfills for node 16
global.crypto = require("crypto");
global.WebSocket = require("isomorphic-ws");
if (typeof crypto !== "undefined" && !crypto.subtle && crypto.webcrypto) {
crypto.subtle = crypto.webcrypto.subtle;
}
} else if (semver.lt(nodeVersion, "20.0.0")) {
// polyfills for node 18
global.crypto = require("crypto");
global.WebSocket = require("isomorphic-ws");
} else {
// polyfills for node 20
global.WebSocket = require("isomorphic-ws");
}
class Nostr extends NotificationProvider {
name = "nostr";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
// All DMs should have same timestamp
const createdAt = Math.floor(Date.now() / 1000);
const senderPrivateKey = await this.getPrivateKey(notification.sender);
const senderPublicKey = getPublicKey(senderPrivateKey);
const recipientsPublicKeys = await this.getPublicKeys(notification.recipients);
// Create NIP-04 encrypted direct message event for each recipient
const events = [];
for (const recipientPublicKey of recipientsPublicKeys) {
const ciphertext = await nip04.encrypt(senderPrivateKey, recipientPublicKey, msg);
let event = {
kind: 4,
pubkey: senderPublicKey,
created_at: createdAt,
tags: [[ "p", recipientPublicKey ]],
content: ciphertext,
};
event.id = getEventHash(event);
event.sig = getSignature(event, senderPrivateKey);
events.push(event);
}
// Publish events to each relay
const relays = notification.relays.split("\n");
let successfulRelays = 0;
// Connect to each relay
for (const relayUrl of relays) {
const relay = relayInit(relayUrl);
try {
await relay.connect();
successfulRelays++;
// Publish events
for (const event of events) {
relay.publish(event);
}
} catch (error) {
continue;
} finally {
relay.close();
}
}
// Report success or failure
if (successfulRelays === 0) {
throw Error("Failed to connect to any relays.");
}
return `${successfulRelays}/${relays.length} relays connected.`;
}
async getPrivateKey(sender) {
try {
const senderDecodeResult = await nip19.decode(sender);
const { data } = senderDecodeResult;
return data;
} catch (error) {
throw new Error(`Failed to get private key: ${error.message}`);
}
}
async getPublicKeys(recipients) {
const recipientsList = recipients.split("\n");
const publicKeys = [];
for (const recipient of recipientsList) {
try {
const recipientDecodeResult = await nip19.decode(recipient);
const { type, data } = recipientDecodeResult;
if (type === "npub") {
publicKeys.push(data);
} else {
throw new Error("not an npub");
}
} catch (error) {
throw new Error(`Error decoding recipient: ${error}`);
}
}
return publicKeys;
}
}
module.exports = Nostr;

@ -8,7 +8,9 @@ class PushDeer extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
let pushdeerlink = "https://api2.pushdeer.com/message/push";
let endpoint = "/message/push";
let serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
let pushdeerlink = `${serverUrl.trim().replace(/\/*$/, "")}${endpoint}`;
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;

@ -21,11 +21,13 @@ const LineNotify = require("./notification-providers/linenotify");
const LunaSea = require("./notification-providers/lunasea");
const Matrix = require("./notification-providers/matrix");
const Mattermost = require("./notification-providers/mattermost");
const Nostr = require("./notification-providers/nostr");
const Ntfy = require("./notification-providers/ntfy");
const Octopush = require("./notification-providers/octopush");
const OneBot = require("./notification-providers/onebot");
const Opsgenie = require("./notification-providers/opsgenie");
const PagerDuty = require("./notification-providers/pagerduty");
const FlashDuty = require("./notification-providers/flashduty");
const PagerTree = require("./notification-providers/pagertree");
const PromoSMS = require("./notification-providers/promosms");
const Pushbullet = require("./notification-providers/pushbullet");
@ -84,11 +86,13 @@ class Notification {
new LunaSea(),
new Matrix(),
new Mattermost(),
new Nostr(),
new Ntfy(),
new Octopush(),
new OneBot(),
new Opsgenie(),
new PagerDuty(),
new FlashDuty(),
new PagerTree(),
new PromoSMS(),
new Pushbullet(),
@ -115,7 +119,6 @@ class Notification {
new GoAlert(),
new ZohoCliq()
];
for (let item of list) {
if (! item.name) {
throw new Error("Notification provider without name");

@ -49,7 +49,7 @@ if (! process.env.NODE_ENV) {
}
log.info("server", "Node Env: " + process.env.NODE_ENV);
log.info("server", "Inside Container: " + process.env.UPTIME_KUMA_IS_CONTAINER === "1");
log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
log.info("server", "Importing Node libraries");
const fs = require("fs");
@ -670,6 +670,10 @@ let needSetup = false;
let notificationIDList = monitor.notificationIDList;
delete monitor.notificationIDList;
// Ensure status code ranges are strings
if (!monitor.accepted_statuscodes.every((code) => typeof code === "string")) {
throw new Error("Accepted status codes are not all strings");
}
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes;
@ -686,7 +690,10 @@ let needSetup = false;
await updateMonitorNotification(bean.id, notificationIDList);
await server.sendMonitorList(socket);
await startMonitor(socket.userID, bean.id);
if (monitor.active !== false) {
await startMonitor(socket.userID, bean.id);
}
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
@ -732,6 +739,11 @@ let needSetup = false;
removeGroupChildren = true;
}
// Ensure status code ranges are strings
if (!monitor.accepted_statuscodes.every((code) => typeof code === "string")) {
throw new Error("Accepted status codes are not all strings");
}
bean.name = monitor.name;
bean.description = monitor.description;
bean.parent = monitor.parent;
@ -742,6 +754,12 @@ let needSetup = false;
bean.headers = monitor.headers;
bean.basic_auth_user = monitor.basic_auth_user;
bean.basic_auth_pass = monitor.basic_auth_pass;
bean.timeout = monitor.timeout;
bean.oauth_client_id = monitor.oauth_client_id,
bean.oauth_client_secret = monitor.oauth_client_secret,
bean.oauth_auth_method = this.oauth_auth_method,
bean.oauth_token_url = monitor.oauth_token_url,
bean.oauth_scopes = monitor.oauth_scopes,
bean.tlsCa = monitor.tlsCa;
bean.tlsCert = monitor.tlsCert;
bean.tlsKey = monitor.tlsKey;
@ -800,6 +818,7 @@ let needSetup = false;
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
bean.validate();
@ -1401,6 +1420,7 @@ let needSetup = false;
// Define default values
let retryInterval = 0;
let timeout = monitorListData[i].timeout || (monitorListData[i].interval * 0.8); // fallback to old value
/*
Only replace the default value with the backup file data for the specific version, where it appears the first time
@ -1426,6 +1446,7 @@ let needSetup = false;
basic_auth_pass: monitorListData[i].basic_auth_pass,
authWorkstation: monitorListData[i].authWorkstation,
authDomain: monitorListData[i].authDomain,
timeout,
interval: monitorListData[i].interval,
retryInterval: retryInterval,
resendInterval: monitorListData[i].resendInterval || 0,

@ -162,6 +162,7 @@ module.exports.statusPageSocketHandler = (socket) => {
statusPage.footer_text = config.footerText;
statusPage.custom_css = config.customCSS;
statusPage.show_powered_by = config.showPoweredBy;
statusPage.show_certificate_expiry = config.showCertificateExpiry;
statusPage.modified_date = R.isoDateTime();
statusPage.google_analytics_tag_id = config.googleAnalyticsId;

@ -11,6 +11,7 @@ const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
const childProcess = require("child_process");
const path = require("path");
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
/**
@ -214,7 +215,7 @@ class UptimeKumaServer {
* @param {boolean} outputToConsole Should the error also be output to console?
*/
static errorLog(error, outputToConsole = true) {
const errorLogStream = fs.createWriteStream(Database.dataDir + "/error.log", {
const errorLogStream = fs.createWriteStream(path.join(Database.dataDir, "/error.log"), {
flags: "a"
});

@ -21,6 +21,8 @@ const grpc = require("@grpc/grpc-js");
const protojs = require("protobufjs");
const radiusClient = require("node-radius-client");
const redis = require("redis");
const oidc = require("openid-client");
const {
dictionaries: {
rfc2865: { file, attributes },
@ -55,6 +57,43 @@ exports.initJWTSecret = async () => {
return jwtSecretBean;
};
/**
* Decodes a jwt and returns the payload portion without verifying the jqt.
* @param {string} jwt The input jwt as a string
* @returns {Object} Decoded jwt payload object
*/
exports.decodeJwt = (jwt) => {
return JSON.parse(Buffer.from(jwt.split(".")[1], "base64").toString());
};
/**
* Gets a Access Token form a oidc/oauth2 provider
* @param {string} tokenEndpoint The token URI form the auth service provider
* @param {string} clientId The oidc/oauth application client id
* @param {string} clientSecret The oidc/oauth application client secret
* @param {string} scope The scope the for which the token should be issued for
* @param {string} authMethod The method on how to sent the credentials. Default client_secret_basic
* @returns {Promise<oidc.TokenSet>} TokenSet promise if the token request was successful
*/
exports.getOidcTokenClientCredentials = async (tokenEndpoint, clientId, clientSecret, scope, authMethod = "client_secret_basic") => {
const oauthProvider = new oidc.Issuer({ token_endpoint: tokenEndpoint });
let client = new oauthProvider.Client({
client_id: clientId,
client_secret: clientSecret,
token_endpoint_auth_method: authMethod
});
// Increase default timeout and clock tolerance
client[oidc.custom.http_options] = () => ({ timeout: 10000 });
client[oidc.custom.clock_tolerance] = 5;
let grantParams = { grant_type: "client_credentials" };
if (scope) {
grantParams.scope = scope;
}
return await client.grant(grantParams);
};
/**
* Send TCP request to specified hostname and port
* @param {string} hostname Hostname / address of machine
@ -489,6 +528,7 @@ exports.radius = function (
host: hostname,
hostPort: port,
timeout: timeout,
retries: 1,
dictionaries: [ file ],
});
@ -500,6 +540,12 @@ exports.radius = function (
[ attributes.CALLING_STATION_ID, callingStationId ],
[ attributes.CALLED_STATION_ID, calledStationId ],
],
}).catch((error) => {
if (error.response?.code) {
throw Error(error.response.code);
} else {
throw Error(error.message);
}
});
};
@ -677,7 +723,6 @@ exports.checkCertificate = function (res) {
* @param {number} status The status code to check
* @param {string[]} acceptedCodes An array of accepted status codes
* @returns {boolean} True if status code within range, false otherwise
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string
*/
exports.checkStatusCode = function (status, acceptedCodes) {
if (acceptedCodes == null || acceptedCodes.length === 0) {
@ -685,6 +730,11 @@ exports.checkStatusCode = function (status, acceptedCodes) {
}
for (const codeRange of acceptedCodes) {
if (typeof codeRange !== "string") {
log.error("monitor", `Accepted status code not a string. ${codeRange} is of type ${typeof codeRange}`);
continue;
}
const codeRangeSplit = codeRange.split("-").map(string => parseInt(string));
if (codeRangeSplit.length === 1) {
if (status === codeRangeSplit[0]) {
@ -695,7 +745,8 @@ exports.checkStatusCode = function (status, acceptedCodes) {
return true;
}
} else {
throw new Error("Invalid status code range");
log.error("monitor", `${codeRange} is not a valid status code range`);
continue;
}
}
@ -1007,3 +1058,13 @@ module.exports.grpcQuery = async (options) => {
};
module.exports.prompt = (query) => new Promise((resolve) => rl.question(query, resolve));
// For unit test, export functions
if (process.env.TEST_BACKEND) {
module.exports.__test = {
parseCertificateInfo,
};
module.exports.__getPrivateFunction = (functionName) => {
return module.exports.__test[functionName];
};
}

@ -111,6 +111,10 @@ optgroup {
padding-right: 20px;
}
.btn-sm {
border-radius: 25px;
}
.btn-primary {
color: white;
@ -158,6 +162,26 @@ optgroup {
background-color: #161B22;
}
.btn-outline-normal {
padding: 4px 10px;
border: 1px solid #ced4da;
border-radius: 25px;
background-color: transparent;
.dark & {
color: $dark-font-color;
border: 1px solid $dark-font-color2;
}
&.active {
background-color: $highlight-white;
.dark & {
background-color: $dark-font-color2;
}
}
}
@media (max-width: 550px) {
.table-shadow-box {
padding: 10px !important;
@ -436,7 +460,6 @@ optgroup {
.monitor-list {
&.scrollbar {
overflow-y: auto;
height: calc(100% - 107px);
}
@media (max-width: 770px) {

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

@ -0,0 +1,56 @@
<template>
<div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{ $t("New Group") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
<div class="modal-body">
<form @submit.prevent="confirm">
<div>
<label for="draftGroupName" class="form-label">{{ $t("Group Name") }}</label>
<input id="draftGroupName" v-model="groupName" type="text" class="form-control">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{{ $t("Cancel") }}
</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" :disabled="groupName == '' || groupName == null" @click="confirm">
{{ $t("Confirm") }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from "bootstrap";
export default {
props: {},
emits: [ "added" ],
data: () => ({
modal: null,
groupName: null,
}),
mounted() {
this.modal = new Modal(this.$refs.modal);
},
methods: {
/** Show the confirm dialog */
show() {
this.modal.show();
},
confirm() {
this.$emit("added", this.groupName);
this.modal.hide();
},
},
};
</script>

@ -5,15 +5,24 @@
v-for="(beat, index) in shortBeatList"
:key="index"
class="beat"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2), 'maintenance' : (beat.status === 3) }"
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
:style="beatStyle"
:title="getBeatTitle(beat)"
/>
</div>
<div
v-if="size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
>
<div>{{ timeSinceFirstBeat }} ago</div>
<div v-if="$root.styleElapsedTime === 'with-line'" class="connecting-line"></div>
<div>{{ timeSinceLastBeat }}</div>
</div>
</div>
</template>
<script>
import dayjs from "dayjs";
export default {
props: {
@ -56,8 +65,30 @@ export default {
}
},
/**
* Calculates the amount of beats of padding needed to fill the length of shortBeatList.
*
* @return {number} The amount of beats of padding needed to fill the length of shortBeatList.
*/
numPadding() {
if (!this.beatList) {
return 0;
}
let num = this.beatList.length - this.maxBeat;
if (this.move) {
num = num - 1;
}
if (num > 0) {
return 0;
}
return -1 * num;
},
shortBeatList() {
if (! this.beatList) {
if (!this.beatList) {
return [];
}
@ -115,6 +146,53 @@ export default {
};
},
/**
* Returns the style object for positioning the time element.
* @return {Object} The style object containing the CSS properties for positioning the time element.
*/
timeStyle() {
return {
"margin-left": this.numPadding * (this.beatWidth + this.beatMargin * 2) + "px",
};
},
/**
* Calculates the time elapsed since the first valid beat.
*
* @return {string} The time elapsed in minutes or hours.
*/
timeSinceFirstBeat() {
const firstValidBeat = this.shortBeatList.at(this.numPadding);
const minutes = dayjs().diff(dayjs.utc(firstValidBeat?.time), "minutes");
if (minutes > 60) {
return (minutes / 60).toFixed(0) + "h";
} else {
return minutes + "m";
}
},
/**
* Calculates the elapsed time since the last valid beat was registered.
*
* @return {string} The elapsed time in a minutes, hours or "now".
*/
timeSinceLastBeat() {
const lastValidBeat = this.shortBeatList.at(-1);
const seconds = dayjs().diff(dayjs.utc(lastValidBeat?.time), "seconds");
let tolerance = 60 * 2; // default for when monitorList not available
if (this.$root.monitorList[this.monitorId] != null) {
tolerance = this.$root.monitorList[this.monitorId].interval * 2;
}
if (seconds < tolerance) {
return "now";
} else if (seconds < 60 * 60) {
return (seconds / 60).toFixed(0) + "m ago";
} else {
return (seconds / 60 / 60).toFixed(0) + "h ago";
}
}
},
watch: {
beatList: {
@ -133,14 +211,14 @@ export default {
},
beforeMount() {
if (this.heartbeatList === null) {
if (! (this.monitorId in this.$root.heartbeatList)) {
if (!(this.monitorId in this.$root.heartbeatList)) {
this.$root.heartbeatList[this.monitorId] = [];
}
}
},
mounted() {
if (this.size === "small") {
if (this.size !== "big") {
this.beatWidth = 5;
this.beatHeight = 16;
this.beatMargin = 2;
@ -151,11 +229,11 @@ export default {
const actualWidth = this.beatWidth * window.devicePixelRatio;
const actualMargin = this.beatMargin * window.devicePixelRatio;
if (! Number.isInteger(actualWidth)) {
if (!Number.isInteger(actualWidth)) {
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
}
if (! Number.isInteger(actualMargin)) {
if (!Number.isInteger(actualMargin)) {
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio;
}
@ -229,4 +307,21 @@ export default {
}
}
.word {
color: #aaa;
font-size: 12px;
}
.connecting-line {
flex-grow: 1;
height: 1px;
background-color: #ededed;
margin-left: 10px;
margin-right: 10px;
margin-top: 2px;
.dark & {
background-color: #333;
}
}
</style>

@ -2,6 +2,10 @@
<div class="shadow-box mb-3" :style="boxStyle">
<div class="list-header">
<div class="header-top">
<button class="btn btn-outline-normal ms-2" :class="{ 'active': selectMode }" type="button" @click="selectMode = !selectMode">
{{ $t("Select") }}
</button>
<div class="placeholder"></div>
<div class="search-wrapper">
<a v-if="searchText == ''" class="search-icon">
@ -21,27 +25,55 @@
<div class="header-filter">
<MonitorListFilter :filterState="filterState" @update-filter="updateFilter" />
</div>
<!-- Selection Controls -->
<div v-if="selectMode" class="selection-controls px-2 pt-2">
<input
v-model="selectAll"
class="form-check-input select-input"
type="checkbox"
/>
<button class="btn-outline-normal" @click="pauseDialog"><font-awesome-icon icon="pause" size="sm" /> {{ $t("Pause") }}</button>
<button class="btn-outline-normal" @click="resumeSelected"><font-awesome-icon icon="play" size="sm" /> {{ $t("Resume") }}</button>
<span v-if="selectedMonitorCount > 0">
{{ $t("selectedMonitorCount", [ selectedMonitorCount ]) }}
</span>
</div>
</div>
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
<div ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle">
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
</div>
<MonitorListItem
v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item"
:isSearch="searchText !== ''"
v-for="(item, index) in sortedMonitorList"
:key="index"
:monitor="item"
:showPathName="filtersActive"
:isSelectMode="selectMode"
:isSelected="isSelected"
:select="select"
:deselect="deselect"
/>
</div>
</div>
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseSelected">
{{ $t("pauseMonitorMsg") }}
</Confirm>
</template>
<script>
import Confirm from "../components/Confirm.vue";
import MonitorListItem from "../components/MonitorListItem.vue";
import MonitorListFilter from "./MonitorListFilter.vue";
import { getMonitorRelativeURL } from "../util.ts";
export default {
components: {
Confirm,
MonitorListItem,
MonitorListFilter,
},
@ -54,6 +86,10 @@ export default {
data() {
return {
searchText: "",
selectMode: false,
selectAll: false,
disableSelectAllWatcher: false,
selectedMonitors: {},
windowTop: 0,
filterState: {
status: null,
@ -81,31 +117,68 @@ export default {
},
/**
* Returns a sorted list of monitors based on the applied filters and search text.
*
* @return {Array} The sorted list of monitors.
*/
sortedMonitorList() {
let result = Object.values(this.$root.monitorList);
// Simple filter by search text
// finds monitor name, tag name or tag value
if (this.searchText !== "") {
const loweredSearchText = this.searchText.toLowerCase();
result = result.filter(monitor => {
return monitor.name.toLowerCase().includes(loweredSearchText)
result = result.filter(monitor => {
// filter by search text
// finds monitor name, tag name or tag value
let searchTextMatch = true;
if (this.searchText !== "") {
const loweredSearchText = this.searchText.toLowerCase();
searchTextMatch =
monitor.name.toLowerCase().includes(loweredSearchText)
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|| tag.value?.toLowerCase().includes(loweredSearchText));
});
} else {
result = result.filter(monitor => monitor.parent === null);
}
}
// filter by status
let statusMatch = true;
if (this.filterState.status != null && this.filterState.status.length > 0) {
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
}
statusMatch = this.filterState.status.includes(monitor.status);
}
// filter by active
let activeMatch = true;
if (this.filterState.active != null && this.filterState.active.length > 0) {
activeMatch = this.filterState.active.includes(monitor.active);
}
// filter by tags
let tagsMatch = true;
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
tagsMatch = monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs
.filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags
.length > 0;
}
// Hide children if not filtering
let showChild = true;
if (this.filterState.status == null && this.filterState.active == null && this.filterState.tags == null && this.searchText === "") {
if (monitor.parent !== null) {
showChild = false;
}
}
return searchTextMatch && statusMatch && activeMatch && tagsMatch && showChild;
});
// Filter result by active state, weight and alphabetical
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
if (m1.active === false) {
return 1;
}
if (m2.active === 0) {
if (m2.active === false) {
return -1;
}
}
@ -123,28 +196,68 @@ export default {
return m1.name.localeCompare(m2.name);
});
if (this.filterState.status != null && this.filterState.status.length > 0) {
result.map(monitor => {
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
}
});
result = result.filter(monitor => this.filterState.status.includes(monitor.status));
}
return result;
},
if (this.filterState.active != null && this.filterState.active.length > 0) {
result = result.filter(monitor => this.filterState.active.includes(monitor.active));
}
isDarkTheme() {
return document.body.classList.contains("dark");
},
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
result = result.filter(monitor => {
return monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs
.filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags
.length > 0;
});
monitorListStyle() {
let listHeaderHeight = 107;
if (this.selectMode) {
listHeaderHeight += 42;
}
return result;
return {
"height": `calc(100% - ${listHeaderHeight}px)`
};
},
selectedMonitorCount() {
return Object.keys(this.selectedMonitors).length;
},
/**
* Determines if any filters are active.
*
* @return {boolean} True if any filter is active, false otherwise.
*/
filtersActive() {
return this.filterState.status != null || this.filterState.active != null || this.filterState.tags != null || this.searchText !== "";
}
},
watch: {
searchText() {
for (let monitor of this.sortedMonitorList) {
if (!this.selectedMonitors[monitor.id]) {
if (this.selectAll) {
this.disableSelectAllWatcher = true;
this.selectAll = false;
}
break;
}
}
},
selectAll() {
if (!this.disableSelectAllWatcher) {
this.selectedMonitors = {};
if (this.selectAll) {
this.sortedMonitorList.forEach((item) => {
this.selectedMonitors[item.id] = true;
});
}
} else {
this.disableSelectAllWatcher = false;
}
},
selectMode() {
if (!this.selectMode) {
this.selectAll = false;
this.selectedMonitors = {};
}
},
},
mounted() {
@ -181,6 +294,53 @@ export default {
updateFilter(newFilter) {
this.filterState = newFilter;
},
/**
* Deselect a monitor
* @param {number} id ID of monitor
*/
deselect(id) {
delete this.selectedMonitors[id];
},
/**
* Select a monitor
* @param {number} id ID of monitor
*/
select(id) {
this.selectedMonitors[id] = true;
},
/**
* Determine if monitor is selected
* @param {number} id ID of monitor
* @returns {bool}
*/
isSelected(id) {
return id in this.selectedMonitors;
},
/** Disable select mode and reset selection */
cancelSelectMode() {
this.selectMode = false;
this.selectedMonitors = {};
},
/** Show dialog to confirm pause */
pauseDialog() {
this.$refs.confirmPause.show();
},
/** Pause each selected monitor */
pauseSelected() {
Object.keys(this.selectedMonitors)
.filter(id => this.$root.monitorList[id].active)
.forEach(id => this.$root.getSocket().emit("pauseMonitor", id));
this.cancelSelectMode();
},
/** Resume each selected monitor */
resumeSelected() {
Object.keys(this.selectedMonitors)
.filter(id => !this.$root.monitorList[id].active)
.forEach(id => this.$root.getSocket().emit("resumeMonitor", id));
this.cancelSelectMode();
},
},
};
</script>
@ -271,4 +431,12 @@ export default {
padding-left: 67px;
margin-top: 5px;
}
.selection-controls {
margin-top: 5px;
display: flex;
align-items: center;
gap: 10px;
}
</style>

@ -44,6 +44,7 @@ export default {
<style lang="scss">
@import "../assets/vars.scss";
@import "../assets/app.scss";
.filter-dropdown-menu {
z-index: 100;
@ -102,18 +103,10 @@ export default {
}
.filter-dropdown-status {
@extend .btn-outline-normal;
display: flex;
align-items: center;
padding: 4px 10px;
margin-left: 5px;
border: 1px solid #ced4da;
border-radius: 25px;
background-color: transparent;
.dark & {
color: $dark-font-color;
border: 1px solid $dark-font-color2;
}
&.active {
border: 1px solid $highlight;

@ -1,34 +1,56 @@
<template>
<div>
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }">
<div class="row">
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="info" :style="depthMargin">
<Uptime :monitor="monitor" type="24" :pill="true" />
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed">
<font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" />
</span>
{{ monitorName }}
<div :style="depthMargin">
<!-- Checkbox -->
<div v-if="isSelectMode" class="select-input-wrapper">
<input
class="form-check-input select-input"
type="checkbox"
:aria-label="$t('Check/Uncheck')"
:checked="isSelected(monitor.id)"
@click.stop="toggleSelection"
/>
</div>
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }">
<div class="row">
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="info">
<Uptime :monitor="monitor" type="24" :pill="true" />
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed">
<font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" />
</span>
{{ monitorName }}
</div>
<div v-if="monitor.tags.length > 0" class="tags">
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
<div class="tags">
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
</div>
</div>
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="small" :monitor-id="monitor.id" />
</div>
</div>
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12 bottom-style">
<HeartbeatBar size="small" :monitor-id="monitor.id" />
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12 bottom-style">
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
</div>
</div>
</div>
</router-link>
</router-link>
</div>
<transition name="slide-fade-up">
<div v-if="!isCollapsed" class="childs">
<MonitorListItem v-for="(item, index) in sortedChildMonitorList" :key="index" :monitor="item" :isSearch="isSearch" :depth="depth + 1" />
<MonitorListItem
v-for="(item, index) in sortedChildMonitorList"
:key="index" :monitor="item"
:showPathName="showPathName"
:isSelectMode="isSelectMode"
:isSelected="isSelected"
:select="select"
:deselect="deselect"
:depth="depth + 1"
/>
</div>
</transition>
</div>
@ -53,8 +75,13 @@ export default {
type: Object,
default: null,
},
/** If the user is currently searching */
isSearch: {
/** Should the monitor name show it's parent */
showPathName: {
type: Boolean,
default: false,
},
/** If the user is in select mode */
isSelectMode: {
type: Boolean,
default: false,
},
@ -63,6 +90,21 @@ export default {
type: Number,
default: 0,
},
/** Callback to determine if monitor is selected */
isSelected: {
type: Function,
default: () => {}
},
/** Callback fired when monitor is selected */
select: {
type: Function,
default: () => {}
},
/** Callback fired when monitor is deselected */
deselect: {
type: Function,
default: () => {}
},
},
data() {
return {
@ -111,13 +153,19 @@ export default {
};
},
monitorName() {
if (this.isSearch) {
if (this.showPathName) {
return this.monitor.pathName;
} else {
return this.monitor.name;
}
}
},
watch: {
isSelectMode() {
// TODO: Resize the heartbeat bar, but too slow
// this.$refs.heartbeatBar.resize();
}
},
beforeMount() {
// Always unfold if monitor is accessed directly
@ -164,6 +212,16 @@ export default {
monitorURL(id) {
return getMonitorRelativeURL(id);
},
/**
* Toggle selection of monitor
*/
toggleSelection() {
if (this.isSelected(this.monitor.id)) {
this.deselect(this.monitor.id);
} else {
this.select(this.monitor.id);
}
},
},
};
</script>
@ -201,4 +259,14 @@ export default {
transition: all 0.2s $easing-in;
}
.select-input-wrapper {
float: left;
margin-top: 15px;
margin-left: 3px;
margin-right: 10px;
padding-left: 4px;
position: relative;
z-index: 15;
}
</style>

@ -126,6 +126,7 @@ export default {
"lunasea": "LunaSea",
"matrix": "Matrix",
"mattermost": "Mattermost",
"nostr": "Nostr",
"ntfy": "Ntfy",
"octopush": "Octopush",
"OneBot": "OneBot",
@ -157,6 +158,7 @@ export default {
"AliyunSMS": "AliyunSMS (阿里云短信服务)",
"DingDing": "DingDing (钉钉自定义机器人)",
"Feishu": "Feishu (飞书)",
"FlashDuty": "FlashDuty (快猫星云)",
"FreeMobile": "FreeMobile (mobile.free.fr)",
"PushDeer": "PushDeer",
"promosms": "PromoSMS",

@ -61,12 +61,17 @@
/>
</span>
</div>
<div v-if="showTags" class="tags">
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
<div class="extra-info">
<div v-if="showCertificateExpiry && monitor.element.type === 'http'">
<Tag :item="{name: $t('Cert Exp.'), value: formattedCertExpiryMessage(monitor), color: certExpiryColor(monitor)}" :size="'sm'" />
</div>
<div v-if="showTags">
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
</div>
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
</div>
</div>
</div>
@ -103,6 +108,10 @@ export default {
/** Should tags be shown? */
showTags: {
type: Boolean,
},
/** Should expiry be shown? */
showCertificateExpiry: {
type: Boolean,
}
},
data() {
@ -154,6 +163,33 @@ export default {
}
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
},
/**
* Returns formatted certificate expiry or Bad cert message
* @param {Object} monitor Monitor to show expiry for
* @returns {string}
*/
formattedCertExpiryMessage(monitor) {
if (monitor?.element?.validCert && monitor?.element?.certExpiryDaysRemaining) {
return monitor.element.certExpiryDaysRemaining + " " + this.$tc("day", monitor.element.certExpiryDaysRemaining);
} else if (monitor?.element?.validCert === false) {
return this.$t("noOrBadCertificate");
} else {
return this.$t("Unknown") + " " + this.$tc("day", 2);
}
},
/**
* Returns certificate expiry based on days remaining
* @param {Object} monitor Monitor to show expiry for
* @returns {string}
*/
certExpiryColor(monitor) {
if (monitor?.element?.validCert && monitor.element.certExpiryDaysRemaining > 7) {
return "#059669";
}
return "#DC2626";
},
}
};
</script>
@ -161,6 +197,15 @@ export default {
<style lang="scss" scoped>
@import "../assets/vars";
.extra-info {
display: flex;
margin-bottom: 0.5rem;
}
.extra-info > div > div:first-child {
margin-left: 0 !important;
}
.no-monitor-msg {
position: absolute;
width: 100%;

@ -0,0 +1,29 @@
<template>
<div class="mb-3">
<label for="flashduty-integration-url" class="form-label">Integration Key</label>
<HiddenInput id="flashduty-integration-url" v-model="$parent.notification.flashdutyIntegrationKey" autocomplete="false"></HiddenInput>
<i18n-t tag="div" keypath="wayToGetFlashDutyKey" class="form-text">
<a href="https://flashcat.cloud/product/flashduty?from=kuma" target="_blank">{{ $t("here") }}</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="flashduty-severity" class="form-label">{{ $t("FlashDuty Severity") }}</label>
<select id="flashduty-severity" v-model="$parent.notification.flashdutySeverity" class="form-select" :required="true">
<option value="Info" selected>Info</option>
<option value="Warning" selected>Warning</option>
<option value="Critical">Critical</option>
</select>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
mounted() {
}
};
</script>

@ -1,9 +1,7 @@
<template>
<div class="mb-3">
<label for="goalert-base-url" class="form-label">{{ $t("Base URL") }}</label>
<div class="input-group mb-3">
<input id="goalert-base-url" v-model="$parent.notification.goAlertBaseURL" type="text" class="form-control" required>
</div>
<input id="goalert-base-url" v-model="$parent.notification.goAlertBaseURL" type="text" class="form-control" required>
<i18n-t tag="div" keypath="goAlertInfo" class="form-text">
<a href="https://goalert.me" target="_blank">https://goalert.me</a>
</i18n-t>

@ -1,16 +1,12 @@
<template>
<div class="mb-3">
<label for="gorush-device-token" class="form-label">{{ $t("Device Token") }}</label><span style="color: red;"><sup>*</sup></span>
<div class="input-group mb-3">
<input id="gorush-device-token" v-model="$parent.notification.gorushDeviceToken" type="text" class="form-control" required>
</div>
<input id="gorush-device-token" v-model="$parent.notification.gorushDeviceToken" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="gorush-server-url" class="form-label">{{ $t("Server URL") }}</label><span style="color: red;"><sup>*</sup></span>
<div class="input-group mb-3">
<input id="gorush-server-url" v-model="$parent.notification.gorushServerURL" type="text" class="form-control" required>
</div>
<input id="gorush-server-url" v-model="$parent.notification.gorushServerURL" type="text" class="form-control" required>
</div>
<div class="mb-3">

@ -5,9 +5,7 @@
</div>
<div class="mb-3">
<label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>
<div class="input-group mb-3">
<input id="gotify-server-url" v-model="$parent.notification.gotifyserverurl" type="text" class="form-control" required>
</div>
<input id="gotify-server-url" v-model="$parent.notification.gotifyserverurl" type="text" class="form-control" required>
</div>
<div class="mb-3">

@ -9,10 +9,7 @@
<div class="mb-3">
<label for="kook-guild-id" class="form-label">{{ $t("Guild ID") }}</label>
<div class="input-group mb-3">
<input id="kook-guild-id" v-model="$parent.notification.kookGuildID" type="text" class="form-control" required>
</div>
<input id="kook-guild-id" v-model="$parent.notification.kookGuildID" type="text" class="form-control" required>
<div class="form-text">
<p style="margin-top: 8px;">

@ -0,0 +1,26 @@
<template>
<div class="mb-3">
<label for="nostr-relays" class="form-label">{{ $t("nostrRelays") }}<span style="color: red;"><sup>*</sup></span></label>
<textarea id="nostr-relays" v-model="$parent.notification.relays" class="form-control" :required="true" placeholder="wss://127.0.0.1:7777/"></textarea>
<small class="form-text text-muted">{{ $t("nostrRelaysHelp") }}</small>
</div>
<div class="mb-3">
<label for="nostr-sender" class="form-label">{{ $t("nostrSender") }}<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="nostr-sender" v-model="$parent.notification.sender" autocomplete="new-password" :required="true"></HiddenInput>
</div>
<div class="mb-3">
<label for="nostr-recipients" class="form-label">{{ $t("nostrRecipients") }}<span style="color: red;"><sup>*</sup></span></label>
<textarea id="nostr-recipients" v-model="$parent.notification.recipients" class="form-control" :required="true" placeholder="npub123...&#10;npub789..."></textarea>
<small class="form-text text-muted">{{ $t("nostrRecipientsHelp") }}</small>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

@ -1,9 +1,7 @@
<template>
<div class="mb-3">
<label for="ntfy-ntfytopic" class="form-label">{{ $t("ntfy Topic") }}</label>
<div class="input-group mb-3">
<input id="ntfy-ntfytopic" v-model="$parent.notification.ntfytopic" type="text" class="form-control" required>
</div>
<input id="ntfy-ntfytopic" v-model="$parent.notification.ntfytopic" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="ntfy-server-url" class="form-label">{{ $t("Server URL") }}</label>
@ -24,21 +22,15 @@
</div>
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'usernamePassword'" class="mb-3">
<label for="ntfy-username" class="form-label">{{ $t("Username") }}</label>
<div class="input-group mb-3">
<input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control">
</div>
<input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control">
</div>
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'usernamePassword'" class="mb-3">
<label for="ntfy-password" class="form-label">{{ $t("Password") }}</label>
<div class="input-group mb-3">
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
</div>
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
</div>
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'accessToken'" class="mb-3">
<label for="ntfy-access-token" class="form-label">{{ $t("Access Token") }}</label>
<div class="input-group mb-3">
<HiddenInput id="ntfy-access-token" v-model="$parent.notification.ntfyaccesstoken"></HiddenInput>
</div>
<HiddenInput id="ntfy-access-token" v-model="$parent.notification.ntfyaccesstoken"></HiddenInput>
</div>
<div class="mb-3">
<label for="ntfy-icon" class="form-label">{{ $t("IconUrl") }}</label>

@ -1,4 +1,9 @@
<template>
<div class="mb-3">
<label for="pushdeer-server" class="form-label">{{ $t("PushDeer Server URL") }}</label>
<input id="pushdeer-server" v-model="$parent.notification.pushdeerServer" type="text" class="form-control" placeholder="https://api2.pushdeer.com">
<div class="form-text">{{ $t("pushDeerServerDescription") }}</div>
</div>
<div class="mb-3">
<label for="pushdeer-key" class="form-label">{{ $t("PushDeer Key") }}</label>
<HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="new-password" placeholder="PDUxxxx"></HiddenInput>

@ -6,9 +6,7 @@
<div class="mb-3">
<label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
<div class="input-group mb-3">
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>

@ -19,11 +19,13 @@ import LineNotify from "./LineNotify.vue";
import LunaSea from "./LunaSea.vue";
import Matrix from "./Matrix.vue";
import Mattermost from "./Mattermost.vue";
import Nostr from "./Nostr.vue";
import Ntfy from "./Ntfy.vue";
import Octopush from "./Octopush.vue";
import OneBot from "./OneBot.vue";
import Opsgenie from "./Opsgenie.vue";
import PagerDuty from "./PagerDuty.vue";
import FlashDuty from "./FlashDuty.vue";
import PagerTree from "./PagerTree.vue";
import PromoSMS from "./PromoSMS.vue";
import Pushbullet from "./Pushbullet.vue";
@ -77,11 +79,13 @@ const NotificationFormList = {
"lunasea": LunaSea,
"matrix": Matrix,
"mattermost": Mattermost,
"nostr": Nostr,
"ntfy": Ntfy,
"octopush": Octopush,
"OneBot": OneBot,
"Opsgenie": Opsgenie,
"PagerDuty": PagerDuty,
"FlashDuty": FlashDuty,
"PagerTree": PagerTree,
"promosms": PromoSMS,
"pushbullet": Pushbullet,

@ -112,6 +112,53 @@
</div>
</div>
</div>
<!-- Timeline -->
<div class="my-4">
<label class="form-label">{{ $t("styleElapsedTime") }}</label>
<div>
<div class="btn-group" role="group">
<input
id="styleElapsedTimeShowNoLine"
v-model="$root.styleElapsedTime"
type="radio"
class="btn-check"
name="styleElapsedTime"
autocomplete="off"
value="no-line"
/>
<label class="btn btn-outline-primary" for="styleElapsedTimeShowNoLine">
{{ $t("styleElapsedTimeShowNoLine") }}
</label>
<input
id="styleElapsedTimeShowWithLine"
v-model="$root.styleElapsedTime"
type="radio"
class="btn-check"
name="styleElapsedTime"
autocomplete="off"
value="with-line"
/>
<label class="btn btn-outline-primary" for="styleElapsedTimeShowWithLine">
{{ $t("styleElapsedTimeShowWithLine") }}
</label>
<input
id="styleElapsedTimeNone"
v-model="$root.styleElapsedTime"
type="radio"
class="btn-check"
name="styleElapsedTime"
autocomplete="off"
value="none"
/>
<label class="btn btn-outline-primary" for="styleElapsedTimeNone">
{{ $t("None") }}
</label>
</div>
</div>
</div>
</div>
</template>

@ -333,7 +333,7 @@
"Post": "Публикувай",
"Please input title and content": "Моля, въведете заглавие и съдържание",
"Created": "Създаден",
"Last Updated": "Последно обновен",
"Last Updated": "Последно обновена",
"Unpin": "Откачи",
"Switch to Light Theme": "Превключи към светла тема",
"Switch to Dark Theme": "Превключи към тъмна тема",
@ -643,8 +643,8 @@
"smseaglePriority": "Приоритет на съобщението (0-9, по подразбиране = 0)",
"IconUrl": "Икона URL адрес",
"webhookAdditionalHeadersTitle": "Допълнителни хедъри",
"webhookAdditionalHeadersDesc": "Задава допълнителни хедъри, изпратени с уеб куката.",
"Enable DNS Cache": "Активирай DNS кеширане",
"webhookAdditionalHeadersDesc": "Задава допълнителни хедъри, изпратени с уеб куката. Всеки хедър трябва да бъде дефиниран като JSON ключ/стойност.",
"Enable DNS Cache": "Активирай DNS кеширане за HTTP(S) монитори",
"Enable": "Активирай",
"Disable": "Деактивирай",
"dnsCacheDescription": "Възможно е да не работи в IPv6 среда - деактивирайте, ако срещнете проблеми.",
@ -738,7 +738,7 @@
"lunaseaDeviceID": "ID на устройството",
"lunaseaUserID": "ID на потребител",
"twilioAccountSID": "Профил SID",
"twilioAuthToken": "Удостоверяващ токен",
"twilioAuthToken": "Удостоверяващ токен / Тайна на API ключа",
"twilioFromNumber": "От номер",
"twilioToNumber": "Към номер",
"sameAsServerTimezone": "Kато часовата зона на сървъра",
@ -747,7 +747,7 @@
"cronSchedule": "График: ",
"invalidCronExpression": "Невалиден \"Cron\" израз: {0}",
"cronExpression": "Израз тип \"Cron\"",
"statusPageRefreshIn": "Обновяване след: {0}",
"statusPageRefreshIn": "Ще се обнови след: {0}",
"ntfyUsernameAndPassword": "Потребителско име и парола",
"ntfyAuthenticationMethod": "Метод за удостоверяване",
"pushoverMessageTtl": "TTL на съобщението (секунди)",
@ -755,7 +755,7 @@
"Badge Generator": "Генератор на баджове на {0}",
"Badge Type": "Тип бадж",
"Badge Duration": "Продължителност на баджа",
"Badge Prefix": "Префикс на баджа",
"Badge Prefix": "Префикс за стйността на баджа",
"Badge Label Color": "Цвят на етикета на баджа",
"Badge Color": "Цвят на баджа",
"Badge Label Suffix": "Суфикс на етикета на значката",
@ -769,9 +769,9 @@
"Badge URL": "URL адрес на баджа",
"Monitor Setting": "Настройка на монитор {0}",
"Show Clickable Link": "Покажи връзка, която може да се кликне",
"Show Clickable Link Description": "Ако е отбелязано, всеки който има достъп до тази статус страница, ще може да достъпва URL адреса на монитора.",
"Show Clickable Link Description": "Ако е отбелязано, всеки който има достъп до тази статус страница, ще може да достъпва мониторирания URL адрес.",
"Badge Label": "Етикет на баджа",
"Badge Suffix": "Суфикс на баджа",
"Badge Suffix": "Суфикс за стойността на баджа",
"Badge Label Prefix": "Префикс на етикета на значката",
"Badge Pending Color": "Цвят на баджа за изчакващ",
"Badge Down Days": "Колко дни баджът да не се показва",
@ -782,5 +782,61 @@
"Edit Maintenance": "Редактиране на поддръжка",
"Home": "Главна страница",
"noGroupMonitorMsg": "Не е налично. Първо създайте групов монитор.",
"Close": "Затвори"
"Close": "Затвори",
"nostrRelays": "Nostr релета",
"nostrRelaysHelp": "Един URL адрес за реле на ред",
"nostrSender": "Частен ключ на изпращача (nsec)",
"nostrRecipients": "Публични ключове на получатели (npub)",
"nostrRecipientsHelp": "npub формат, по един на ред",
"chromeExecutable": "Chrome/Chromium изпълним файл",
"chromeExecutableAutoDetect": "Автоматично откриване",
"chromeExecutableDescription": "За потребителите на Docker, ако Chromium все още не е инсталиран, инсталирането и показването на резултата от теста може да отнеме няколко минути. Заема 1GB дисково пространство.",
"Invert Keyword": "Обърнат режим за ключова дума",
"invertKeywordDescription": "При търсене ключовата дума трябва да отсъства, а не да присъства.",
"webhookBodyPresetOption": "Предварителна настройка - {0}",
"webhookBodyCustomOption": "Персонализирано тяло",
"webhookCustomBodyDesc": "Дефинирайте персонализирано HTTP тяло за заявката. Приемат се шаблонни променливи {msg}, {heartbeat}, {monitor}.",
"Request Body": "Тяло на заявката",
"twilioApiKey": "API ключ (по избор)",
"Expected Value": "Очаквана стойност",
"Json Query": "Заявка тип JSON",
"jsonQueryDescription": "Прави JSON заявка срещу отговора и проверява за очаквана стойност (Върнатата стойност ще бъде преобразувана в низ за сравнение). Разгледайте <a href='https://jsonata.org/'>jsonata.org</a> за документация относно езика на заявката. Имате възможност да тествате <a href='https://try.jsonata.org/'>тук</a>.",
"Badge Duration (in hours)": "Времетраене на баджа (в часове)",
"Badge Preview": "Преглед на баджа",
"Notify Channel": "Канал за известяване",
"aboutNotifyChannel": "Каналът за известяване ще задейства известие на настолен компютър или мобилно устройство за всички членове на канала, независимо дали тяхната наличност е в състояние активен или отсъстващ.",
"filterActive": "Активен",
"filterActivePaused": "На пауза",
"Kafka Brokers": "Kafka брокери",
"Enter the list of brokers": "Въведете списъка с брокери",
"Press Enter to add broker": "Натиснете Enter, за да добавите брокер",
"Kafka Topic Name": "Име на темата за Kafka",
"Enable Kafka SSL": "Активирай Kafka SSL",
"Enable Kafka Producer Auto Topic Creation": "Активирай автоматично създаване на темa в Kafka Producer",
"Kafka Producer Message": "Съобщение на Kafka Producer",
"Kafka SASL Options": "Опции на Kafka SASL",
"Mechanism": "Механизъм",
"Pick a SASL Mechanism...": "Изберете SASL механизъм...",
"Authorization Identity": "Идентичност за оторизиране",
"AccessKey Id": "AccessKey ID",
"Secret AccessKey": "Таен ключ за достъп",
"Session Token": "Токен за сесия",
"tailscalePingWarning": "За да използвате Tailscale Ping монитор, трябва да инсталирате Uptime Kuma без Docker и също така да инсталирате Tailscale клиент на вашия сървър.",
"Server URL should not contain the nfty topic": "URL адресът на сървъра не трябва да съдържа nfty темата",
"FlashDuty Severity": "Степен на тежест",
"showCertificateExpiry": "Показвай изтичащ сертификат",
"noOrBadCertificate": "Няма/лош сертификат",
"Select": "Избери",
"selectedMonitorCount": "Избрано: {0}",
"wayToGetFlashDutyKey": "Можете да отидете на страница 'Channel -> (Select a Channel) -> Integrations -> Add a new integration' и да добавите 'Custom Event', за да получите 'push' адрес и да копирате ключа за интегриране в адреса. За повече информация, моля посетете",
"PushDeer Server": "PushDeer сървър",
"pushDeerServerDescription": "Оставете празно, за да използвате официалния сървър",
"Check/Uncheck": "Постави/Премахни отметка",
"Request Timeout": "Време за изтичане на заявката",
"timeoutAfter": "Времето изтича след {0} секунди",
"styleElapsedTime": "Изминало време под лентата с проверки",
"styleElapsedTimeShowNoLine": "Покажи (без ред)",
"gamedigGuessPort": "Gamedig: Познай порт",
"gamedigGuessPortDescription": "Портът, използван от Valve Server Query Protocol, може да е различен от клиентския порт. Опитайте това, ако мониторът не може да се свърже с вашия сървър.",
"styleElapsedTimeShowWithLine": "Покажи (с ред)"
}

@ -18,13 +18,13 @@
"Pick Affected Monitors...": "Vyberte dotčené dohledy…",
"Start of maintenance": "Zahájit údržbu",
"All Status Pages": "Všechny stavové stránky",
"Select status pages...": "Vyberte stavovou stránku…",
"Select status pages...": "Vyberte stavové stránky…",
"recurringIntervalMessage": "Spustit jednou každý den | Spustit jednou každých {0} dní",
"affectedMonitorsDescription": "Vyberte dohledy, které budou ovlivněny touto údržbou",
"affectedStatusPages": "Zobrazit tuto zprávu o údržbě na vybraných stavových stránkách",
"atLeastOneMonitor": "Vyberte alespoň jeden dotčený dohled",
"passwordNotMatchMsg": "Hesla se neshodují.",
"notificationDescription": "Aby byla upozornění fungovalo, je nutné ho přiřadit k dohledu.",
"notificationDescription": "Aby oznámení fungovala, je nutné jej přiřadit k dohledu.",
"keywordDescription": "Vyhledat klíčové slovo v prosté odpovědi HTML nebo JSON. Při hledání se rozlišuje velikost písmen.",
"pauseDashboardHome": "Pauza",
"deleteMonitorMsg": "Opravdu chcete odstranit tento dohled?",
@ -208,7 +208,7 @@
"Status Page": "Stavová stránka",
"Status Pages": "Stavová stránka",
"defaultNotificationName": "Moje {notification} upozornění ({číslo})",
"here": "sem",
"here": "klikněte sem",
"Required": "Vyžadováno",
"telegram": "Telegram",
"ZohoCliq": "ZohoCliq",
@ -225,7 +225,7 @@
"webhookJsonDesc": "{0} je vhodný pro všechny moderní servery HTTP, jako je Express.js",
"webhookFormDataDesc": "{multipart} je vhodné pro PHP. JSON bude nutné analyzovat prostřednictvím {decodeFunction}",
"webhookAdditionalHeadersTitle": "Dodatečné hlavičky",
"webhookAdditionalHeadersDesc": "Nastavte dodatečné hlavičky, které se odešlou společně s webhookem.",
"webhookAdditionalHeadersDesc": "Nastavte dodatečné hlavičky, které se odešlou společně s webhookem. Každá hlavička by měla být definována jako klíč/hodnota v JSON.",
"smtp": "E-mail (SMTP)",
"secureOptionNone": "Žádné / STARTTLS (25, 587)",
"secureOptionTLS": "TLS (465)",
@ -243,8 +243,8 @@
"Hello @everyone is...": "Dobrý den, {'@'}všichni jsou…",
"teams": "Microsoft Teams",
"Webhook URL": "URL adresa webhooku",
"wayToGetTeamsURL": "Informace o tom, jak vytvořit URL adresu webhooku naleznete na {0}.",
"wayToGetZohoCliqURL": "Informace o tom, jak vytvořit URL adresu webhooku naleznete na {0}.",
"wayToGetTeamsURL": "Pro informace o tom, jak vytvořit URL adresu webhooku {0}.",
"wayToGetZohoCliqURL": "Pro informace o tom, jak vytvořit URL adresu webhooku {0}.",
"signal": "Signal",
"Number": "Číslo",
"Recipients": "Příjemci",
@ -339,7 +339,7 @@
"PasswordsDoNotMatch": "Hesla se neshodují.",
"records": "záznamů",
"One record": "Jeden záznam",
"steamApiKeyDescription": "Pro monitorování Steam Game Serveru je nutné zadat Steam Web-API klíč. Svůj API klíč získáte na následující stránce: ",
"steamApiKeyDescription": "Pro monitorování herního serveru ve službě Steam je nutné zadat Steam Web-API klíč. Svůj API klíč získáte na následující stránce: ",
"Current User": "Aktuálně přihlášený uživatel",
"topic": "Téma",
"topicExplanation": "MQTT téma, které chcete sledovat",
@ -525,8 +525,8 @@
"RadiusCalledStationIdDescription": "Identifikátor volaného zařízení",
"RadiusCallingStationId": "ID volajícího zařízení",
"RadiusCallingStationIdDescription": "Identifikátor volajícího zařízení",
"Certificate Expiry Notification": "Oznámení na blížící se konec platnosti certifikátu",
"API Username": "Uživatelské jména API",
"Certificate Expiry Notification": "Upozornění na blížící se konec platnosti certifikátu",
"API Username": "Uživatelské jméno API",
"API Key": "API klíč",
"Recipient Number": "Číslo příjemce",
"From Name/Number": "Jméno/číslo odesílatele",
@ -567,7 +567,7 @@
"Also check beta release": "Kontrolovat také dostupnost beta verzí",
"Using a Reverse Proxy?": "Používáte reverzní proxy?",
"Check how to config it for WebSocket": "Zjistěte, jak ji nakonfigurovat pro WebSockety",
"Steam Game Server": "Steam Game Server",
"Steam Game Server": "Herní server ve službě Steam",
"Most likely causes:": "Nejčastější důvody:",
"The resource is no longer available.": "Zdroj již není k dispozici.",
"There might be a typing error in the address.": "Při zadávání adresy jste udělali chybu.",
@ -638,7 +638,7 @@
"dayOfWeek": "Den v týdnu",
"dayOfMonth": "Den v měsíci",
"lastDay": "Poslední den",
"lastDay1": "1. poslední den v měsíci",
"lastDay1": "Poslední den v měsíci",
"lastDay2": "2. poslední den v měsíci",
"lastDay3": "3. poslední den v měsíci",
"lastDay4": "4. poslední den v měsíci",
@ -653,10 +653,10 @@
"Server Timezone": "Časové pásmo serveru",
"statusPageMaintenanceEndDate": "Konec",
"IconUrl": "Adresa URL ikony",
"Enable DNS Cache": "Povolit DNS Cache",
"Enable DNS Cache": "Povolit DNS Cache pro HTTP(s) dohledy",
"Enable": "Povolit",
"Disable": "Zakázat",
"dnsCacheDescription": "V některých IPv6 prostředích nemusí fungovat. Pokud narazíte na nějaké problémy, vypněte jej.",
"dnsCacheDescription": "V některých IPv6 prostředích nemusí fungovat. Pokud narazíte na nějaké problémy, tuto možnost vypněte.",
"Single Maintenance Window": "Konkrétní časové okno pro údržbu",
"Maintenance Time Window of a Day": "Časové okno pro údržbu v daný den",
"Effective Date Range": "Časové období (volitelné)",
@ -690,7 +690,7 @@
"uninstall": "Odinstalace",
"uninstalling": "Odinstalování",
"Packet Size": "Velikost paketu",
"markdownSupported": "Markdown syntaxe podporována",
"markdownSupported": "Markdown syntaxe je podporována",
"Google Analytics ID": "ID Google Analytics",
"Edit Tag": "Upravit štítek",
"Server Address": "Adresa serveru",
@ -741,46 +741,101 @@
"twilioAccountSID": "SID účtu",
"twilioFromNumber": "Číslo odesílatele",
"twilioToNumber": "Číslo příjemce",
"twilioAuthToken": "Autorizační token",
"twilioAuthToken": "Autorizační token / Tajemství API klíče",
"sameAsServerTimezone": "Stejné jako časové pásmo serveru",
"cronExpression": "Cron výraz",
"cronSchedule": "Plán: ",
"invalidCronExpression": "Neplatný cron výraz: {0}",
"startDateTime": "Počáteční datum/čas",
"startDateTime": "Datum/čas začátku",
"endDateTime": "Datum/čas konce",
"ntfyAuthenticationMethod": "Způsob ověření",
"ntfyUsernameAndPassword": "Uživatelské jméno a heslo",
"pushoverMessageTtl": "Zpráva TTL (Sekund)",
"Show Clickable Link": "Zobrazit klikatelný odkaz",
"Show Clickable Link Description": "Pokud je zaškrtnuto, všichni, kdo mají přístup k této stavové stránce, mají přístup k adrese URL monitoru.",
"Show Clickable Link Description": "Pokud je zaškrtnuto, všichni, kdo mají přístup k této stavové stránce, mají přístup k adrese URL dohledu.",
"Open Badge Generator": "Otevřít generátor odznaků",
"Badge Type": "Typ odznaku",
"Badge Duration": "Platnost odznaku",
"Badge Label": "Štítek odznaku",
"Badge Prefix": "Prefix odznaku",
"Monitor Setting": "{0}'s Nastavení dohledu",
"Badge Prefix": "Prefix hodnoty odznaku",
"Monitor Setting": "Nastavení dohledu pro {0}",
"Badge Generator": "Generátor odznaků pro {0}",
"Badge Label Color": "Barva štítku odznaku",
"Badge Color": "Barva odznaku",
"Badge Style": "Styl odznaku",
"Badge Label Suffix": "Přípona štítku odznaku",
"Badge URL": "URL odznaku",
"Badge Suffix": "Přípona odznaku",
"Badge Suffix": "Přípona hodnoty odznaku",
"Badge Label Prefix": "Prefix štítku odznaku",
"Badge Up Color": "Barva odznaku při Běží",
"Badge Down Color": "Barva odznaku při Nedostupné",
"Badge Pending Color": "Barva odznaku při Pauze",
"Badge Maintenance Color": "Barva odznaku při Údržbě",
"Badge Warn Color": "Barva odznaku při Upozornění",
"Reconnecting...": "Obnovení spojení...",
"Cannot connect to the socket server": "Nelze se připojit k soketovému serveru",
"Reconnecting...": "Obnovování spojení…",
"Cannot connect to the socket server": "Nelze se připojit k socketu serveru",
"Edit Maintenance": "Upravit Údržbu",
"Home": "Hlavní stránka",
"Badge Down Days": "Odznak nedostupných dní",
"Group": "Skupina",
"Monitor Group": "Sledovaná skupina",
"noGroupMonitorMsg": "Není k dispozici. Nejprve vytvořte skupin dohledů.",
"noGroupMonitorMsg": "Není k dispozici. Nejprve vytvořte skupinu dohledů.",
"Close": "Zavřít",
"Badge value (For Testing only.)": "Hodnota odznaku (pouze pro testování)",
"Badge Warn Days": "Odznak dní s upozorněním"
"Badge Warn Days": "Odznak dní s upozorněním",
"nostrSender": "Privátní klíč odesílatele (nsec)",
"nostrRelaysHelp": "Jedno relay URL na řádku",
"nostrRecipients": "Privátní klíče příjemců (npub)",
"nostrRecipientsHelp": "formát npub, jeden na řádku",
"chromeExecutable": "Spustitelný soubor Chrome/Chromium",
"chromeExecutableAutoDetect": "Automatická detekce",
"chromeExecutableDescription": "Pokud uživatelé nástroje Docker ještě nemají nainstalovanou aplikaci Chromium, může instalace a zobrazení výsledku testu trvat několik minut. Zabere 1 GB místa na disku.",
"Invert Keyword": "Inverzní klíčové slovo",
"webhookBodyPresetOption": "Uložená hodnota - {0}",
"webhookBodyCustomOption": "Vlastní tělo",
"invertKeywordDescription": "Hledá se klíčové slovo, které je spíše nepřítomné než přítomné.",
"webhookCustomBodyDesc": "Nastaví vlastní tělo HTTP pro požadavek. Akceptovány jsou proměnné {msg}, {heartbeat}, {monitor}.",
"Request Body": "Tělo požadavku",
"twilioApiKey": "Klíč k API (volitelný)",
"Expected Value": "Očekávaná hodnota",
"Json Query": "Json dotaz",
"Badge Duration (in hours)": "Zobrazení odznaku (v hodinách)",
"Badge Preview": "Náhled odznaku",
"Notify Channel": "Kanál nofitikací",
"aboutNotifyChannel": "Upozornění kanálu spustí upozornění na počítači nebo v mobilu pro všechny členy kanálu, ať už jsou dostupní nebo ne.",
"filterActive": "Aktivní",
"filterActivePaused": "Pozastaveno",
"Enter the list of brokers": "Vytvořte seznam zprostředkovatelů",
"Press Enter to add broker": "Stiskem klávesy Enter přidáte zprostředkovatele",
"Kafka Topic Name": "Název Kafka vlákna",
"Enable Kafka SSL": "Zapnout Kafka SSL",
"Mechanism": "Mechanismus",
"Kafka Brokers": "Kafka zprostředkovatelé",
"Authorization Identity": "Autorizační identita",
"AccessKey Id": "AccessKey Id",
"Session Token": "Token relace",
"Pick a SASL Mechanism...": "Vyberte SASL mechanismus…",
"Secret AccessKey": "Secret AccessKey",
"Server URL should not contain the nfty topic": "URL serveru by neměla obsahovat nfty vlákno",
"Kafka SASL Options": "Možnosti Kafka SASL",
"Enable Kafka Producer Auto Topic Creation": "Povolit Kafka zprostředkovateli automatické vytváření vláken",
"Kafka Producer Message": "Zpráva Kafka zprostředkovatele",
"tailscalePingWarning": "Abyste mohli používat Tailscale Ping monitor, je nutné Uptime Kuma nainstalovat mimo Docker, a dále na váš server nainstalovat Tailscale klienta.",
"jsonQueryDescription": "Proveďte JSON dotaz vůči odpovědi a zkontrolujte očekávaný výstup (za účelem porovnání bude návratová hodnota převedena na řetězec). Dokumentaci k dotazovacímu jazyku naleznete na <a href='https://jsonata.org/'>jsonata.org</a>, a využít můžete též <a href='https://try.jsonata.org/'>playground</a>.",
"Select": "Vybrat",
"selectedMonitorCount": "Vybráno: {0}",
"Check/Uncheck": "Vybrat/Zrušit výběr",
"showCertificateExpiry": "Zobrazit vypršení platnosti certifikátu",
"pushDeerServerDescription": "Chcete-li používat oficiální server, ponechte prázdné",
"noOrBadCertificate": "Žádný/Vadný certifikát",
"nostrRelays": "Relé Nostr",
"FlashDuty Severity": "Závažnost",
"PushDeer Server": "Server PushDeer",
"wayToGetFlashDutyKey": "Můžete přejít na stránku Kanál -> (Vyberte kanál) -> Integrace -> Přidat novou integraci, přidat \"Vlastní událost\" a získat adresu pro odeslání, zkopírovat klíč integrace do adresy. Další informace naleznete na adrese",
"Request Timeout": "Časový limit požadavku",
"timeoutAfter": "Vypršení časového limitu po {0} sekundách",
"styleElapsedTime": "Čas uplynulý pod pruhem poslední odpovědi",
"styleElapsedTimeShowWithLine": "Zobrazit (s linkou)",
"gamedigGuessPortDescription": "Port používaný protokolem Valve Server Query Protocol se může lišit od portu klienta. Pokud se monitor nemůže připojit k serveru, zkuste to.",
"styleElapsedTimeShowNoLine": "Zobrazit (bez linky)"
}

@ -211,7 +211,7 @@
"supportTelegramChatID": "Support Direct Chat / Group / Channel's Chat ID",
"wayToGetTelegramChatID": "Du kan få dit chat-ID ved at sende en besked til bot'en og gå til denne URL for at se chat_id'et:",
"YOUR BOT TOKEN HERE": "DIT BOT TOKEN HER",
"chatIDNotFound": "Chat-ID blev ikke fundet; send venligst en besked til denne bot først ",
"chatIDNotFound": "Chat-ID blev ikke fundet; send venligst en besked til denne bot først",
"Post URL": "Post URL",
"Content Type": "Indholdstype",
"webhookJsonDesc": "{0} er god til alle moderne HTTP-servere som f.eks Express.js",

@ -48,7 +48,7 @@
"Port": "Port",
"Heartbeat Interval": "Prüfintervall",
"Retries": "Wiederholungen",
"retriesDescription": "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
"retriesDescription": "Maximale Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird",
"Advanced": "Erweitert",
"ignoreTLSError": "Ignoriere TLS-/SSL-Fehler von Webseiten",
"Upside Down Mode": "Umgekehrter Modus",
@ -218,7 +218,7 @@
"wayToGetTelegramToken": "Hier kannst du einen Token erhalten {0}.",
"Chat ID": "Chat ID",
"supportTelegramChatID": "Unterstützt Direkt Chat / Gruppe / Kanal Chat-ID's",
"wayToGetTelegramChatID": "Du kannst die Chat-ID erhalten, indem du eine Nachricht an den Bot sendest und zu dieser URL gehst, um die chat_id: zu sehen.",
"wayToGetTelegramChatID": "Du kannst die Chat-ID erhalten, indem du eine Nachricht an den Bot sendest und zu dieser URL gehst, um die chat_id zu sehen",
"YOUR BOT TOKEN HERE": "HIER DEIN BOT TOKEN",
"chatIDNotFound": "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
"Post URL": "Post URL",
@ -655,10 +655,10 @@
"telegramSendSilentlyDescription": "Sende die Nachricht stumm. Nutzer bekommen eine Benachrichtigung ohne Ton.",
"markdownSupported": "Markdown-Syntax unterstützt",
"webhookAdditionalHeadersTitle": "Zusätzliche Header",
"webhookAdditionalHeadersDesc": "Legt zusätzliche Kopfzeilen fest, die mit dem Webhook gesendet werden.",
"webhookAdditionalHeadersDesc": "Legt zusätzliche Kopfzeilen fest, die mit dem Webhook gesendet werden. Jede Kopfzeile sollte als JSON Schlüssel/Wert definiert werden.",
"Packet Size": "Paketgrösse",
"IconUrl": "Symbol URL",
"Enable DNS Cache": "DNS Cache aktivieren",
"Enable DNS Cache": "DNS-Cache für HTTP(s)-Monitore aktivieren",
"Help": "Hilfe",
"Game": "Spiel",
"General Monitor Type": "Allgemeiner Monitortyp",
@ -743,7 +743,7 @@
"twilioAccountSID": "Account SID",
"twilioFromNumber": "Absender",
"twilioToNumber": "Empfänger",
"twilioAuthToken": "Auth Token",
"twilioAuthToken": "Auth Token / Api Key Secret",
"statusPageRefreshIn": "Aktualisierung in: {0}",
"sameAsServerTimezone": "Gleiche Zeitzone wie Server",
"startDateTime": "Start Datum/Uhrzeit",
@ -756,8 +756,8 @@
"Badge Type": "Badge Typ",
"Badge Duration": "Badge Dauer",
"Badge Label": "Badge Label",
"Badge Prefix": "Badge Präfix",
"Badge Suffix": "Badge Suffix",
"Badge Prefix": "Badge Wert Präfix",
"Badge Suffix": "Badge Wert Suffix",
"Badge Label Color": "Badge Label Farbe",
"Badge Color": "Badge Farbe",
"Badge Label Prefix": "Badge Label Präfix",
@ -779,5 +779,41 @@
"Group": "Gruppe",
"Monitor Group": "Monitor Gruppe",
"noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
"Close": "Schliessen"
"Close": "Schliessen",
"chromeExecutableAutoDetect": "Automatische Erkennung",
"chromeExecutableDescription": "Für Docker-Benutzer, die Chromium noch nicht installiert haben, kann es ein paar Minuten dauern, bis es installiert ist und das Testergebnis angezeigt wird. Es benötigt 1 GB Speicherplatz.",
"chromeExecutable": "Chrome/Chromium Ausführbare Datei",
"Invert Keyword": "Schlüsselwort invertieren",
"webhookCustomBodyDesc": "Definiere einen benutzerdefinierten HTTP-Body für die Anfrage. Die Template-Variablen {msg}, {heartbeat} und {monitor} werden akzeptiert.",
"webhookBodyPresetOption": "Voreinstellung - {0}",
"webhookBodyCustomOption": "Benutzerdefinierter Body",
"invertKeywordDescription": "Achte darauf, dass das Schlüsselwort eher fehlt als vorhanden ist.",
"Request Body": "Anforderungstext",
"twilioApiKey": "API-Schlüssel (optional)",
"aboutNotifyChannel": "Notify Kanal löst eine Desktop- oder Mobilbenachrichtigung für alle Mitglieder des Kanals aus, unabhängig davon, ob deine Verfügbarkeit auf aktiv oder abwesend eingestellt ist.",
"Notify Channel": "Notify Kanal",
"Enter the list of brokers": "Gib die Liste der Broker ein",
"Kafka Topic Name": "Kafka Topic Name",
"Kafka Producer Message": "Kafka Producer Nachricht",
"Enable Kafka SSL": "Kafka SSL aktivieren",
"Enable Kafka Producer Auto Topic Creation": "Kafka Producer Auto Topic Creation aktivieren",
"Kafka SASL Options": "Kafka SASL Optionen",
"Mechanism": "Mechanismus",
"Pick a SASL Mechanism...": "Wähle ein SASL Mechanismus...",
"AccessKey Id": "AccessKey Id",
"Secret AccessKey": "Secret AccessKey",
"Session Token": "Sitzungs-Token",
"Kafka Brokers": "Kafka Brokers",
"Press Enter to add broker": "Drücke Enter um den Broker hinzuzufügen",
"Authorization Identity": "Authorization Identity",
"Expected Value": "Erwarteter Wert",
"Json Query": "Json-Abfrage",
"filterActive": "Aktiv",
"filterActivePaused": "Pausiert",
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf <a href='https://jsonata.org/'>jsonata.org</a> findest du die Dokumentation zur Abfragesprache. <a href='https://try.jsonata.org/'>Hier</a> kannst du Abfragen üben.",
"Badge Duration (in hours)": "Badge Dauer (in Stunden)",
"Badge Preview": "Badge Vorschau",
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
"Server URL should not contain the nfty topic": "Die Server-URL sollte das nfty-Thema nicht enthalten",
"pushDeerServerDescription": "Leer lassen, um den offiziellen Server zu verwenden"
}

@ -48,7 +48,7 @@
"Port": "Port",
"Heartbeat Interval": "Prüfintervall",
"Retries": "Wiederholungen",
"retriesDescription": "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
"retriesDescription": "Maximale Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird",
"Advanced": "Erweitert",
"ignoreTLSError": "Ignoriere TLS-/SSL-Fehler von Webseiten",
"Upside Down Mode": "Umgekehrter Modus",
@ -218,7 +218,7 @@
"wayToGetTelegramToken": "Hier kannst du einen Token erhalten {0}.",
"Chat ID": "Chat ID",
"supportTelegramChatID": "Unterstützt Direkt Chat / Gruppe / Kanal Chat-ID's",
"wayToGetTelegramChatID": "Du kannst deine Chat-ID erhalten, indem du eine Nachricht an den Bot sendest und zu dieser URL gehst, um die chat_id: zu sehen.",
"wayToGetTelegramChatID": "Du kannst deine Chat-ID erhalten, indem du eine Nachricht an den Bot sendest und zu dieser URL gehst, um die chat_id zu sehen:",
"YOUR BOT TOKEN HERE": "HIER DEIN BOT TOKEN",
"chatIDNotFound": "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
"Post URL": "Post URL",
@ -380,7 +380,7 @@
"alertaAlertState": "Alarmstatus",
"alertaRecoverState": "Wiederherstellungsstatus",
"deleteStatusPageMsg": "Bist du sicher, dass du diese Status-Seite löschen willst?",
"Proxies": "Proxies",
"Proxies": "Proxys",
"default": "Standard",
"enabled": "Aktiviert",
"setAsDefault": "Als Standard setzen",
@ -644,11 +644,11 @@
"Help": "Hilfe",
"Game": "Spiel",
"Custom": "Benutzerdefiniert",
"Enable DNS Cache": "DNS-Cache aktivieren",
"Enable DNS Cache": "DNS-Cache für HTTP(s)-Monitore aktivieren",
"Enable": "Aktivieren",
"Disable": "Deaktivieren",
"Custom Monitor Type": "Benutzerdefinierter Monitortyp",
"webhookAdditionalHeadersDesc": "Legt zusätzliche Header fest, die mit der Webhook gesendet wurden.",
"webhookAdditionalHeadersDesc": "Legt zusätzliche Kopfzeilen fest, die mit dem Webhook gesendet werden. Jede Kopfzeile sollte als JSON Schlüssel/Wert definiert werden.",
"dnsCacheDescription": "In einigen IPv6-Umgebungen funktioniert es möglicherweise nicht. Deaktiviere es, wenn Probleme auftreten.",
"loadingError": "Die Daten konnten nicht abgerufen werden, bitte später noch einmal versuchen.",
"confirmUninstallPlugin": "Möchtest du dieses Plugin wirklich deinstallieren?",
@ -700,7 +700,7 @@
"Edit Tag": "bearbeite Tag",
"Server Address": "Server Adresse",
"Learn More": "Erfahre mehr",
"Body Encoding": "Körperkodierung",
"Body Encoding": "Inhaltskodierung",
"Add API Key": "API Schlüssel hinzufügen",
"apiKey-active": "Aktiv",
"apiKey-expired": "Abgelaufen",
@ -747,7 +747,7 @@
"twilioAccountSID": "Account SID",
"twilioFromNumber": "Absender",
"twilioToNumber": "Empfänger",
"twilioAuthToken": "Auth Token",
"twilioAuthToken": "Auth Token / Api Key Secret",
"statusPageRefreshIn": "Aktualisierung in: {0}",
"sameAsServerTimezone": "Gleiche Zeitzone wie Server",
"startDateTime": "Start Datum/Uhrzeit",
@ -776,11 +776,47 @@
"Badge Pending Color": "Badge Pending Farbe",
"Badge Down Days": "Badge Down Tage",
"Monitor Setting": "{0}'s Monitor Einstellung",
"Badge Prefix": "Badge Präfix",
"Badge Suffix": "Badge Suffix",
"Badge Prefix": "Badge Wert Präfix",
"Badge Suffix": "Badge Wert Suffix",
"Badge Warn Days": "Badge Warnung Tage",
"Group": "Gruppe",
"Monitor Group": "Monitor Gruppe",
"noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
"Close": "Schließen"
"Close": "Schließen",
"chromeExecutableAutoDetect": "Automatische Erkennung",
"chromeExecutableDescription": "Für Docker-Benutzer, die Chromium noch nicht installiert haben, kann es ein paar Minuten dauern, bis es installiert ist und das Testergebnis angezeigt wird. Es benötigt 1 GB Speicherplatz.",
"chromeExecutable": "Chrome/Chromium Ausführbare Datei",
"Invert Keyword": "Schlüsselwort invertieren",
"invertKeywordDescription": "Achte darauf, dass das Schlüsselwort eher fehlt als vorhanden ist.",
"webhookCustomBodyDesc": "Definiere einen benutzerdefinierten HTTP-Body für die Anfrage. Die Template-Variablen {msg}, {heartbeat} und {monitor} werden akzeptiert.",
"webhookBodyPresetOption": "Voreinstellung - {0}",
"webhookBodyCustomOption": "Benutzerdefinierter Body",
"Request Body": "Anforderungstext",
"Badge Duration (in hours)": "Badge Dauer (in Stunden)",
"Badge Preview": "Badge Vorschau",
"twilioApiKey": "API-Schlüssel (optional)",
"Notify Channel": "Notify Kanal",
"Enter the list of brokers": "Gib die Liste der Broker ein",
"Kafka Topic Name": "Kafka Topic Name",
"Kafka Producer Message": "Kafka Producer Nachricht",
"Enable Kafka SSL": "Kafka SSL aktivieren",
"Enable Kafka Producer Auto Topic Creation": "Kafka Producer Auto Topic Creation aktivieren",
"Kafka SASL Options": "Kafka SASL Optionen",
"Mechanism": "Mechanismus",
"Pick a SASL Mechanism...": "Wähle ein SASL Mechanismus...",
"Authorization Identity": "Authorization Identity",
"AccessKey Id": "AccessKey Id",
"Secret AccessKey": "Secret AccessKey",
"Session Token": "Sitzungs-Token",
"aboutNotifyChannel": "Notify Kanal löst eine Desktop- oder Mobilbenachrichtigung für alle Mitglieder des Kanals aus, unabhängig davon, ob deine Verfügbarkeit auf aktiv oder abwesend eingestellt ist.",
"Kafka Brokers": "Kafka Brokers",
"Press Enter to add broker": "Drücke Enter um den Broker hinzuzufügen",
"filterActive": "Aktiv",
"filterActivePaused": "Pausiert",
"Expected Value": "Erwarteter Wert",
"Json Query": "Json-Abfrage",
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf <a href='https://jsonata.org/'>jsonata.org</a> findest du die Dokumentation zur Abfragesprache. <a href='https://try.jsonata.org/'>Hier</a> kannst du Abfragen üben.",
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
"Server URL should not contain the nfty topic": "Die Server-URL sollte das nfty-Thema nicht enthalten",
"pushDeerServerDescription": "Leer lassen, um den offiziellen Server zu verwenden"
}

@ -64,6 +64,8 @@
"Hostname": "Hostname",
"Port": "Port",
"Heartbeat Interval": "Heartbeat Interval",
"Request Timeout": "Request Timeout",
"timeoutAfter": "Timeout after {0} seconds",
"Retries": "Retries",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Resend Notification if Down X times consecutively": "Resend Notification if Down X times consecutively",
@ -90,6 +92,9 @@
"Dark": "Dark",
"Auto": "Auto",
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar",
"styleElapsedTime": "Elapsed time under the heartbeat bar",
"styleElapsedTimeShowNoLine": "Show (No Line)",
"styleElapsedTimeShowWithLine": "Show (With Line)",
"Normal": "Normal",
"Bottom": "Bottom",
"None": "None",
@ -274,6 +279,9 @@
"Services": "Services",
"Discard": "Discard",
"Cancel": "Cancel",
"Select": "Select",
"selectedMonitorCount": "Selected: {0}",
"Check/Uncheck": "Check/Uncheck",
"Powered by": "Powered by",
"shrinkDatabaseDescription": "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
"Customize": "Customize",
@ -446,7 +454,7 @@
"Server Timezone": "Server Timezone",
"statusPageMaintenanceEndDate": "End",
"IconUrl": "Icon URL",
"Enable DNS Cache": "Enable DNS Cache",
"Enable DNS Cache": "Enable DNS Cache for HTTP(s) monitors",
"Enable": "Enable",
"Disable": "Disable",
"chromeExecutable": "Chrome/Chromium Executable",
@ -702,6 +710,8 @@
"onebotPrivateMessage": "Private",
"onebotUserOrGroupId": "Group/User ID",
"onebotSafetyTips": "For safety, must set access token",
"PushDeer Server": "PushDeer Server",
"pushDeerServerDescription": "Leave blank to use the official server",
"PushDeer Key": "PushDeer Key",
"wayToGetClickSendSMSToken": "You can get API Username and API Key from {0} .",
"Custom Monitor Type": "Custom Monitor Type",
@ -790,5 +800,16 @@
"Session Token": "Session Token",
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
"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 'Custom Event' to get a push address, copy the Integration Key in the address. For more information, please visit",
"FlashDuty Severity":"Severity",
"nostrRelays": "Nostr relays",
"nostrRelaysHelp": "One relay URL per line",
"nostrSender": "Sender Private Key (nsec)",
"nostrRecipients": "Recipients Public Keys (npub)",
"nostrRecipientsHelp": "npub format, one per line",
"showCertificateExpiry": "Show Certificate Expiry",
"noOrBadCertificate": "No/Bad Certificate",
"gamedigGuessPort": "Gamedig: Guess Port",
"gamedigGuessPortDescription": "The port used by Valve Server Query Protocol may be different from the client port. Try this if the monitor cannot connect to your server."
}

@ -270,7 +270,7 @@
"Display Timezone": "Mostrar Zona Horaria",
"Server Timezone": "Servidor de Zona Horaria",
"statusPageMaintenanceEndDate": "Finaliza",
"Enable DNS Cache": "Habilitar Cache DNS",
"Enable DNS Cache": "Habilitar Cache DNS de monitores HTTP(s)",
"No Maintenance": "Sin Mantenimiento",
"weekdayShortSun": "Dom",
"dayOfWeek": "Día de la Semana",
@ -389,7 +389,7 @@
"emojiCheatSheet": "Hoja de trucos Emoji: {0}",
"webhookJsonDesc": "{0} es bueno para cualquier servidor HTTP moderno como Express.js",
"webhookFormDataDesc": "{multipart} es bueno para PHP. El JSON deberá analizarse con {decodeFunction}",
"webhookAdditionalHeadersDesc": "Establece encabezados adicionales enviados con el webhook.",
"webhookAdditionalHeadersDesc": "Establece encabezados adicionales enviados con el webhook. Cada cabecera debe definirse como una clave/valor JSON.",
"appriseInstalled": "Apprise está instalado.",
"successMessage": "Mensaje de éxito",
"Pick Accepted Status Codes...": "Seleccione Códigos de Estado Aceptados…",
@ -588,8 +588,8 @@
"GoogleChat": "Chat de Google (sólo Google Workspace)",
"Kook": "Kook",
"wayToGetKookBotToken": "Crea aplicación y obtén tu token de bot en {0}",
"wayToGetKookGuildID": "Activa 'Modo Desarrollador' en los ajustes de Kook, y haz click derecho en la unión para obtener su ID",
"Guild ID": "ID de Gremio",
"wayToGetKookGuildID": "Activa 'Modo Desarrollador' en los ajustes de Kook, y haz click derecho en el grupo para obtener su ID",
"Guild ID": "ID de grupo",
"User Key": "Key de Usuario",
"octopushTypePremium": "Premium (Rápido - recomendado para alertas)",
"octopushTypeLowCost": "Bajo Coste (Lento - algunas veces bloqueado por operador)",
@ -653,7 +653,7 @@
"gorush": "Gorush",
"squadcast": "Squadcast",
"Maintenance Time Window of a Day": "Ventana de tiempo de mantenimiento de un día",
"Effective Date Range": "Rango de Fecha Efectivo",
"Effective Date Range": "Rango de Fecha Efectivo (Opcional)",
"Free Mobile User Identifier": "Identificador de Usuario de Free Mobile",
"Gateway Type": "Tipo de Puerta de Enlace",
"SMSManager": "SMSManager",
@ -696,7 +696,7 @@
"High": "Alto",
"alertaApiEndpoint": "Endpoint API",
"Body Encoding": "Codificación del cuerpo",
"Expiry date": "Fecha de expiración",
"Expiry date": "Fecha de vencimiento",
"Expiry": "Expiración",
"API Keys": "Claves API",
"Key Added": "Clave añadida",
@ -749,7 +749,22 @@
"statusPageRefreshIn": "Reinicio en: {0}",
"twilioAuthToken": "Token de Autentificación",
"ntfyUsernameAndPassword": "Nombre de Usuario y Contraseña",
"ntfyAuthenticationMethod": "Método de Autentificación",
"ntfyAuthenticationMethod": "Método de Autenticación",
"Cannot connect to the socket server": "No se puede conectar al servidor socket",
"Reconnecting...": "Reconectando..."
"Reconnecting...": "Reconectando...",
"Select": "Seleccionar",
"chromeExecutableAutoDetect": "Auto Detectar",
"Edit Maintenance": "Editar mantenimiento",
"pushoverMessageTtl": "Mensaje TTL (segundos)",
"Notify Channel": "Canal de notificación",
"Show Clickable Link Description": "Si está marcado, todos los que tienen acceso a esta página de estado pueden tener acceso a la URL del monitor.",
"webhookBodyCustomOption": "Cuerpo Personalizado",
"selectedMonitorCount": "Seleccionado: {0}",
"Check/Uncheck": "Marcar/Desmarcar",
"Invert Keyword": "Invertir palabra clave",
"filterActive": "Activo",
"filterActivePaused": "Pausado",
"Home": "Inicio",
"Expected Value": "Valor esperado",
"Json Query": "Consulta Json"
}

@ -223,7 +223,7 @@
"webhookJsonDesc": "{0} est bien pour tous les serveurs HTTP modernes comme Express.js",
"webhookFormDataDesc": "{multipart} est bien pour du PHP. Le JSON aura besoin d'être parsé avec {decodeFunction}",
"webhookAdditionalHeadersTitle": "En-têtes supplémentaires",
"webhookAdditionalHeadersDesc": "Définit des en-têtes supplémentaires envoyés avec le webhook.",
"webhookAdditionalHeadersDesc": "Définit des en-têtes supplémentaires envoyés avec le webhook. Chaque en-tête doit être défini comme une clé/valeur JSON.",
"smtp": "Courriel (SMTP)",
"secureOptionNone": "Aucun / STARTTLS (25, 587)",
"secureOptionTLS": "TLS (465)",
@ -463,7 +463,7 @@
"User": "Utilisateur",
"Installed": "Installé",
"Not installed": "Non installé",
"Running": "Fonctionne",
"Running": "En cours",
"Not running": "Ne fonctionne pas",
"Remove Token": "Supprimer le jeton",
"Start": "Démarrer",
@ -650,7 +650,7 @@
"Server Timezone": "Fuseau horaire du serveur",
"statusPageMaintenanceEndDate": "Fin",
"IconUrl": "URL vers l'icône",
"Enable DNS Cache": "Activer le cache DNS",
"Enable DNS Cache": "Activer le cache DNS pour les sondes HTTP(s)",
"Enable": "Activer",
"Disable": "Désactiver",
"dnsCacheDescription": "Il peut ne pas fonctionner dans certains environnements IPv6, désactivez-le si vous rencontrez des problèmes.",
@ -741,7 +741,7 @@
"twilioFromNumber": "Du Nombre",
"twilioToNumber": "Au Nombre",
"twilioAccountSID": "ID du compte",
"twilioAuthToken": "Jeton d'authentification",
"twilioAuthToken": "Jeton d'authentification / Clé secrète de l'API",
"sameAsServerTimezone": "Identique au fuseau horaire du serveur",
"startDateTime": "Date/heure de début",
"endDateTime": "Date/heure de fin",
@ -756,8 +756,8 @@
"Open Badge Generator": "Ouvrir le générateur de badges",
"Badge Type": "Type de badge",
"Badge Duration": "Durée du badge",
"Badge Prefix": "Préfixe de badge",
"Badge Suffix": "Suffixe de badge",
"Badge Prefix": "Préfixe de la valeur du badge",
"Badge Suffix": "Suffixe de la valeur du badge",
"Badge Label Color": "Couleur de l'étiquette du badge",
"Badge Color": "Couleur du badge",
"Badge Label Prefix": "Préfixe d'étiquette de badge",
@ -782,5 +782,61 @@
"Group": "Groupe",
"Home": "Accueil",
"noGroupMonitorMsg": "Pas disponible. Créez d'abord une sonde de groupe.",
"Close": "Fermer"
"Close": "Fermer",
"chromeExecutableDescription": "Pour les utilisateurs sous Docker, si Chromium n'est pas encore installé, quelques minutes seront nécessaires pour installer et afficher le résultat du test. Cela peut prendre 1 Go d'espace disque.",
"chromeExecutableAutoDetect": "Auto-détecter",
"chromeExecutable": "Exécutable Chrome/Chromium",
"Invert Keyword": "Inverser le mot-clé",
"invertKeywordDescription": "Recherchez le mot-clé absent plutôt que présent.",
"webhookCustomBodyDesc": "Définissez un corps HTTP personnalisé pour la requête. Les variables de modèle {msg}, {heartbeat}, {monitor} sont acceptées.",
"webhookBodyCustomOption": "Corps personnalisé",
"webhookBodyPresetOption": "Préréglages - {0}",
"Request Body": "Corps de la requête",
"twilioApiKey": "Clé API (facultatif)",
"Expected Value": "Valeur attendue",
"Json Query": "Requête Json",
"jsonQueryDescription": "Faites une requête json contre la réponse et vérifiez la valeur attendue (la valeur de retour sera convertie en chaîne pour comparaison). Consultez <a href='https://jsonata.org/'>jsonata.org</a> pour la documentation sur le langage de requête. Une aire de jeux peut être trouvée <a href='https://try.jsonata.org/'>ici</a>.",
"Badge Duration (in hours)": "Durée du badge (en heures)",
"Badge Preview": "Aperçu du badge",
"aboutNotifyChannel": "Notifier le canal déclenchera une notification de bureau ou mobile pour tous les membres du canal, que leur disponibilité soit active ou absente.",
"Notify Channel": "Notifier le canal",
"filterActive": "Actif",
"filterActivePaused": "En pause",
"Enter the list of brokers": "Entrez la liste des courtiers",
"Press Enter to add broker": "Appuyez sur Entrée pour ajouter un courtier",
"Kafka Topic Name": "Nom du sujet Kafka",
"Enable Kafka SSL": "Activer Kafka SSL",
"Kafka SASL Options": "Options de Kafka SAS",
"Mechanism": "Mécanisme",
"Pick a SASL Mechanism...": "Choisissez un mécanisme SASL...",
"Authorization Identity": "Identité d'autorisation",
"AccessKey Id": "ID de la clé d'accès",
"Secret AccessKey": "Clé d'accès secrète",
"Session Token": "Jeton de session",
"Kafka Brokers": "Courtiers Kafka",
"Kafka Producer Message": "Message du producteur Kafka",
"Enable Kafka Producer Auto Topic Creation": "Activer la création automatique de rubrique Kafka",
"tailscalePingWarning": "Afin d'utiliser la sonde Tailscale Ping, vous devez installer Uptime Kuma sans Docker et également installer le client Tailscale sur votre serveur.",
"Server URL should not contain the nfty topic": "L'URL du serveur ne doit pas contenir le sujet nfty",
"Select": "Sélectionner",
"selectedMonitorCount": "Sélectionné : {0}",
"Check/Uncheck": "Cocher/décocher",
"nostrRelaysHelp": "Une URL relais par ligne",
"nostrRecipients": "Clés publiques des bénéficiaires (npub)",
"nostrSender": "Émetteur clé privée (nsec)",
"nostrRecipientsHelp": "Format npub, un par ligne",
"nostrRelays": "Relais Nostr",
"PushDeer Server": "PushDeer Server",
"showCertificateExpiry": "Afficher l'expiration du certificat",
"noOrBadCertificate": "Pas/Mauvais certificat",
"pushDeerServerDescription": "Laissez le champ vide pour utiliser le serveur officiel",
"FlashDuty Severity": "Gravité",
"wayToGetFlashDutyKey": "Vous pouvez aller dans Canal -> (Sélectionner un canal) -> Intégrations -> Ajouter une nouvelle page d'intégration, ajouter un \"événement personnalisé\" pour obtenir une adresse push, copier la clé d'intégration dans l'adresse. Pour plus d'informations, veuillez visiter",
"Request Timeout": "Délai d'expiration de la demande",
"timeoutAfter": "Délai dépassé après {0} secondes",
"gamedigGuessPort": "Gamedig: Devinez le port",
"gamedigGuessPortDescription": "Le port utilisé par Valve Server Query Protocol peut être différent du port client. Essayez ceci si la sonde ne peut pas se connecter à votre serveur.",
"styleElapsedTimeShowNoLine": "Afficher (pas de ligne)",
"styleElapsedTimeShowWithLine": "Afficher (avec ligne)",
"styleElapsedTime": "Temps écoulé sous la barre d'état"
}

@ -39,5 +39,6 @@
"Reconnecting...": "पुनः कनेक्ट किया जा रहा है...",
"Down": "बंद",
"Passive Monitor Type": "निष्क्रिय मॉनिटर प्रकार",
"Status": "स्थिति"
"Status": "स्थिति",
"showCertificateExpiry": "प्रमाणपत्र समाप्ति दिखाएँ"
}

@ -113,7 +113,7 @@
"Password": "Lozinka",
"Remember me": "Zapamti me",
"Login": "Prijava",
"No Monitors, please": "Nema monitora, ",
"No Monitors, please": "Nema monitora,",
"add one": "dodaj jedan",
"Notification Type": "Tip obavijesti",
"Email": "E-pošta",

@ -11,13 +11,13 @@
"acceptedStatusCodesDescription": "Pilih kode status yang dianggap sebagai tanggapan yang berhasil.",
"passwordNotMatchMsg": "Kata sandi kedua tidak cocok.",
"notificationDescription": "Harap atur notifikasi ke monitor agar berfungsi.",
"keywordDescription": "Cari kata kunci dalam code html atau JSON huruf besar-kecil berpengaruh",
"keywordDescription": "Kata kunci pencarian dalam HTML biasa atau respons JSON. Pencarian bersifat peka terhadap huruf besar/kecil.",
"pauseDashboardHome": "Jeda",
"deleteMonitorMsg": "Apakah Anda mau menghapus monitor ini?",
"deleteNotificationMsg": "Apakah Anda mau menghapus notifikasi untuk semua monitor?",
"dnsPortDescription": "Port server DNS. Bawaan menggunakan 53. Anda dapat mengubah port kapan saja.",
"resolverserverDescription": "Cloudflare adalah server bawaan, Anda dapat mengubah server resolver kapan saja.",
"rrtypeDescription": "Pilih RR-Type yang mau Anda monitor",
"rrtypeDescription": "Pilih RR Type yang mau Anda monitor",
"pauseMonitorMsg": "Apakah Anda yakin mau menjeda?",
"enableDefaultNotificationDescription": "Untuk setiap monitor baru, notifikasi ini akan diaktifkan secara bawaan. Anda masih dapat menonaktifkan notifikasi secara terpisah untuk setiap monitor.",
"clearEventsMsg": "Apakah Anda yakin mau menghapus semua event di monitor ini?",
@ -25,13 +25,13 @@
"confirmClearStatisticsMsg": "Apakah Anda yakin mau menghapus semua statistik?",
"importHandleDescription": "Pilih 'Lewati yang ada' jika Anda ingin melewati setiap monitor atau notifikasi dengan nama yang sama. 'Timpa' akan menghapus setiap monitor dan notifikasi yang ada.",
"confirmImportMsg": "Apakah Anda yakin untuk mengimpor cadangan? Pastikan Anda telah memilih opsi impor yang tepat.",
"twoFAVerifyLabel": "Silakan ketik token Anda untuk memverifikasi bahwa 2FA berfungsi",
"twoFAVerifyLabel": "Masukkan token Anda untuk memverifikasi 2FA:",
"tokenValidSettingsMsg": "Token benar! Anda sekarang dapat menyimpan pengaturan 2FA.",
"confirmEnableTwoFAMsg": "Apakah Anda yakin ingin mengaktifkan 2FA?",
"confirmDisableTwoFAMsg": "Apakah Anda yakin ingin menonaktifkan 2FA?",
"Settings": "Pengaturan",
"Dashboard": "Dasbor",
"New Update": "Pembaruan Baru",
"New Update": "Update terbaru",
"Language": "Bahasa",
"Appearance": "Tampilan",
"Theme": "Tema",
@ -58,7 +58,7 @@
"Delete": "Hapus",
"Current": "Saat ini",
"Uptime": "Waktu aktif",
"Cert Exp.": "Batas kedaluwarsa SSL",
"Cert Exp.": "Sertifikat Kedaluwarsa.",
"day": "hari | hari-hari",
"-day": "-hari",
"hour": "Jam",
@ -69,9 +69,9 @@
"Keyword": "Kata Kunci",
"Friendly Name": "Nama yang Ramah",
"URL": "URL",
"Hostname": "Hostname",
"Hostname": "Nama host",
"Port": "Port",
"Heartbeat Interval": "Jarak Waktu Heartbeat",
"Heartbeat Interval": "Rentang Waktu Heartbeat",
"Retries": "Coba lagi",
"Heartbeat Retry Interval": "Jeda Pengulangan Heartbeat",
"Resend Notification if Down X times consecutively": "Kirim Ulang Notifikasi jika Tidak Aktif X kali",
@ -106,7 +106,7 @@
"Enable Auth": "Aktifkan Autentikasi",
"disableauth.message1": "Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>?",
"disableauth.message2": "Ini untuk <strong>mereka yang memiliki autentikasi pihak ketiga</strong> diletakkan di depan Uptime Kuma, misalnya akses Cloudflare.",
"Please use this option carefully!": "Gunakan dengan hati-hati.",
"Please use this option carefully!": "Silahkan gunakan opsi ini dengan hati-hati!",
"Logout": "Keluar",
"Leave": "Pergi",
"I understand, please disable": "Saya mengerti, silakan dinonaktifkan",
@ -117,14 +117,14 @@
"Password": "Sandi",
"Remember me": "Ingat saya",
"Login": "Masuk",
"No Monitors, please": "Tidak ada monitor, silakan",
"No Monitors, please": "Tolong, jangan ada Monitor",
"add one": "tambahkan satu",
"Notification Type": "Tipe Notifikasi",
"Email": "Surel",
"Test": "Tes",
"Certificate Info": "Info Sertifikasi",
"Resolver Server": "Resolver Server",
"Resource Record Type": "Resource Record Type",
"Resource Record Type": "Jenis Rekam Sumber Daya",
"Last Result": "Hasil Terakhir",
"Create your admin account": "Buat akun admin Anda",
"Repeat Password": "Ulangi Sandi",
@ -162,7 +162,7 @@
"Token": "Token",
"Show URI": "Lihat URI",
"Tags": "Tanda",
"Add New below or Select...": "Tambahkan Baru di bawah atau Pilih...",
"Add New below or Select...": "Tambahkan Baru di bawah atau Pilih",
"Tag with this name already exist.": "Tanda dengan nama ini sudah ada.",
"Tag with this value already exist.": "Tanda dengan nilai ini sudah ada.",
"color": "warna",
@ -175,7 +175,7 @@
"Indigo": "Biru Tua",
"Purple": "Ungu",
"Pink": "Merah Muda",
"Search...": "Cari...",
"Search...": "Cari",
"Avg. Ping": "Rata-rata Ping",
"Avg. Response": "Rata-rata Tanggapan",
"Entry Page": "Halaman Masuk",
@ -194,7 +194,7 @@
"here": "di sini",
"Required": "Wajib",
"telegram": "Telegram",
"Bot Token": "Bot Token",
"Bot Token": "Token Bot",
"wayToGetTelegramToken": "Anda dapat mendapatkan token dari {0}.",
"Chat ID": "Chat ID",
"supportTelegramChatID": "Mendukung Obrolan Langsung / Grup / Channel Chat ID",
@ -216,13 +216,13 @@
"smtpCC": "CC",
"smtpBCC": "BCC",
"discord": "Discord",
"Discord Webhook URL": "Discord Webhook URL",
"wayToGetDiscordURL": "Anda bisa mendapatkan ini dengan pergi ke Server Pengaturan -> Integrasi -> Buat Webhook",
"Discord Webhook URL": "URL Webhook Discord",
"wayToGetDiscordURL": "Anda bisa mendapatkan ini dengan pergi ke Server Pengaturan -> Integrasi -> Lihat Webhooks -> Buat Webhook",
"Bot Display Name": "Nama Bot",
"Prefix Custom Message": "Awalan Pesan",
"Hello @everyone is...": "Halo {'@'}everyone is...",
"Hello @everyone is...": "Halo {'@'}everyone is",
"teams": "Microsoft Teams",
"Webhook URL": "Webhook URL",
"Webhook URL": "URL Webhook",
"wayToGetTeamsURL": "Anda dapat mempelajari cara membuat url webhook {0}.",
"signal": "Sinyal",
"Number": "Nomer",
@ -285,7 +285,7 @@
"lineDevConsoleTo": "Konsol Pengembang Line - {0}",
"Basic Settings": "Pengaturan Dasar",
"User ID": "ID User",
"Messaging API": "Messaging API",
"Messaging API": "API Messaging",
"wayToGetLineChannelToken": "Pertama akses {0}, buat penyedia dan saluran (Messaging API), lalu Anda bisa mendapatkan token akses saluran dan id pengguna dari item menu yang disebutkan di atas.",
"Icon URL": "Icon URL",
"aboutIconURL": "Anda dapat memberikan tautan ke gambar di \"Icon URL\" untuk mengganti gambar profil bawaan. Tidak akan digunakan jika Ikon Emoji diset.",
@ -293,7 +293,7 @@
"matrix": "Matrix",
"promosmsTypeEco": "SMS ECO - murah tapi lambat dan sering kelebihan beban. Terbatas hanya untuk penerima Polandia.",
"promosmsTypeFlash": "SMS FLASH - Pesan akan otomatis muncul di perangkat penerima. Terbatas hanya untuk penerima Polandia.",
"promosmsTypeFull": "SMS FULL - SMS tingkat premium, Anda dapat menggunakan Nama Pengirim Anda (Anda harus mendaftarkan nama terlebih dahulu). Dapat diandalkan untuk peringatan.",
"promosmsTypeFull": "SMS FULL - Tingkat Premium SMS, Anda dapat menggunakan Nama Pengirim Anda (Nama Anda harus didaftarkan terlebih dahulu). Dapat diandalkan untuk peringatan.",
"promosmsTypeSpeed": "SMS SPEED - Prioritas tertinggi dalam sistem. Sangat cepat dan dapat diandalkan tetapi mahal (sekitar dua kali lipat dari harga SMS FULL).",
"promosmsPhoneNumber": "Nomor telepon (untuk penerima Polandia Anda dapat melewati kode area)",
"promosmsSMSSender": "Nama Pengirim SMS : Nama pra-registrasi atau salah satu bawaan: InfoSMS, Info SMS, MaxSMS, INFO, SMS",
@ -302,7 +302,7 @@
"Internal Room Id": "Internal Room ID",
"matrixDesc1": "Kamu dapat menemukan Internal Room ID dengan melihat di bagian konfigurasi ruang di Matrix. Seharusnya berbentuk seperti !QMdRCpUIfLwsfjxye6:home.server.",
"matrixDesc2": "Sangat direkomendasikan kepada Anda untuk membuat akun baru dan jangan menggunakan token atas akun terkini yang memiliki token akses secara penuh terhadap akun dan seluruh ruang yang terdaftar. Alih - alih, buat akun baru dan undang akun tsb ke ruang tempat anda ingin menerima notifikasi. Untuk mendapatkan token akses anda dapat menjalankan {0}",
"Method": "Method",
"Method": "Metode",
"Body": "Body",
"Headers": "Headers",
"PushUrl": "Push URL",
@ -315,18 +315,18 @@
"One record": "Satu catatan",
"steamApiKeyDescription": "Untuk monitoring Steam Game Server Anda membutuhkan kunci Steam Web-API. Anda dapat mendaftarkan Kunci API Anda melalui: ",
"Current User": "Pengguna Saat Ini",
"topic": "Topic",
"topicExplanation": "MQTT topic untuk dimonitor",
"topic": "topik",
"topicExplanation": "MQTT topik untuk dimonitor",
"successMessage": "Pesan Berhasil",
"successMessageExplanation": "Pesan MQTT yang akan dianggap berhasil",
"recent": "Baru saja",
"Done": "Selesai",
"Info": "Info",
"Security": "Keamanan",
"Steam API Key": "Steam API Key",
"Shrink Database": "Shrink Database",
"Pick a RR-Type...": "Pilih RR-Type...",
"Pick Accepted Status Codes...": "Pilih Kode Status yang Diterima...",
"Steam API Key": "Kunci API Steam",
"Shrink Database": "Kecilkan Database",
"Pick a RR-Type...": "Pilih RR-Type",
"Pick Accepted Status Codes...": "Pilih Kode Status yang Diterima",
"Default": "Default",
"HTTP Options": "Opsi HTTP",
"Create Incident": "Buat Incident",
@ -373,8 +373,8 @@
"smtpDkimDesc": "Silakan merujuk ke Nodemailer DKIM {0} untuk penggunaan.",
"documentation": "dokumentasi",
"smtpDkimDomain": "Nama Domain",
"smtpDkimKeySelector": "Key Selector",
"smtpDkimPrivateKey": "Private Key",
"smtpDkimKeySelector": "Selektor Kunci",
"smtpDkimPrivateKey": "Kunci Pribadi",
"smtpDkimHashAlgo": "Algoritma Hash (Opsional)",
"smtpDkimheaderFieldNames": "Header Keys untuk ditambahkan (Optional)",
"smtpDkimskipFields": "Header Keys not untuk ditambahkan (Optional)",
@ -401,8 +401,8 @@
"proxyDescription": "Proxy harus ditambahkan ke monitor agar berfungsi.",
"enableProxyDescription": "Proxy berikut tidak akan berdampak ke monitor hingga diaktifkan. Anda dapat mengontrol menonaktifkan sementara proxy dari semua monitor dengan status aktivasi.",
"setAsDefaultProxyDescription": "Proxy berikut akan diaktifkan sebagai bawaan untuk monitor baru. Anda masih dapat menonaktifkan proxy secara terpisah untuk setiap monitor.",
"Certificate Chain": "Certificate Chain",
"Valid": "Valid",
"Certificate Chain": "Rangkaian Sertifikat",
"Valid": "Berlaku",
"Invalid": "Tidak Valid",
"AccessKeyId": "AccessKey ID",
"SecretAccessKey": "AccessKey Secret",
@ -445,7 +445,7 @@
"The slug is already taken. Please choose another slug.": "Slug telah digunakan. Silakan pilih slug lain.",
"No Proxy": "Tidak ada Proxy",
"Authentication": "Autentikasi",
"HTTP Basic Auth": "HTTP Basic Auth",
"HTTP Basic Auth": "Autentikasi Dasar HTTP",
"New Status Page": "Halaman Status Baru",
"Page Not Found": "Halaman Tidak Ditemukan",
"Reverse Proxy": "Proxy Terbalik",
@ -456,7 +456,7 @@
"Message:": "Pesan:",
"Don't know how to get the token? Please read the guide:": "Tidak tahu cara mendapatkan token? Silakan baca panduannya:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Koneksi saat ini mungkin hilang jika Anda saat ini terhubung melalui Cloudflare Tunel. Apakah Anda yakin ingin menghentikannya? Ketik kata sandi Anda saat ini untuk mengonfirmasinya.",
"HTTP Headers": "HTTP Headers",
"HTTP Headers": "Header HTTP",
"Trust Proxy": "Proxy Terpercaya",
"Other Software": "Perangkat Lunak lainnya",
"For example: nginx, Apache and Traefik.": "Sebagai contoh: nginx, Apache and Traefik.",
@ -510,12 +510,12 @@
"pushoversounds cosmic": "Cosmic",
"pushoversounds falling": "Falling",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Incoming",
"pushoversounds intermission": "Intermission",
"pushoversounds incoming": "Masuk",
"pushoversounds intermission": "Jeda",
"pushoversounds magic": "Magic",
"pushoversounds mechanical": "Mechanical",
"pushoversounds mechanical": "Mekanik",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Siren",
"pushoversounds siren": "Sirene",
"pushoversounds spacealarm": "Space Alarm",
"pushoversounds tugboat": "Tug Boat",
"pushoversounds alien": "Alien Alarm (long)",
@ -551,17 +551,17 @@
"socket": "Socket",
"tcp": "TCP / HTTP",
"Docker Container": "Docker Container",
"Container Name / ID": "Container Name / ID",
"Docker Host": "Docker Host",
"Docker Hosts": "Docker Hosts",
"Container Name / ID": "Nama / ID Container",
"Docker Host": "Host Docker",
"Docker Hosts": "Hosts Docker",
"ntfy Topic": "ntfy Topic",
"Domain": "Domain",
"Workstation": "Workstation",
"disableCloudflaredNoAuthMsg": "Anda berada dalam mode Tanpa Otentikasi, kata sandi tidak diperlukan.",
"trustProxyDescription": "Trust 'X-Forwarded-*' headers. Jika Anda ingin mendapatkan IP klien yang benar dan Uptime Kuma Anda dibalik layanan seperti Nginxor Apache, Anda harus mengaktifkan ini.",
"trustProxyDescription": "Trust 'X-Forwarded-*' headers. Jika Anda ingin mendapatkan IP klien yang benar dan Uptime Kuma Anda dibalik proxy seperti Nginx or Apache, Anda harus mengaktifkan ini.",
"wayToGetLineNotifyToken": "Anda bisa mendapatkan token akses dari {0}",
"Examples": "Contoh",
"Home Assistant URL": "Home Assistant URL",
"Home Assistant URL": "URL Home Asisten",
"Long-Lived Access Token": "Token Akses Berumur Panjang",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Token Akses Berumur Panjang dapat dibuat dengan mengklik nama profil Anda (kiri bawah) dan menggulir ke bawah lalu klik Buat Token. ",
"Notification Service": "Layanan Pemberitahuan",
@ -578,15 +578,213 @@
"goAlertInfo": "GoAlert adalah aplikasi open source untuk penjadwalan panggilan, eskalasi otomatis dan pemberitahuan (seperti SMS atau panggilan suara). Secara otomatis melibatkan orang yang tepat, dengan cara yang benar, dan pada waktu yang tepat! {0}",
"goAlertIntegrationKeyInfo": "Dapatkan kunci integrasi API generik untuk layanan dalam format ini \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" biasanya nilai parameter token dari URL yang disalin.",
"goAlert": "GoAlert",
"backupOutdatedWarning": "Tidak digunakan lagi: Karena banyak fitur ditambahkan dan fitur cadangan ini agak tidak terawat, itu tidak dapat menghasilkan atau memulihkan cadangan lengkap.",
"backupRecommend": "Harap cadangkan volume atau folder data (./data/) secara langsung.",
"backupOutdatedWarning": "Tidak digunakan lagi: Karena banyak fitur ditambahkan dan fitur pencadangan ini agak tidak terpelihara, fitur ini tidak dapat menghasilkan atau memulihkan cadangan lengkap.",
"backupRecommend": "Silahkan backup volume atau folder (./data/) secara langsung.",
"Help": "Bantuan",
"Game": "Gim/Permainan",
"Game": "Permainan",
"markdownSupported": "Dukungan sintaks markdown",
"statusMaintenance": "Pemeliharaan",
"Maintenance": "Pemeliharaan",
"General Monitor Type": "Tipe Monitor Umum",
"Passive Monitor Type": "Tipe Monitor Pasif",
"Specific Monitor Type": "Tipe Monitor Spesifik",
"Monitor": "Monitor"
"Monitor": "Monitor",
"Guild ID": "ID Guild",
"twilioAccountSID": "SID akun",
"twilioAuthToken": "Token Autentikasi",
"ntfyAuthenticationMethod": "Metode Autentikasi",
"ntfyUsernameAndPassword": "Nama pengguna dan kata sandi",
"Add Another": "Tambah Lainnya",
"Key Added": "Kunci Ditambahkan",
"Google Analytics ID": "ID Google Analytics",
"pagertreeIntegrationUrl": "URL integrasi",
"pagertreeUrgency": "Darurat",
"Home": "Beranda",
"startDateTime": "Tanggal/Waktu Mulai",
"Recurring": "Berulang",
"strategyManual": "Aktif/TidakAktif Secara Manual",
"infiniteRetention": "Setel ke 0 untuk retensi tak terbatas.",
"enableGRPCTls": "Izinkan untuk mengirim permintaan gRPC dengan koneksi TLS",
"grpcMethodDescription": "Nama metode dikonversi ke format cammelCase seperti sayHello, check, dll.",
"deleteMaintenanceMsg": "Apakah Anda yakin ingin menghapus pemeliharaan ini?",
"Free Mobile API Key": "Kunci API Seluler Gratis",
"Enable TLS": "Aktifkan TLS",
"Proto Method": "Metode Proto",
"Proto Content": "Konten Proto",
"Economy": "Ekonomi",
"Free Mobile User Identifier": "Pengidentifikasi Pengguna Seluler",
"Proto Service Name": "Nama Layanan Proto",
"SMSManager API Docs": "Dokumen API SMSManager ",
"Expiry date": "Tanggal kadaluarsa",
"No API Keys": "Tidak ada Kunci API",
"Expires": "Berakhir",
"pagertreeCritical": "Penting",
"pagertreeResolve": "Penyelesaian Otomatis",
"lunaseaDeviceID": "ID perangkat",
"lunaseaUserID": "ID Pengguna",
"twilioFromNumber": "Dari Nomor",
"twilioToNumber": "Ke Nomor",
"Badge Generator": "Pembuat Lencana {0}",
"Badge Duration": "Durasi Lencana",
"Badge Label": "Label Lencana",
"Badge Prefix": "Prefiks Lencana",
"Badge Suffix": "Suffix Lencana",
"Badge Label Color": "Warna Label Lencana",
"Badge Color": "Warna Lencana",
"Badge Label Prefix": "Prefiks Label Lencana",
"telegramSendSilently": "Kirim Secara Senyap",
"Invert Keyword": "Balikkan Kata Kunci",
"Pick Affected Monitors...": "Pilih Monitor yang Terkena Dampak…",
"Badge Label Suffix": "Suffix Label Lencana",
"statusPageMaintenanceEndDate": "berakhir",
"Add API Key": "Tambahkan Kunci API",
"apiKey-expired": "Kedaluwarsa",
"apiKey-active": "Aktif",
"apiKey-inactive": "Tidak aktif",
"Monitor Setting": "Pengaturan Pemantauan {0}",
"Show Clickable Link": "Tampilkan Tautan yang Dapat Diklik",
"Badge Type": "Tipe lencana",
"confirmDeleteTagMsg": "Yakin ingin menghapus tag ini? Monitor yang terkait dengan tag ini tidak akan dihapus.",
"Gateway Type": "tipe Gateway",
"Don't expire": "Jangan sampai kadaluarsa",
"apiKeyAddedMsg": "Kunci API Anda telah ditambahkan. Mohon dicatat karena tidak akan ditampilkan lagi.",
"disableAPIKeyMsg": "Yakin ingin menonaktifkan kunci API ini?",
"pagertreeSilent": "Bisu",
"pagertreeLow": "Rendah",
"pagertreeDoNothing": "Jangan Lakukan Apa-apa",
"wayToGetPagerTreeIntegrationURL": "Setelah membuat integrasi Uptime Kuma di PagerTree, salin Endpoint. Lihat detail lengkap {0}",
"lunaseaTarget": "Sasaran",
"Show Clickable Link Description": "Jika dicentang, setiap orang yang memiliki akses ke halaman status ini dapat memiliki akses ke URL monitor.",
"Open Badge Generator": "Buka Pembuat Lencana",
"Cannot connect to the socket server": "Tidak dapat terhubung ke server soket",
"Reconnecting...": "Menghubungkan ulang...",
"deleteAPIKeyMsg": "Apakah Anda yakin ingin menghapus kunci API ini?",
"Generate": "Hasilkan",
"pagertreeMedium": "Sedang",
"pagertreeHigh": "Tinggi",
"Group": "Grup",
"Body Encoding": "Body Encoding",
"Add New Tag": "Tambahkan Tag Baru",
"chromeExecutableDescription": "Untuk pengguna Docker, jika Chromium belum diinstal, mungkin perlu waktu beberapa menit untuk menginstal dan menampilkan hasil pengujian. Dibutuhkan 1GB ruang penyimpanan.",
"recurringIntervalMessage": "Jalankan sekali setiap hari | Jalankan sekali setiap {0} hari",
"wayToGetKookBotToken": "Buat aplikasi dan dapatkan token bot Anda di {0}",
"Custom Monitor Type": "Tipe Monitor Khusus",
"API Keys": "Kunci API",
"Expiry": "Kadaluarsa",
"noGroupMonitorMsg": "Tidak tersedia. Buat Monitor Grup Terlebih Dahulu.",
"Close": "Tutup",
"telegramMessageThreadID": "(Opsional) ID Pesan",
"Date and Time": "Tanggal dan waktu",
"Single Maintenance Window": "Jendela Pemeliharaan Tunggal",
"wayToGetZohoCliqURL": "Anda dapat mempelajari cara membuat URL webhook {0}.",
"dayOfWeek": "Hari dalam seminggu",
"dayOfMonth": "Hari dalam Bulan",
"lastDay": "Hari terakhir",
"Clone Monitor": "Klon Monitor",
"Clone": "Klon",
"Server Address": "Alamat server",
"Edit Tag": "Sunting Tag",
"smseagleTo": "Nomor telepon",
"maintenanceStatus-under-maintenance": "Dalam perbaikan",
"webhookAdditionalHeadersDesc": "Menetapkan header tambahan yang dikirim dengan webhook. Setiap header harus didefinisikan sebagai kunci/nilai JSON.",
"webhookCustomBodyDesc": "Tentukan Body HTTP khusus untuk permintaan tersebut. Variabel template {msg}, {heartbeat}, {monitor} yang diterima.",
"webhookBodyPresetOption": "Prasetel - {0}",
"webhookBodyCustomOption": "Body Kustom",
"Packet Size": "Ukuran Paket",
"telegramMessageThreadIDDescription": "Pengidentifikasi unik Opsional untuk pesan target (topik) forum; untuk forum supergrup saja",
"telegramProtectContent": "Lindungi Forwarding/Saving",
"or": "atau",
"sameAsServerTimezone": "Sama seperti Zona Waktu Server",
"endDateTime": "Tanggal/Waktu Berakhir",
"cronExpression": "Ekspresi Cron",
"cronSchedule": "Jadwal: ",
"invalidCronExpression": "Ekspresi Cron Tidak Valid: {0}",
"recurringInterval": "Selang waktu",
"warningTimezone": "Itu menggunakan zona waktu server",
"weekdayShortMon": "Senin",
"weekdayShortTue": "Selasa",
"weekdayShortWed": "Rabu",
"weekdayShortThu": "Kamis",
"weekdayShortFri": "Jum'at",
"weekdayShortSat": "Sabtu",
"weekdayShortSun": "Minggu",
"lastDay1": "Hari Terakhir dalam Sebulan",
"lastDay2": "2 Hari Terakhir Bulan Ini",
"lastDay3": "3 Hari Terakhir Bulan Ini",
"lastDay4": "4 Hari Terakhir Bulan Ini",
"No Maintenance": "Tidak Ada Pemeliharaan",
"pauseMaintenanceMsg": "Anda yakin ingin menjeda?",
"maintenanceStatus-inactive": "Tidak aktif",
"Display Timezone": "Tampilkan Zona Waktu",
"IconUrl": "URL ikon",
"Enable DNS Cache": "Aktifkan Cache DNS",
"Enable": "Aktifkan",
"Disable": "Nonaktifkan",
"affectedStatusPages": "Tampilkan pesan pemeliharaan ini pada halaman status yang dipilih",
"invertKeywordDescription": "Carilah kata kunci untuk menjadi tidak ada daripada hadir.",
"wayToGetKookGuildID": "Aktifkan 'Mode Pengembang' di pengaturan Kook, dan klik kanan guild untuk mendapatkan ID-nya",
"Strategy": "Strategi",
"high": "tinggi",
"SendKey": "SendKey",
"Lowcost": "rendah",
"smseagleContact": "nama kontak buku telepon",
"smseagleRecipient": "Penerima (jika banyak harus dipisahkan dengan koma)",
"smseagleEncoding": "Kirim sebagai Unicode",
"smseaglePriority": "Prioritas pesan (0-9, default = 0)",
"Learn More": "Pelajari lebih lanjut",
"Badge Up Color": "Warna atas Lencana",
"Badge Maintenance Color": "Warna Lencana Pemeliharaan",
"Badge Warn Color": "Warna Lencana Peringatan",
"Request Body": "Permintaan Body",
"uninstalling": "Menghapus instalan",
"notificationRegional": "Daerah",
"atLeastOneMonitor": "Pilih setidaknya satu monitor yang terpengaruh",
"pushoverMessageTtl": "TTL pesan (Detik)",
"smseagleGroup": "Nama grup buku telepon",
"smseagleRecipientType": "Tipe Penerima",
"smseagleToken": "Token Akses API",
"smseagleUrl": "URL perangkat SMSEagle Anda",
"Schedule maintenance": "Jadwalkan pemeliharaan",
"Affected Monitors": "Monitor yang Terpengaruh",
"Start of maintenance": "Mulai pemeliharaan",
"All Status Pages": "Semua Halaman Status",
"Select status pages...": "Pilih halaman status…",
"Custom": "Khusus",
"Optional": "Opsional",
"dnsCacheDescription": "Ini mungkin tidak berfungsi di beberapa lingkungan IPv6, nonaktifkan jika Anda mengalami masalah.",
"Maintenance Time Window of a Day": "Jendela Waktu Perawatan dalam Sehari",
"Effective Date Range": "Rentang Tanggal Efektif (Opsional)",
"Schedule Maintenance": "Jadwal Pemeliharaan",
"Badge Down Color": "Warna bawah Lencana",
"Badge Warn Days": "Hari Lencana Peringatan",
"statusPageRefreshIn": "Muat ulang dalam: {0}",
"webhookAdditionalHeadersTitle": "Header Tambahan",
"maintenanceStatus-unknown": "Tidak dikenal",
"Server Timezone": "Zona Waktu Server",
"maintenanceStatus-scheduled": "Dijadwalkan",
"maintenanceStatus-ended": "Berakhir",
"dataRetentionTimeError": "Periode retensi harus 0 atau lebih besar",
"chromeExecutable": "Chrome/Chromium Dapat Dijalankan",
"chromeExecutableAutoDetect": "Deteksi otomatis",
"Edit Maintenance": "Sunting Pemeliharaan",
"DateTime Range": "Rentang Tanggal dan Waktu",
"loadingError": "Tidak dapat mengambil data, harap coba lagi nanti.",
"installing": "Memasang",
"uninstall": "Copot pemasangan",
"confirmUninstallPlugin": "Anda yakin ingin mencopot pemasangan plugin ini?",
"cloneOf": "Klon dari {0}",
"affectedMonitorsDescription": "Pilih monitor yang terpengaruh oleh pemeliharaan saat ini",
"You can divide numbers with": "Anda dapat membagi angka dengan",
"Continue": "Lanjutkan",
"Badge Style": "Gaya Lencana",
"Badge value (For Testing only.)": "Nilai lencana (Hanya untuk Pengujian.)",
"Badge URL": "URL lencana",
"Badge Down Days": "Hari Penghentian Lencana",
"telegramSendSilentlyDescription": "Mengirim pesan secara senyap. Pengguna akan menerima notifikasi tanpa suara.",
"telegramProtectContentDescription": "Jika diaktifkan, pesan bot di Telegram akan dilindungi dari forwarding dan saving.",
"plugin": "Pengaya | Plugin",
"install": "Pasang",
"promosmsAllowLongSMS": "Izinkan SMS panjang",
"Badge Pending Color": "Warna Lencana Tertunda",
"Monitor Group": "Monitor Grup"
}

@ -515,5 +515,42 @@
"Body Encoding": "ボディエンコード",
"Learn More": "さらに詳しく",
"infiniteRetention": "保持期間を無制限にしたい場合は、0に設定してください。",
"Display Timezone": "表示タイムゾーン"
"Display Timezone": "表示タイムゾーン",
"startDateTime": "開始日時",
"User Key": "ユーザーキー",
"SecretKey": "シークレットキー",
"Home": "ホーム",
"webhookBodyCustomOption": "カスタムbody",
"octopushPhoneNumber": "電話番号 (初期フォーマット, 例: +33612345678) ",
"Topic": "トピック",
"pushoverMessageTtl": "メッセージTTL(秒)",
"apiCredentials": "API認証情報",
"Economy": "エコノミー",
"statusPageRefreshIn": "{0}後に再読み込みします",
"filterActivePaused": "停止中",
"filterActive": "有効",
"Example:": "例: {0}",
"Read more:": "さらに: {0}",
"Status:": "ステータス: {0}",
"Enable TLS": "TLS 有効",
"AccessKeyId": "アクセスキーID",
"SecretAccessKey": "アクセスキーシークレット",
"PhoneNumbers": "携帯電話番号",
"Date and Time": "日時",
"chromeExecutableAutoDetect": "自動検出",
"More info on:": "詳細はこちら: {0}",
"Cannot connect to the socket server": "ソケットサーバーに接続できません",
"Reconnecting...": "再接続中...",
"endDateTime": "終了日時",
"cronSchedule": "スケジュール ",
"Edit Maintenance": "メンテナンスの編集",
"WebHookUrl": "ウェブフックUrl",
"Notification Service": "通知サービス",
"atLeastOneMonitor": "最低一つは影響を受けるモニターを選択してください",
"Json Query": "Jsonクエリ",
"octopushSMSSender": "SMS送信者名311文字の英数字とスペースazAz09",
"Lowcost": "低コスト",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "通知サービスの一覧からデバイス名を見つけるには、Home Assistantの「開発者ツール > サービス」から通知で検索してください。",
"Notify Channel": "通知チャンネル",
"Icon Emoji": "絵文字アイコン"
}

@ -589,6 +589,8 @@
"RadiusCalledStationIdDescription": "접속 스테이션의 식별자",
"RadiusCallingStationId": "접속 요청 스테이션의 Id",
"RadiusCallingStationIdDescription": "접속 요청 스테이션의 식별자",
"timeoutAfter": "{0}초 후 타임아웃",
"Request Timeout": "요청 타임아웃",
"Query": "쿼리",
"settingsCertificateExpiry": "TLS 인증서 만료",
"certificationExpiryDescription": "HTTPS 모니터링 TLS 인증서가 만료되면 알림을 활성화해요:",

@ -0,0 +1,93 @@
{
"languageName": "အင်္ဂလိပ်ဘာသာ",
"Settings": "ပြင်ဆင်ချက်များ",
"Help": "အကူအညီ",
"New Update": "အသစ်ထွက်ရှိခြင်း",
"Language": "ဘာသာစကား",
"Appearance": "သွင်ပြင်လက္ခဏာ",
"Theme": "သဏ္ဍာန်",
"General": "အထွေထွေ",
"Primary Base URL": "ဦးစားပေးအင်တာနက်လိပ်စာ",
"Version": "စနစ်အဆင့်",
"List": "စာရင်း",
"Home": "ပင်မစာမျက်နှာ",
"Dashboard": "ခြုံငုံသုံးသပ်ချက်စာမျက်နှာ",
"Add": "အသစ်ပေါင်းထည့်မည်",
"Quick Stats": "စာရင်းအားခြုံငုံကြည့်ရှုခြင်း",
"Up": "ချိတ်ဆက်မှုအောင်မြင်နေသည်",
"Maintenance": "ပြုပြင်နေသည်",
"Unknown": "အမည်မသိအကြောင်းအရာ",
"Reconnecting...": "ပြန်လည်ချိတ်ဆက်နေပါသည်...",
"General Monitor Type": "အထွေထွေစောင့်ကြည့်မှုပုံစံ",
"markdownSupported": "Markdown syntax အားထောက်ပံ့သည်",
"pauseDashboardHome": "ခေတ္တရပ်တန့်မည်",
"Pause": "ခေတ္တရပ်တန့်မည်",
"Name": "အမ်",
"Status": "အခြေအနေများ",
"DateTime": "နေ့ရက်နှင့်အချိန်",
"Message": "စာတို",
"No important events": "အရေးမကြီးသောဖြစ်ရပ်များ",
"Game": "ဂိမ်း",
"Check Update On GitHub": "အသစ်ထွက်ရှိမှုအား GitHub တွင် စစ်ဆေးရန်",
"Add New Monitor": "စောင့်ကြည့်မှုအသစ်ပေါင်းထည့်မည်",
"Down": "ကွန်ရက်ပြတ်တောက်နေသည်",
"Pending": "ဆိုင်းငံ့ဆဲ",
"statusMaintenance": "ပြုပြင်ထိန်းသိမ်းခြင်း",
"Cannot connect to the socket server": "Socket Server အားချိတ်ဆက်မှုမအောင်မြင်ပါ",
"Passive Monitor Type": "Passive စောင့်ကြည့်မှုပုံစံ",
"Specific Monitor Type": "သီးခြားစောင့်ကြည့်မှုပုံစံ",
"Resume": "ဆက်သွားမည်",
"Edit": "ပြောင်းလဲမည်",
"Delete": "ဖျက်သိမ်းမည်",
"Current": "ယခုလက်ရှိ",
"Uptime": "ကွန်ယက်ချိတ်ဆက်မှုကြာချိန်",
"Cert Exp.": "ဆာတီဖီကိတ်ကုန်ဆုံးချိန်",
"Monitor": "စောင့်ကြည့်မှု | စောင့်ကြည့်မှုများ",
"day": "နေ့ရက် | ရက်များ",
"-day": "-နေ့ရက်",
"hour": "နာရီ",
"-hour": "-နာရီ",
"Response": "တုံ့ပြန်မှု",
"Monitor Type": "စောင့်ကြည့်မှုပုံစံ",
"Keyword": "စာသား",
"Invert Keyword": "စာသားပြောင်းပြန်",
"Friendly Name": "မှတ်သားရန်လွယ်ကူသည့်အမည်",
"URL": "အင်တာနက်လိပ်စာ",
"Hostname": "စောင့်ကြည့်မှုအမည်",
"Port": "အပေါက်",
"Advanced": "အဆင့်မြင့်ပြင်ဆင်မှုများ",
"checkEverySecond": "{0} စက္ကန့်တိုင်း‌စောင့်ကြည့်မည်",
"retryCheckEverySecond": "{0} စက္ကန့်တိုင်း‌ထပ်မံကြိုးစားမည်",
"resendEveryXTimes": "{0} စက္ကန့်တိုင်း‌ထပ်မံပေးပို့မည်",
"resendDisabled": "ပိတ်ဆို့ထားခြင်းအားပေးပို့မ်",
"ignoreTLSError": "HTTPS ဝက်ဘ်ဆိုဒ်များအတွက် TLS/SSL အပြစ်အား‌လျစ်လျှူရှုမည်",
"Save": "သိမ်းဆည်းမည်",
"Notifications": "သတိပေးချက်များ",
"Not available, please setup.": "လတ်တလောမရရှိနိုင်ပါ၊ ကျေးဇူးပြု၍ပြင်ဆင်ပါ",
"Setup Notification": "ပြင်ဆင်ခြင်းသတိပေးချက်",
"Light": "အလင်း",
"Dark": "အမှောင်",
"Auto": "အလိုအလျောက်",
"Theme - Heartbeat Bar": "သဏ္ဍာန်-ကွန်ယက်တိုင်းတာနှုန်းပေတံ",
"Normal": "ပုံမှန်",
"Bottom": "အောက်ဆုံး",
"None": "ဘာမှမရှိပါ",
"Timezone": "အချိန်ဇုန်",
"Allow indexing": "အစီအစဉ်ချခြင်းကိုခွင့်ပြုရန်",
"Change Password": "စကားဝှက်ပြောင်းမည်",
"Current Password": "လက်ရှိစကားဝှက်",
"New Password": "စကားဝှက်အသစ်",
"Repeat New Password": "စကားဝှက်အသစ်အားထပ်မံရိုက်ထည့်ပါ",
"Update Password": "စကားဝှက်အားပြင်ဆင်မည်",
"Disable Auth": "ဝင်ရောက်မှုထပ်မံစစ်ဆေးခြင်းအားဖျက်သိမ်းမည်",
"Enable Auth": "ဝင်ရောက်မှုထပ်မံစစ်ဆေးခြင်းအားအတည်ပြုမည်",
"disableauth.message1": "</strong>ဝင်ရောက်မှုထပ်မံစစ်ဆေးခြင်းကိုဖျက်သိမ်းရန်</strong>သေချာပါသလား?",
"Ping": "ချိတ်ဆက်မှုတိုင်းတာခြင်း",
"Expected Value": "မျှော်လင့်ထားသည့်တန်ဖိုး",
"Heartbeat Interval": "ကွန်ယက်ချိတ်ဆက်နိုင်မှုတိုင်းတာခြင်း အချိန်ကွာဟချက်",
"Retries": "ထပ်မံကြိုးစားမှုများ",
"Heartbeat Retry Interval": "ကွန်ယက်ချိတ်ဆက်နိုင်မှုတိုင်းတာခြင်း ထပ်မံကြိုးစားခြင်း အချိန်ကွာဟချက်",
"Resend Notification if Down X times consecutively": "ကွန်ယက်ချိတ်ဆက်မှု X အကြိမ်ထိ ဆက်တိုက်ကျနေပါက သတိပေးချက်ထပ်မံပေးပို့ရန်",
"retriesDescription": "ဝန်ဆောင်မှုကွန်ယက်ပြတ်တောက်နေ၍ သတိပေးချက်ပေးပို့ပြီး အများဆုံးထပ်မံကြိုးစားနိုင်မှု",
"Search Engine Visibility": "ရှာဖွေမှုအင်ဂျင်များ၏မြင်နိုင်စွမ်း"
}

@ -584,7 +584,7 @@
"Enable": "Inschakelen",
"Disable": "Uitschakelen",
"Single Maintenance Window": "Enkel onderhoudsperiode",
"Effective Date Range": "Effectieve periode",
"Effective Date Range": "Effectieve periode (Optioneel)",
"Schedule Maintenance": "Onderhoud inplannen",
"Date and Time": "Datum en tijd",
"DateTime Range": "Datum en tijd periode",
@ -618,7 +618,7 @@
"Lowcost": "Lowcost",
"Economy": "Economy",
"webhookAdditionalHeadersTitle": "Extra Headers",
"webhookAdditionalHeadersDesc": "Voegt extra headers toe die meegestuurd worden met de webhook.",
"webhookAdditionalHeadersDesc": "Voegt extra headers toe die meegestuurd worden met de webhook. Elke header moet worden gedefinieerd als een JSON key/value.",
"Help": "Hulp",
"Game": "Game",
"statusMaintenance": "Onderhoud",
@ -701,5 +701,106 @@
"pagertreeHigh": "Hoog",
"Clone": "Dupliceer",
"cloneOf": "Duplicaat van {0}",
"Add New Tag": "Voeg nieuw label toe"
"Add New Tag": "Voeg nieuw label toe",
"Body Encoding": "Body Encoding",
"twilioAuthToken": "Auth Token / Api Sleutel Secret",
"twilioAccountSID": "Account SID",
"Badge Preview": "Badge voorbeeld",
"ntfyAuthenticationMethod": "Authenticatiemethode",
"ntfyUsernameAndPassword": "Gebruikersnaam en Wachtwoord",
"twilioApiKey": "Api Sleutel (optioneel)",
"Badge Label Prefix": "Badge Label Voorvoegsel",
"Badge Label Suffix": "Badge label achtervoegsel",
"API Keys": "API Sleutels",
"Expiry": "Verval",
"noGroupMonitorMsg": "Niet beschikbaar. Creëer eerst een Groep Monitor.",
"Notify Channel": "Notify Channel",
"Expiry date": "Vervaldatum",
"Key Added": "Sleutel toegevoegd",
"Badge value (For Testing only.)": "Badgewaarde (Alleen voor testen)",
"aboutNotifyChannel": "Notify channel activeert een melding op bureaublad of mobiel voor alle leden van de channel, ongeacht of hun beschikbaarheid is ingesteld op actief of afwezig.",
"apiKey-inactive": "Inactief",
"disableAPIKeyMsg": "Weet je zeker dat je deze API-sleutel wilt uitschakelen?",
"Show Clickable Link Description": "Als deze optie is aangevinkt, heeft iedereen die toegang heeft tot deze statuspagina toegang tot de monitor URL.",
"Badge Duration (in hours)": "Duur badge (in uren)",
"Badge Maintenance Color": "Badge Onderhoud Kleur",
"Badge URL": "Badge URL",
"Close": "Sluit",
"Request Body": "Request Body",
"pagertreeIntegrationUrl": "Integratie URL",
"pagertreeUrgency": "Urgentie",
"pagertreeSilent": "Stil",
"telegramMessageThreadID": "(Optioneel) Message Thread ID",
"Clone Monitor": "Kloon Monitor",
"Expires": "Vervalt",
"webhookCustomBodyDesc": "Definieer een aangepaste HTTP Body voor de request. Template variabelen {msg}, {heartbeat}, {monitor} worden geaccepteerd.",
"webhookBodyPresetOption": "Vooraf ingesteld - {0}",
"webhookBodyCustomOption": "Aangepaste Body",
"notificationRegional": "Regionaal",
"No API Keys": "Geen API Sleutels",
"apiKeyAddedMsg": "Je API-sleutel is toegevoegd. Noteer deze, want hij wordt niet meer weergegeven.",
"Add API Key": "Voeg API Sleutel toe",
"telegramSendSilently": "Stil verzenden",
"telegramSendSilentlyDescription": "Stille verzending van het bericht. Gebruikers ontvangen een melding zonder geluid.",
"Home": "Home",
"Don't expire": "Verval nooit",
"Continue": "Ga verder",
"Add Another": "Nog een toevoegen",
"lunaseaTarget": "Doel",
"lunaseaDeviceID": "Apparaat ID",
"lunaseaUserID": "Gebruiker ID",
"Badge Color": "Badge kleur",
"wayToGetPagerTreeIntegrationURL": "Nadat je de Uptime Kuma-integratie in PagerTree hebt gemaakt, kopieert je het eindpunt. Bekijk alle details {0}",
"Badge Warn Color": "Badge Waarschuwing Kleur",
"Invert Keyword": "Trefwoord omkeren",
"filterActive": "Actief",
"filterActivePaused": "Gepauzeerd",
"statusPageRefreshIn": "Vernieuwen in: {0}",
"telegramMessageThreadIDDescription": "Optioneel Unique identifier voor de target message thread (topic) van het forum; alleen voor forum-supergroepen",
"telegramProtectContentDescription": "Als deze optie is ingeschakeld, zijn de botberichten in Telegram beveiligd tegen doorsturen en opslaan.",
"telegramProtectContent": "Beveilig Doorsturen/Opslaan",
"sameAsServerTimezone": "Zelfde als Server Tijdzone",
"startDateTime": "Begindatum/Tijd",
"endDateTime": "Einddatum/Tijd",
"cronExpression": "Cron expressie",
"cronSchedule": "Planning: ",
"invalidCronExpression": "Ongeldige Cron expressie: {0}",
"chromeExecutableDescription": "Voor Docker-gebruikers, als Chromium nog niet is geïnstalleerd, kan het een paar minuten duren om te installeren en het testresultaat weer te geven. Het neemt 1GB schijfruimte in beslag.",
"invertKeywordDescription": "Kijk of het trefwoord afwezig is in plaats van aanwezig.",
"pushoverMessageTtl": "Bericht TTL (seconden)",
"goAlertInfo": "GoAlert is een open source applicatie voor het plannen van aanwezigheidsdiensten, geautomatiseerde escalaties en meldingen (zoals SMS of spraakoproepen). Schakel automatisch de juiste persoon in, op de juiste manier en op het juiste moment! {0}",
"goAlertIntegrationKeyInfo": "Verkrijg generieke API-integratiesleutel voor de service in dit formaat \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" meestal de waarde van tokenparameter van gekopieerde URL.",
"deleteAPIKeyMsg": "Weet je zeker dat je deze API-sleutel wilt verwijderen?",
"Generate": "Genereer",
"pagertreeMedium": "Medium",
"pagertreeCritical": "Kritisch",
"pagertreeResolve": "Automatisch oplossen",
"pagertreeDoNothing": "Doe niks",
"twilioFromNumber": "Van Nummer",
"twilioToNumber": "Naar Nummer",
"Monitor Setting": "{0}'s Monitor Instelling",
"Show Clickable Link": "Laat klikbare link zien",
"Open Badge Generator": "Open Badge Generator",
"Badge Generator": "{0}'s Badge Generator",
"Badge Type": "Badge type",
"Badge Up Color": "Badge Online Kleur",
"Badge Down Color": "Badge Offline Kleur",
"Badge Warn Days": "Badge Waarschuwing dagen",
"Badge Down Days": "Badge Offline dagen",
"Badge Style": "Badge stijl",
"chromeExecutable": "Chrome/Chromium Executable",
"chromeExecutableAutoDetect": "Automatisch detecteren",
"Edit Maintenance": "Onderhoud bewerken",
"Badge Label": "Badge Label",
"Badge Label Color": "Badge label kleur",
"Badge Prefix": "Badge Waarde Voorvoegsel",
"Badge Pending Color": "Badge In Afwachting Kleur",
"Badge Suffix": "Badge waarde achtervoegsel",
"Group": "Groep",
"Monitor Group": "Monitor Groep",
"Cannot connect to the socket server": "Kan geen verbinding maken met de socket-server",
"Reconnecting...": "Opnieuw verbinden...",
"Expected Value": "Verwachte waarde",
"Json Query": "Json Query",
"jsonQueryDescription": "Voer een JSON-query uit op de respons en controleer de verwachte waarde (De retourwaarde wordt omgezet naar een string voor vergelijking). Bekijk <a href='https://jsonata.org/'>jsonata.org</a> voor de documentatie over de querytaal. Een speelplaats is beschikbaar <a href='https://try.jsonata.org/'>hier</a>."
}

@ -448,7 +448,7 @@
"Backup": "Backup",
"About": "O skrypcie",
"wayToGetCloudflaredURL": "(Pobierz cloudflared z {0})",
"cloudflareWebsite": "Strona Cloudflare",
"cloudflareWebsite": "strona Cloudflare",
"Message:": "Wiadomość:",
"Don't know how to get the token? Please read the guide:": "Nie wiesz jak uzyksać token? Przeczytaj proszę poradnik:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Bieżące połączenie może zostać utracone, jeśli aktualnie łączysz się przez tunel Cloudflare. Czy na pewno chcesz to przerwać? Wpisz swoje aktualne hasło, aby je potwierdzić.",
@ -613,7 +613,7 @@
"You can divide numbers with": "Możesz dzielić liczby przez",
"or": "lub",
"recurringInterval": "odstęp czasu",
"Recurring": "powtarzający się",
"Recurring": "Powtarzający się",
"strategyManual": "Aktywowany/dezaktywowany ręcznie",
"warningTimezone": "Używa strefy czasowej serwera",
"weekdayShortMon": "pon",
@ -623,8 +623,8 @@
"weekdayShortFri": "pt",
"weekdayShortSat": "sob",
"weekdayShortSun": "niedz",
"dayOfWeek": "Dzień tygodnia",
"dayOfMonth": "Dzień miesiąca",
"dayOfWeek": "dzień tygodnia",
"dayOfMonth": "dzień miesiąca",
"lastDay": "Ostatni dzień",
"lastDay1": "Ostatni dzień miesiąca",
"lastDay2": "2. ostatni dzień miesiąca",
@ -646,8 +646,8 @@
"confirmDeleteTagMsg": "Czy na pewno chcesz usunąć ten tag? Monitory powiązane z tym tagiem nie zostaną usunięte.",
"Kook": "Kook",
"Enable TLS": "Włącz TLS",
"webhookAdditionalHeadersDesc": "Ustawia dodatkowe nagłówki wysyłane z webhookiem.",
"dnsCacheDescription": "Może nie działać w niektórych środowiskach z IPv6. Wyłącz, jeśli napotkasz jakiekolwiek problemy.",
"webhookAdditionalHeadersDesc": "Ustawia dodatkowe nagłówki wysyłane z webhookiem. Każdy nagłówek powinien być zdefiniowany jako klucz/wartość JSON.",
"dnsCacheDescription": "Może nie działać w niektórych środowiskach IPv6, wyłącz ją, jeśli napotkasz jakiekolwiek problemy.",
"wayToGetKookBotToken": "Utwórz aplikację i uzyskaj swój token bota na {0}",
"wayToGetKookGuildID": "Włącz 'Developer Mode' w ustawieniach Kook'a i kliknij prawym przyciskiem myszy na gildię, aby uzyskać jej ID",
"Game": "Gra",
@ -659,7 +659,7 @@
"Disable": "Wyłącz",
"Date and Time": "Data i czas",
"IconUrl": "URL ikony",
"Enable DNS Cache": "Włącz pamięć podręczną DNS",
"Enable DNS Cache": "Włącz pamięć podręczną DNS dla monitorów HTTP",
"Single Maintenance Window": "Pojedyncze okno konserwacji",
"Effective Date Range": "Zakres dat obowiązywania (opcjonalnie)",
"Schedule Maintenance": "Planowanie konserwacji",
@ -730,7 +730,7 @@
"twilioToNumber": "Do numeru",
"lunaseaTarget": "Cel",
"twilioAccountSID": "SID konta",
"twilioAuthToken": "Token autoryzacyjny",
"twilioAuthToken": "Token autoryzacji / klucz Api Secret",
"apiKeyAddedMsg": "Twój klucz API został dodany. Prosimy o zanotowanie go, ponieważ nie będzie on już więcej pokazywany.",
"telegramMessageThreadID": "(Opcjonalne) ID wątku wiadomości",
"telegramMessageThreadIDDescription": "Opcjonalny Unikalny identyfikator dla docelowego wątku wiadomości (tematu) forum; tylko dla supergrup forum",
@ -748,7 +748,7 @@
"sameAsServerTimezone": "Tak jak strefa czasowa serwera",
"endDateTime": "Data/godzina zakończenia",
"cronExpression": "Wyrażenie Cron",
"ntfyAuthenticationMethod": "Metoda Uwierzytelnienia",
"ntfyAuthenticationMethod": "Metoda uwierzytelniania",
"ntfyUsernameAndPassword": "Nazwa użytkownika i hasło",
"noGroupMonitorMsg": "Niedostępna. Stwórz najpierw grupę monitorów.",
"Close": "Zamknij",
@ -757,5 +757,86 @@
"Group": "Grupa",
"Monitor Group": "Grupa monitora",
"Reconnecting...": "Ponowne łączenie...",
"Cannot connect to the socket server": "Nie można połączyć się z serwerem gniazda"
"Cannot connect to the socket server": "Nie można połączyć się z serwerem gniazda",
"Badge Label Suffix": "Sufiks etykiety odznaki",
"Badge Warn Color": "Kolor ostrzeżenia odznaki",
"Badge Style": "Styl odznaki",
"Badge value (For Testing only.)": "Wartość odznaki (tylko do testów.)",
"Show Clickable Link": "Pokaż klikalne łącze",
"Show Clickable Link Description": "Jeśli to pole jest zaznaczone, każdy, kto ma dostęp do tej strony stanu, może mieć dostęp do adresu URL monitora.",
"Open Badge Generator": "Generator odznak Open Badge",
"Badge Generator": "{0} Generator odznak",
"Monitor Setting": "{0} Ustawienia monitora",
"Badge Duration": "Czas trwania odznaki",
"Badge Label": "Etykieta odznaki",
"Badge Suffix": "Sufiks wartości odznaki",
"chromeExecutable": "Plik wykonywalny Chrome/Chromium",
"chromeExecutableAutoDetect": "Wykryj automatycznie",
"chromeExecutableDescription": "W przypadku użytkowników Dockera, jeśli Chromium nie jest jeszcze zainstalowany, instalacja i wyświetlenie wyniku testu może zająć kilka minut. Zajmuje 1 GB miejsca na dysku.",
"Edit Maintenance": "Edycja konserwacji",
"Badge Type": "Typ odznaki",
"Badge Prefix": "Prefiks wartości odznaki",
"Badge Color": "Kolor odznaki",
"Badge Label Color": "Kolor etykiety Odznaki",
"Badge Label Prefix": "Prefiks etykiety identyfikatora",
"Badge Pending Color": "Oczekujący kolor odznaki",
"Badge Maintenance Color": "Kolor utrzymania odznaki",
"Badge URL": "Adres URL odznaki",
"gamedigGuessPort": "Gamedig: Port Guess",
"filterActive": "Aktywny",
"webhookCustomBodyDesc": "Definiuje niestandardową treść HTTP dla żądania. Akceptowane są zmienne szablonowe {msg}, {heartbeat}, {monitor}.",
"Select": "Wybierz",
"aboutNotifyChannel": "Powiadomienie kanału wywoła powiadomienie na komputerze lub urządzeniu mobilnym dla wszystkich członków kanału, niezależnie od tego, czy ich dostępność jest ustawiona jako aktywna, czy nie.",
"twilioApiKey": "Klucz API (opcjonalnie)",
"styleElapsedTime": "Czas, który upłynął pod paskiem bicia serca",
"tailscalePingWarning": "Aby korzystać z monitora Tailscale Ping, należy zainstalować Uptime Kuma bez Dockera, a także zainstalować klienta Tailscale na serwerze.",
"invertKeywordDescription": "Słowo kluczowe powinno być raczej nieobecne niż obecne.",
"jsonQueryDescription": "Wykonaj zapytanie json względem odpowiedzi i sprawdź oczekiwaną wartość (wartość zwracana zostanie przekonwertowana na ciąg znaków do porównania). Sprawdź <a href='https://jsonata.org/'>jsonata.org</a>, aby uzyskać dokumentację dotyczącą języka zapytań. Plac zabaw można znaleźć <a href='https://try.jsonata.org/'>tutaj</a>.",
"Server URL should not contain the nfty topic": "Adres URL serwera nie powinien zawierać tematu nfty",
"Badge Duration (in hours)": "Czas trwania odznaki (w godzinach)",
"Enter the list of brokers": "Wprowadź listę brokerów",
"Enable Kafka Producer Auto Topic Creation": "Włącz automatyczne tworzenie tematów w producencie Kafka",
"showCertificateExpiry": "Pokaż wygaśnięcie certyfikatu",
"gamedigGuessPortDescription": "Port używany przez Valve Server Query Protocol może różnić się od portu klienta. Spróbuj tego, jeśli monitor nie może połączyć się z serwerem.",
"Secret AccessKey": "Tajny klucz AccessKey",
"wayToGetFlashDutyKey": "Możesz przejść do Channel -> (Select a Channel) -> Integrations -> Add a new integration' page, dodać \"Custom Event\", aby uzyskać adres push, skopiować klucz integracji w adresie. Więcej informacji można znaleźć na stronie",
"Badge Down Color": "Kolor odznaki Offline",
"Notify Channel": "Powiadom kanał",
"Request Timeout": "Limit czasu żądania",
"timeoutAfter": "Przekroczenie limitu czasu po {0} sekundach",
"styleElapsedTimeShowNoLine": "Pokaż (bez linii)",
"styleElapsedTimeShowWithLine": "Pokaż (z linią)",
"filterActivePaused": "Wstrzymany",
"webhookBodyCustomOption": "Niestandardowa treść",
"webhookBodyPresetOption": "Wstępne ustawienie - {0}",
"selectedMonitorCount": "Wybrano: {0}",
"Check/Uncheck": "Zaznacz/Odznacz",
"PushDeer Server": "Serwer PushDeer",
"pushDeerServerDescription": "Pozostaw puste, aby korzystać z oficjalnego serwera",
"Badge Preview": "Podgląd odznaki",
"Kafka Brokers": "Broker Kafka",
"Press Enter to add broker": "Naciśnij Enter, aby dodać brokera",
"Kafka Topic Name": "Nazwa tematu Kafka",
"Kafka Producer Message": "Wiadomość producenta Kafka",
"Enable Kafka SSL": "Włącz Kafka SSL",
"Kafka SASL Options": "Opcje SASL Kafki",
"Mechanism": "Mechanizm",
"Pick a SASL Mechanism...": "Wybierz mechanizm SASL...",
"nostrRelaysHelp": "Jeden adres URL przekaźnika w wierszu",
"nostrRecipients": "Klucze publiczne odbiorców (npub)",
"nostrRecipientsHelp": "format npub, po jednym w wierszu",
"Authorization Identity": "Identyfikacja autoryzacji",
"AccessKey Id": "Identyfikator klucza AccessKey",
"Session Token": "Token sesji",
"Request Body": "Treść żądania",
"FlashDuty Severity": "Poziom istotności",
"nostrRelays": "Przekaźniki Nostr",
"nostrSender": "Prywatny klucz nadawcy (nsec)",
"Badge Up Color": "Kolor odznaki Online",
"Badge Down Days": "Odznaka dni offline",
"Badge Warn Days": "Odznaka dni z ostrzeżeniem",
"noOrBadCertificate": "Brak/zły certyfikat",
"Invert Keyword": "Odwróć słowo kluczowe",
"Expected Value": "Oczekiwana wartość",
"Json Query": "Zapytanie Json"
}

@ -29,7 +29,7 @@
"Settings": "Configurações",
"Dashboard": "Painel",
"New Update": "Nova Atualização",
"Language": "Linguagem",
"Language": "Idioma",
"Appearance": "Aparência",
"Theme": "Tema",
"General": "Geral",
@ -476,13 +476,13 @@
"Query": "Query",
"settingsCertificateExpiry": "O Certificado TLS Expira",
"Connection Type": "Tipo Da Conexão",
"signedInDisp": "Assinado como {0}",
"signedInDisp": "Logado como {0}",
"RadiusCallingStationId": "ID Da Estação De Chamada",
"RadiusCalledStationIdDescription": "Identificador do dispositivo de chamada",
"Coming Soon": "Em Breve",
"Connection String": "String De Conexão",
"Docker Daemon": "Daemon Do Docker",
"Show Powered By": "Mostrar Distribuído Por",
"Show Powered By": "Mostrar Fornecido Por",
"RadiusSecret": "Segredo Radius",
"RadiusCalledStationId": "ID Da Estação Chamada",
"deleteDockerHostMsg": "Você tem certeza que quer deletar esse host do Docker para todos os monitores?",
@ -569,10 +569,10 @@
"disableAPIKeyMsg": "Você tem certeza de que quer desativar essa chave de API?",
"smtp": "Email (SMTP)",
"secureOptionTLS": "TLS (465)",
"From Email": "Email De",
"From Email": "Remetente",
"smtpCC": "CC",
"smtpBCC": "CCO",
"To Email": "Email Para",
"To Email": "Destinatário",
"Recipients": "Destinatários",
"Google Analytics ID": "ID Google Analytics",
"Post": "Post",
@ -583,5 +583,10 @@
"Automations can optionally be triggered in Home Assistant:": "Automações podem opcionalmente ser disparadas no Home Assistant:",
"secureOptionNone": "Nenhum / STARTTLS (25, 587)",
"apiKeyAddedMsg": "Sua chave de API foi adicionada. Por favor anote essa chave, ela não será mostrada novamente.",
"Show Clickable Link": "Mostrar Link Clicável"
"Show Clickable Link": "Mostrar Link Clicável",
"backupOutdatedWarning": "Obsoleto: Já que muitos recursos foram adicionados e este recurso de backup não foi atualizado, ele não consegue gerar ou restaurar um backup completo.",
"wayToGetDiscordURL": "Voce pode configurar isso indo à Configurações do Servidor -> Integrações -> Ver Webhooks -> Novo Webhook",
"Home": "Início",
"Reconnecting...": "Reconectando...",
"Cannot connect to the socket server": "Não foi possível conectar ao servidor socket"
}

@ -114,7 +114,7 @@
"Notification Type": "Tipo de Notificação",
"Email": "Email",
"Test": "Testar",
"Certificate Info": "Info. do Certificado ",
"Certificate Info": "Info. do Certificado",
"Resolver Server": "Resolver Servidor",
"Resource Record Type": "Tipo de registro de aplicação",
"Last Result": "Último resultado",

@ -0,0 +1,35 @@
{
"Settings": "Definições",
"Help": "Ajuda",
"New Update": "Nova actualização",
"Language": "Linguagem",
"Appearance": "Aspecto",
"Theme": "Tema",
"General": "Geral",
"Game": "Jogo",
"Version": "Versão",
"List": "Lista",
"Add": "Adicionar",
"Quick Stats": "Estatísticas rápidas",
"Up": "Acima",
"Down": "Abaixo",
"Pending": "Pendente",
"statusMaintenance": "Manutenção",
"Maintenance": "Manutenção",
"Unknown": "Desconhecido",
"Reconnecting...": "Reconectando...",
"pauseDashboardHome": "Pausa",
"Pause": "Pausa",
"Name": "Nome",
"Status": "Estado",
"Message": "Mensagem",
"Resume": "Retomar",
"Edit": "Editar",
"Delete": "Remover",
"Current": "Actual",
"Uptime": "Tempo de atividade",
"day": "dia | dias",
"languageName": "Português",
"Primary Base URL": "URL base primário",
"No important events": "Nenhum evento importante"
}

@ -6,7 +6,7 @@
"upsideDownModeDescription": "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.",
"maxRedirectDescription": "Максимальное количество перенаправлений. Поставьте 0, чтобы отключить перенаправления.",
"acceptedStatusCodesDescription": "Выберите коды статусов для определения доступности сервиса.",
"passwordNotMatchMsg": "Введёные пароли не совпадают",
"passwordNotMatchMsg": "Введённые пароли не совпадают",
"notificationDescription": "Привяжите уведомления к мониторам.",
"keywordDescription": "Поиск слова в чистом HTML или в JSON-ответе (чувствительно к регистру).",
"pauseDashboardHome": "Пауза",
@ -446,7 +446,7 @@
"Message:": "Сообщение:",
"Don't know how to get the token? Please read the guide:": "Не знаете, как получить токен? Пожалуйста, прочтите руководство:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Текущее соединение может быть потеряно, если вы в данный момент подключаетесь через туннель Cloudflare. Вы уверены, что хотите это остановить? Введите свой текущий пароль, чтобы подтвердить это.",
"HTTP Headers": "заголовки HTTP",
"HTTP Headers": "Заголовки HTTP",
"Trust Proxy": "Доверять прокси",
"Other Software": "Другое программное обеспечение",
"For example: nginx, Apache and Traefik.": "К примеру: nginx, Apache и Traefik.",
@ -652,7 +652,7 @@
"enableGRPCTls": "Разрешить отправлять gRPC запрос через TLS соединение",
"Free Mobile API Key": "API ключ Free Mobile",
"Edit Tag": "Редактировать тэг",
"webhookAdditionalHeadersDesc": "Устанавливает дополнительные заголовки, отправляемые с помощью веб-хука.",
"webhookAdditionalHeadersDesc": "Устанавливает дополнительные заголовки, отправляемые с помощью веб-хука. Каждый заголовок должен быть определён как JSON ключ/значение.",
"topic": "Тема",
"Customize": "Персонализировать",
"Custom Footer": "Пользовательский footer",
@ -748,7 +748,7 @@
"telegramMessageThreadID": "(Необязательно) ID цепочки сообщений",
"statusPageRefreshIn": "Обновлять каждые: {0}",
"twilioAccountSID": "SID учетной записи",
"twilioAuthToken": "Токен авторизации",
"twilioAuthToken": "Токен авторизации / Секретный API ключ",
"twilioFromNumber": "С номера",
"twilioToNumber": "На номер",
"sameAsServerTimezone": "Аналогично часовому поясу сервера",
@ -765,7 +765,7 @@
"Badge Type": "Тип значка",
"Badge Duration": "Срок действия значка",
"Badge Label": "Надпись для значка",
"Badge Prefix": "Префикс значка",
"Badge Prefix": "Значение префикса значка",
"Badge Label Color": "Цвет надписи значка",
"Badge Color": "Цвет значка",
"Badge Label Prefix": "Префикс надписи для значка",
@ -774,7 +774,7 @@
"Badge Pending Color": "Цвет значка для статуса \"Ожидание\"",
"Badge Maintenance Color": "Цвет значка для статуса \"Техобслуживание\"",
"Badge Style": "Стиль значка",
"Badge Suffix": "Суффикс значка",
"Badge Suffix": "Значение суффикса значка",
"Badge value (For Testing only.)": "Значение значка (только для тестирования)",
"Badge URL": "URL значка",
"Group": "Группа",
@ -791,5 +791,39 @@
"Badge Down Days": "Значок для \"дней недоступности\"",
"Home": "Главная",
"noGroupMonitorMsg": "Не доступно. Создайте сначала группу мониторов.",
"Close": "Закрыть"
"Close": "Закрыть",
"chromeExecutableDescription": "Для пользователей Docker, если Chromium еще не установлен, может потребоваться несколько минут для установки и отображения результата тестирования. Он занимает 1 ГБ дискового пространства.",
"chromeExecutable": "Исполняемый файл Chrome/Chromium",
"chromeExecutableAutoDetect": "Автообнаружение",
"Badge Preview": "Предпросмотр значка",
"Badge Duration (in hours)": "Срок действия значка (в часах)",
"twilioApiKey": "API ключ (необязательно)",
"Expected Value": "Ожидаемое значение",
"Json Query": "JSON запрос",
"Kafka Brokers": "Kafka Brokers",
"Press Enter to add broker": "Нажмите Enter чтобы добавить брокера",
"Kafka Topic Name": "Название темы Kafka",
"Kafka Producer Message": "Сообщение продюсера Kafka",
"Kafka SASL Options": "Параметры SASL в Kafka",
"Mechanism": "Механизм",
"Pick a SASL Mechanism...": "Выберите механизм SASL...",
"AccessKey Id": "AccessKey Id",
"Secret AccessKey": "Секретный ключ доступа",
"Session Token": "Токен сессии",
"jsonQueryDescription": "Выполните json-запрос к ответу и проверьте наличие ожидаемого значения (возвращаемое значение будет преобразовано в строку для сравнения). Посмотрите <a href='https://jsonata.org/'>jsonata.org</a> для получения документации по языку запросов. A Потренироваться вы можете <a href='https://try.jsonata.org/'>сдесь</a>.",
"Notify Channel": "Канал оповещений",
"aboutNotifyChannel": "Уведомление о канале вызовет настольное или мобильное уведомление для всех участников канала, независимо от того, установлена ли их доступность как активная или отсутствующая.",
"Enter the list of brokers": "Введите список брокеров",
"Enable Kafka SSL": "Включение протокола Kafka SSL",
"Enable Kafka Producer Auto Topic Creation": "Включение автоматического создания тем в Kafka Producer",
"Authorization Identity": "Авторизационная идентичность",
"Request Body": "Тело запроса",
"webhookCustomBodyDesc": "Определите пользовательское HTTP Body для запроса. Принимаются шаблонные переменные {msg}, {heartbeat}, {monitor}.",
"webhookBodyCustomOption": "Кастомное тело",
"webhookBodyPresetOption": "Пресет - {}",
"invertKeywordDescription": "Искать, чтобы ключевое слово отсутствовало, а не присутствовало.",
"filterActive": "Активный",
"filterActivePaused": "На паузе",
"Invert Keyword": "Инвертировать ключевое слово",
"tailscalePingWarning": "Для того чтобы использовать монитор Tailscale Ping, необходимо установить Uptime Kuma без Docker, а также установить на сервер клиент Tailscale."
}

@ -472,7 +472,7 @@
"From Name/Number": "จาก ชื่อ / หมายเลข",
"Leave blank to use a shared sender number.": "ไม่ต้องกรอกเพื่อใช้ชื่อผู้ส่งร่วมกัน",
"Octopush API Version": "เวอร์ชั่น API Octopush",
"Legacy Octopush-DM": "Octopush-DM แบบเก่า",
"Legacy Octopush-DM": "Octopush-DM แบบเก่า",
"endpoint": "endpoint",
"octopushAPIKey": "\"API key\" จากข้อมูลยืนยันตัวตน HTTP API ในแผงควบคุม",
"octopushLogin": "\"Login\" จากข้อมูลยืนยันตัวตน HTTP API ในแผงควบคุม",
@ -668,5 +668,9 @@
"uninstalling": "กำลังถอนการติดตั้ง",
"confirmUninstallPlugin": "แน่ใจหรือไม่ว่าต้องการถอนการติดตั้งปลั้กอินนี้?",
"Schedule Maintenance": "กำหนดเวลาซ่อมแซม",
"Edit Maintenance": "แก้ใขการบำรุงรักษา"
"Edit Maintenance": "แก้ใขการบำรุงรักษา",
"recurringIntervalMessage": "ดำเนินการทุกวัน | ดำเนินการทุก {0} วัน",
"chromeExecutableAutoDetect": "ตรวจจับอัตโนมัติ",
"chromeExecutableDescription": "สำหรับผู้ใช้งาน Docker, ถ้ายังไม่ได้ติดตั่ง Chromium, อาจจะเสียเวลาในการติดตั่งและแสดงผลการทดสอบเพิ่มเติม, ใช้พื้นที่ประมาณ 1GB",
"notificationRegional": "ภูมิภาค"
}

@ -587,7 +587,7 @@
"deleteMaintenanceMsg": "Bu bakımı silmek istediğinizden emin misiniz?",
"ZohoCliq": "ZohoCliq",
"webhookAdditionalHeadersTitle": "Ek Başlıklar",
"webhookAdditionalHeadersDesc": "Webhook ile gönderilen ek başlıkları ayarlar.",
"webhookAdditionalHeadersDesc": "Webhook ile gönderilen ek başlıkları ayarlar. Her başlık bir JSON anahtarı/değeri olarak tanımlanmalıdır.",
"wayToGetZohoCliqURL": "Bir webhook URL'sinin nasıl oluşturulacağını öğrenebilirsiniz {0}.",
"Kook": "Kook",
"wayToGetKookBotToken": "Uygulama oluşturun ve {0} adresinde bot tokenı alın",
@ -640,7 +640,7 @@
"Server Timezone": "Sunucu Saat Dilimi",
"statusPageMaintenanceEndDate": "Bitiş Zamanı",
"IconUrl": "Icon URL",
"Enable DNS Cache": "DNS Önbelleğini Etkinleştir",
"Enable DNS Cache": "HTTP monitörleri için DNS Önbelleğini etkinleştir",
"Enable": "Etkin",
"Disable": "Devre Dışı",
"dnsCacheDescription": "Bazı IPv6 ortamlarında çalışmıyor olabilir, herhangi bir sorunla karşılaşırsanız devre dışı bırakın.",
@ -738,7 +738,7 @@
"lunaseaDeviceID": "Cihaz ID",
"lunaseaUserID": "Kullanıcı ID",
"statusPageRefreshIn": "{0} içinde yenilenecek",
"twilioAuthToken": "Kimlik Doğrulama Jetonu",
"twilioAuthToken": "Kimlik Doğrulama Jetonu / Gizli Api Anahtarı",
"twilioFromNumber": "Gönderen Numara",
"twilioToNumber": "Alıcı Numara",
"twilioAccountSID": "Hesap ID",
@ -757,8 +757,8 @@
"Badge Type": "Rozet Türü",
"Badge Duration": "Rozet Süresi",
"Badge Label": "Rozet Etiketi",
"Badge Prefix": "Rozet Öneki",
"Badge Suffix": "Rozet Eki",
"Badge Prefix": "Rozet Değer Öneki",
"Badge Suffix": "Rozet Değer Soneki",
"Badge Label Color": "Rozet Etiket Rengi",
"Badge Color": "Rozet Rengi",
"Badge Label Prefix": "Rozet Etiket Öneki",
@ -782,5 +782,56 @@
"Reconnecting...": "Yeniden bağlanılıyor...",
"Home": "Anasayfa",
"noGroupMonitorMsg": "Uygun değil. Önce bir Grup Monitörü oluşturun.",
"Close": "Kapalı"
"Close": "Kapalı",
"chromeExecutable": "Çalıştırılabilir Chrome/Chromium",
"chromeExecutableAutoDetect": "Otomatik algılama",
"chromeExecutableDescription": "Docker kullanıcıları için Chromium henüz kurulmamışsa, yüklenmesi ve test sonucunun görüntülenmesi birkaç dakika sürebilir. 1 GB disk alanı gerektirir.",
"Invert Keyword": "Anahtar Kelimeyi Ters Çevir",
"invertKeywordDescription": "Anahtar kelimenin mevcut olmasından ziyade mevcut olmamasına bakın.",
"webhookCustomBodyDesc": "İstek için özel bir HTTP Gövdesi tanımlayın. {msg}, {heartbeat}, {monitor} şablon değişkenleri kabul edilir.",
"webhookBodyPresetOption": "Ön ayar - {0}",
"webhookBodyCustomOption": "Özel Gövde",
"Request Body": "İstek Gövdesi",
"jsonQueryDescription": "Yanıta karşı bir json sorgusu yapın ve beklenen değeri kontrol edin (Dönüş değeri, karşılaştırma için dizgeye dönüştürülür). Sorgu diliyle ilgili belgeler için <a href='https://jsonata.org/'>jsonata.org</a>'a bakın. Bir oyun alanı <a href='https://try.jsonata.org/'>burada</a> bulunabilir.",
"twilioApiKey": "Api Anahtarı (isteğe bağlı)",
"Expected Value": "Beklenen Değer",
"Json Query": "Json Sorgusu",
"Badge Duration (in hours)": "Rozet Süresi (saat cinsinden)",
"Badge Preview": "Rozet Önizlemesi",
"Notify Channel": "Bildirim Kanalı",
"aboutNotifyChannel": "Bildirim kanalı, müsaitlik durumu etkin veya uzakta olarak ayarlanmış olsun, kanalın tüm üyeleri için bir masaüstü veya mobil bildirimi tetikler.",
"filterActive": "Aktif",
"filterActivePaused": "Duraklatıldı",
"Enter the list of brokers": "Aracı listesine girin",
"Kafka Topic Name": "Kafka Başlık Adı",
"Kafka Producer Message": "Kafka Üretici Mesajı",
"Enable Kafka SSL": "Kafka SSL'i etkinleştir",
"Kafka SASL Options": "Kafka SASL Seçenekleri",
"Mechanism": "Mekanizma",
"Pick a SASL Mechanism...": "Bir SASL Mekanizması seçin...",
"Authorization Identity": "Yetki Kimliği",
"Secret AccessKey": "Gizli Erişim Anahtarı",
"Session Token": "Oturum Jetonu",
"Kafka Brokers": "Kafka Aracıları",
"Press Enter to add broker": "Aracı eklemek için Enter'a basın",
"Enable Kafka Producer Auto Topic Creation": "Kafka Üreticisi Otomatik Başlık Oluşturmayı Etkinleştir",
"AccessKey Id": "Erişim Anahtarı Kimliği",
"tailscalePingWarning": "Tailscale Ping monitörünü kullanabilmek için Docker olmadan Uptime Kuma kurmanız ve ayrıca sunucunuza Tailscale client kurmanız gerekmektedir.",
"Server URL should not contain the nfty topic": "Sunucu URL'si nfty konusunu içermemelidir",
"FlashDuty Severity": "Önem derecesi",
"nostrRelays": "Nostr röleleri",
"nostrRelaysHelp": "Satır başına bir geçiş URL'si",
"nostrSender": "Gönderen Özel Anahtarı (nsec)",
"nostrRecipients": "Alıcıların Genel Anahtarları (npub)",
"nostrRecipientsHelp": "npub biçimi, her satıra bir tane",
"showCertificateExpiry": "Sertifika Geçerlilik Süresini Göster",
"noOrBadCertificate": "Sertifika Yok/Geçersiz",
"Select": "Seç",
"PushDeer Server": "PushDeer Sunucusu",
"wayToGetFlashDutyKey": "Kanal -> (Bir Kanal Seçin) -> Entegrasyonlar -> Yeni entegrasyon ekle sayfasına gidebilir, bir push adresi almak için bir 'Özel Etkinlik' ekleyebilir, adresteki Entegrasyon Anahtarını kopyalayabilirsiniz. Daha fazla bilgi için lütfen ziyaret edin",
"selectedMonitorCount": "Seçildi: {0}",
"Check/Uncheck": "İşaretle/İşareti Kaldır",
"pushDeerServerDescription": "Resmi sunucuyu kullanmak için boş bırakın",
"Request Timeout": "İstek zaman aşımına uğradı",
"timeoutAfter": "{0} saniye sonra zaman aşımı"
}

@ -556,7 +556,7 @@
"General Monitor Type": "Основний моніторинг",
"error": "Помилка",
"webhookAdditionalHeadersTitle": "Додаткові заголовки",
"webhookAdditionalHeadersDesc": "Задати додаткові заголовки, що за допомогою вебхука.",
"webhookAdditionalHeadersDesc": "Задати додаткові заголовки, що за допомогою вебхука. Кожен заголовок має бути заданий у вигляді JSON ключа/значення.",
"critical": "Критичний",
"Custom": "Нестандартний",
"successMessage": "Повідомлення про успіх",
@ -678,7 +678,7 @@
"Server Timezone": "Часовий пояс сервера",
"statusPageMaintenanceEndDate": "Закінчення",
"IconUrl": "URL-адреса іконки",
"Enable DNS Cache": "Увімкнути DNS-кеш",
"Enable DNS Cache": "Увімкнути DNS-кеш для HTTP(s) моніторів",
"Enable": "Увімкнути",
"confirmDeleteTagMsg": "Ви впевнені, що хочете видалити цей тег? Монітори, пов'язані з цим тегом, не будуть видалені.",
"Guild ID": "ID гільдії",
@ -744,7 +744,7 @@
"lunaseaDeviceID": "ID пристрою",
"lunaseaUserID": "ID користувача",
"twilioAccountSID": "SID облікового запису",
"twilioAuthToken": "Токен авторизації",
"twilioAuthToken": "Токен авторизації / Секретний ключ Api",
"twilioFromNumber": "З номера",
"twilioToNumber": "На номер",
"sameAsServerTimezone": "Такий самий, як часовий пояс сервера",
@ -765,8 +765,8 @@
"Badge Type": "Тип бейджа",
"Badge Duration": "Тривалість бейджа",
"Badge Label": "Ярлик бейджа",
"Badge Prefix": "Префікс бейджа",
"Badge Suffix": "Суфікс бейджа",
"Badge Prefix": "Префікс значення бейджа",
"Badge Suffix": "Суфікс значення бейджа",
"Badge Label Color": "Колір ярлика бейджа",
"Badge Color": "Колір бейджа",
"Badge Label Prefix": "Префікс ярлика бейджа",
@ -788,5 +788,61 @@
"Reconnecting...": "Повторне підключення...",
"Home": "Головна",
"noGroupMonitorMsg": "Недоступно. Спочатку створіть групу моніторів.",
"Close": "Закрити"
"Close": "Закрити",
"chromeExecutableDescription": "Для користувачів Docker, якщо Chromium ще не встановлено, встановлення та відображення результатів тесту може зайняти кілька хвилин. Потрібно 1 ГБ дискового простору.",
"chromeExecutableAutoDetect": "Автоматичне визначення",
"chromeExecutable": "Виконуваний файл Chrome/Chromium",
"Invert Keyword": "Інвертоване ключове слово",
"invertKeywordDescription": "Слідкувати за тим, щоб ключове слово було відсутнім, а не присутнім.",
"webhookCustomBodyDesc": "Задати користувацьке HTTP-тіло для запиту. Приймаються шаблонні змінні {msg}, {heartbeat}, {monitor}.",
"webhookBodyPresetOption": "Пресет - {0}",
"webhookBodyCustomOption": "Користувацьке тіло",
"Request Body": "Тіло запиту",
"Badge Preview": "Попередній перегляд бейджа",
"Badge Duration (in hours)": "Тривалість бейджа (у годинах)",
"jsonQueryDescription": "Виконувати json-запит до відповіді та перевірити очікуване значення (значення, що повертається, буде перетворено в рядок для порівняння). Зверніться до <a href='https://jsonata.org/'>jsonata.org</a> щоб ознайомитися з документацією про мову запитів. Навчальний майданчик можна знайти <a href='https://try.jsonata.org/'>тут</a>.",
"twilioApiKey": "Api ключ (необов'язково)",
"Expected Value": "Очікуване значення",
"Json Query": "Json-запит",
"Notify Channel": "Сповіщення каналу",
"aboutNotifyChannel": "Сповіщення каналу надішле сповіщення на десктоп або мобільний для всіх учасників каналу, незалежно від того, встановлена їхня доступність як активна чи відсутня.",
"filterActive": "Активні",
"filterActivePaused": "Призупинені",
"Kafka Brokers": "Брокери Kafka",
"Press Enter to add broker": "Натисніть Enter, щоб додати брокер",
"Kafka Topic Name": "Назва теми Kafka",
"Kafka SASL Options": "Параметри Kafka SASL",
"Mechanism": "Механізм",
"Pick a SASL Mechanism...": "Виберіть механізм SASL...",
"Authorization Identity": "Ідентифікатор авторизації",
"AccessKey Id": "AccessKey Id",
"Secret AccessKey": "Secret AccessKey",
"Session Token": "Токен сесії",
"Enable Kafka SSL": "Увімкнути Kafka SSL",
"Enable Kafka Producer Auto Topic Creation": "Увімкнути автоматичне створення тем Kafka Producer",
"Enter the list of brokers": "Введіть список брокерів",
"Kafka Producer Message": "Повідомлення Kafka Producer",
"tailscalePingWarning": "Для того, щоб використовувати монітор Tailscale Ping, вам потрібно встановити Uptime Kuma без Docker, а також встановити клієнт Tailscale на вашому сервері.",
"Server URL should not contain the nfty topic": "URL-адреса сервера не повинна містити тему nfty",
"FlashDuty Severity": "Серйозність",
"nostrRelays": "Реле Nostr",
"nostrRelaysHelp": "Одна URL-адреса реле в рядку",
"nostrSender": "Приватний ключ відправника (nsec)",
"nostrRecipients": "Відкриті ключі одержувачів (npub)",
"showCertificateExpiry": "Показати термін дії сертифікату",
"noOrBadCertificate": "Відсутність/поганий сертифікат",
"Select": "Вибрати",
"selectedMonitorCount": "Вибрано: {0}",
"wayToGetFlashDutyKey": "Ви можете перейти на сторінку \"Канал -> (Виберіть канал) -> Інтеграції -> Додати нову інтеграцію\", додати \"Користувацьку подію\", щоб отримати пуш-адресу, скопіювати ключ інтеграції в адресу. Для отримання додаткової інформації, будь ласка, відвідайте",
"nostrRecipientsHelp": "Формат npub, по одному в рядку",
"Check/Uncheck": "Встановити/зняти галочку",
"PushDeer Server": "Сервер PushDeer",
"pushDeerServerDescription": "Залиште порожнім, щоб використовувати офіційний сервер",
"Request Timeout": "Таймаут запиту",
"timeoutAfter": "Таймаут через {0} секунд",
"styleElapsedTime": "Час, що минув під індикатором серцебиття",
"gamedigGuessPort": "Gamedig: Вгадати порт",
"gamedigGuessPortDescription": "Порт, що використовується протоколом запитів до сервера Valve, може відрізнятися від порту клієнта. Спробуйте це, якщо монітор не може підключитися до вашого сервера.",
"styleElapsedTimeShowWithLine": "Показати (з лінією)",
"styleElapsedTimeShowNoLine": "Показати (без лінії)"
}

@ -273,7 +273,7 @@
"Custom CSS": "اپنی مرضی کے مطابق سی ایس ایس",
"deleteProxyMsg": "کیا آپ واقعی اس پراکسی کو تمام مانیٹر کے لیے حذف کرنا چاہتے ہیں؟",
"enableProxyDescription": "یہ پراکسی مانیٹر کی درخواستوں پر اس وقت تک اثر نہیں کرے گی جب تک کہ اسے فعال نہ کیا جائے۔ آپ ایکٹیویشن اسٹیٹس کے ذریعے تمام مانیٹرس سے پراکسی کو عارضی طور پر غیر فعال کر سکتے ہیں۔",
"setAsDefaultProxyDescription": "یہ پراکسی نئے مانیٹرز کے لیے بطور ڈیفالٹ فعال ہو جائے گی۔ آپ اب بھی ہر مانیٹر کے لیے الگ الگ پراکسی کو غیر فعال کر سکتے ہیں۔",
"setAsDefaultProxyDescription": "یہ پراکسی نئے مانیٹرز کے لئے ڈیفالٹ طور پر فعال ہوجائے گی۔ آپ اب بھی ہر مانیٹر کے لئے پراکسی کو الگ سے غیر فعال کرسکتے ہیں۔",
"Page Not Found": "صفحہ نہیں ملا",
"wayToGetCloudflaredURL": "({0} سے کلاؤڈ فلارڈ ڈاؤن لوڈ کریں)",
"Don't know how to get the token? Please read the guide:": "ٹوکن حاصل کرنے کا طریقہ نہیں جانتے؟ براہ کرم گائیڈ پڑھیں:",
@ -430,7 +430,7 @@
"weekdayShortTue": "منگل",
"Add New Tag": "نیا ٹیگ شامل کریں",
"Enable DNS Cache": "ڈی این ایس کیشے کو فعال کریں",
"Effective Date Range": "مؤثر تاریخ کی حد",
"Effective Date Range": "مؤثر تاریخ کی حد (اختیاری)",
"Schedule Maintenance": "شیڈول کی بحالی",
"Date and Time": "تاریخ اور وقت",
"DateTime Range": "تاریخ کے وقت کی حد",
@ -447,5 +447,19 @@
"Display Timezone": "ٹائم زون ڈسپلے کریں",
"Server Timezone": "سرور ٹائم زون",
"statusPageMaintenanceEndDate": "ختم",
"IconUrl": "آئیکن یو آر ایل"
"IconUrl": "آئیکن یو آر ایل",
"Cannot connect to the socket server": "ساکٹ سرور سے رابطہ قائم نہیں کیا جا سکتا",
"chromeExecutable": "کروم/کرومیم قابل عمل",
"chromeExecutableDescription": "ڈوکر صارفین کے لئے ، اگر کرومیم ابھی تک انسٹال نہیں ہے تو ، ٹیسٹ کے نتائج کو انسٹال کرنے اور ظاہر کرنے میں کچھ منٹ لگ سکتے ہیں۔ یہ ڈسک کی 1 جی بی جگہ لیتا ہے.",
"cronSchedule": "شیڈول: ",
"cronExpression": "کرون ایکسپریشن",
"invalidCronExpression": "غیر قانونی کرون اظہار: {0}",
"Home": "گھر",
"sameAsServerTimezone": "سرور ٹائم زون کی طرح",
"startDateTime": "آغاز کی تاریخ / وقت",
"endDateTime": "اختتام کی تاریخ / وقت",
"installing": "تنصیب",
"chromeExecutableAutoDetect": "آٹو کھوج",
"Edit Maintenance": "دیکھ بھال میں ترمیم کریں",
"Reconnecting...": "دوبارہ رابطہ قائم کرنا..."
}

@ -434,7 +434,7 @@
"Page Not Found": "Page Not Found",
"Reverse Proxy": "Reverse Proxy",
"Backup": "Backup",
"About": "About",
"About": "Về",
"wayToGetCloudflaredURL": "(Download cloudflared from {0})",
"cloudflareWebsite": "Cloudflare Website",
"Message:": "Message:",
@ -489,5 +489,16 @@
"Check how to config it for WebSocket": "Kiểm tra cách cấu hình nó cho WebSocket",
"Go back to the previous page.": "Quay trở lại trang trước.",
"wayToGetLineNotifyToken": "Bạn có thể lấy access token từ {0}",
"Resend Notification if Down X times consecutively": "Gửi lại thông báo nếu Down X lần liên tiếp"
"Resend Notification if Down X times consecutively": "Gửi lại thông báo nếu Down X lần liên tiếp",
"Cannot connect to the socket server": "Không thể kết nối đến máy chủ socket",
"Home": "Trang chủ",
"Reconnecting...": "Đang kết nối lại...",
"Trust Proxy": "Proxy tin cậy",
"signedInDispDisabled": "Xác thực bị vô hiệu hóa.",
"RadiusSecretDescription": "Bí mật được chia sẻ giữa máy khách và máy chủ",
"Show Powered By": "Hiển thị được cung cấp bởi",
"Domain Names": "Tên tên miền",
"signedInDisp": "Đăng ký với tư cách là {0}",
"resendDisabled": "Vô hiệu hoá gửi phản hồi liên tục",
"resendEveryXTimes": "Gửi phản hồi mỗi {0} lần"
}

@ -98,5 +98,7 @@
"Heartbeat Interval": "檢查間距",
"Add New Monitor": "新增監測器",
"Quick Stats": "綜合數據",
"markdownSupported": "可以用 Markdown"
"markdownSupported": "可以用 Markdown",
"wayToGetFlashDutyKey": "您可以进入 协作空间 -> (选择一个 协作空间) -> 集成数据 -> 新增一个集成 页面,添加“自定义事件”获得一个推送地址,复制地址中的 Integration Key更多信息前往{0}",
"FlashDuty Severity":"严重程度"
}

@ -223,7 +223,7 @@
"webhookJsonDesc": "{0} 适合现代的 HTTP 服务器,例如 Express.js",
"webhookFormDataDesc": "{multipart} 适合 PHP其中 JSON 需要使用 {decodeFunction} 解码",
"webhookAdditionalHeadersTitle": "额外 Header",
"webhookAdditionalHeadersDesc": "设置通过此 Webhook 发送的额外 Header。",
"webhookAdditionalHeadersDesc": "设置 webhook 请求的额外 Header。每一个 Header 应被定义为一对 JSON 键值对。",
"smtp": "电子邮件SMTP",
"secureOptionNone": "无 / STARTTLS常用端口 25、587",
"secureOptionTLS": "TLS常用端口 465",
@ -413,6 +413,7 @@
"smtpDkimheaderFieldNames": "包含在哈希计算对象内的 Header 列表(可选)",
"smtpDkimskipFields": "不包含在哈希计算对象内的 Header 列表(可选)",
"wayToGetPagerDutyKey": "您可以在 Service -> Service Directory -> (选择一个 Service) -> Integrations -> Add integration 页面中搜索“Events API V2”以获取此 Integration Key更多信息请看{0}",
"wayToGetFlashDutyKey": "您可以进入 协作空间 -> (选择一个 协作空间) -> 集成数据 -> 新增一个集成 页面,添加“自定义事件”获得一个推送地址,复制地址中的 Integration Key更多信息前往{0}",
"Integration Key": "集成密钥",
"Integration URL": "集成网址",
"Auto resolve or acknowledged": "自动标记为已解决或已读",
@ -653,7 +654,7 @@
"Server Timezone": "服务器时区",
"statusPageMaintenanceEndDate": "结束时间",
"IconUrl": "图标 URL",
"Enable DNS Cache": "启用 DNS 缓存",
"Enable DNS Cache": "为 HTTP(s) 监控项启用 DNS 缓存",
"Enable": "启用",
"Disable": "禁用",
"dnsCacheDescription": "可能无法在某些 IPv6 环境工作,如果遇到问题请禁用。",
@ -741,7 +742,7 @@
"lunaseaUserID": "用户 ID",
"statusPageRefreshIn": "将于 {0} 后刷新",
"twilioAccountSID": "账户 SID",
"twilioAuthToken": "验证 Token",
"twilioAuthToken": "鉴权 Token / API Key Secret",
"twilioFromNumber": "发信号码",
"twilioToNumber": "收信号码",
"sameAsServerTimezone": "使用服务器时区",
@ -758,13 +759,13 @@
"Badge Suffix": "徽章内容后缀",
"Badge Prefix": "徽章内容前缀",
"Badge Label": "徽章标签",
"Badge Duration": "徽章显示时段",
"Badge Duration": "徽章显示时段(最后 x 小时)",
"Badge Type": "徽章类型",
"Badge Generator": "{0} 徽章生成器",
"Open Badge Generator": "打开徽章生成器",
"Badge Style": "徽章样式",
"Badge Down Days": "徽章证书到期故障天数",
"Badge Warn Days": "徽章证书到期警告天数",
"Badge Down Days": "故障状态所需剩余天数",
"Badge Warn Days": "警告状态所需剩余天数",
"Badge Warn Color": "警告状态下徽章颜色",
"Badge Maintenance Color": "维护状态下徽章颜色",
"Badge Down Color": "故障状态下徽章颜色",
@ -784,5 +785,55 @@
"Edit Maintenance": "编辑维护计划",
"Home": "首页",
"noGroupMonitorMsg": "暂无可用,请先创建一个监控项组。",
"Close": "关闭"
"Close": "关闭",
"nostrRecipients": "接收者公钥npub 格式)",
"nostrSender": "发送者私钥nsec 格式)",
"nostrRecipientsHelp": "npub 格式,每行一个",
"nostrRelaysHelp": "Relay 服务地址,每行一个",
"nostrRelays": "Nostr relay 服务",
"chromeExecutableDescription": "至 Docker 用户,如果 Chromium 尚未安装,则需要几分钟时间来安装并显示测试结果。这需要 1 GB 硬盘空间。",
"chromeExecutableAutoDetect": "自动检测",
"chromeExecutable": "Chrome/Chromium 可执行文件",
"Invert Keyword": "反转模式",
"invertKeywordDescription": "出现关键词将令检测结果为失败,而非成功。",
"webhookBodyCustomOption": "自定义内容",
"webhookCustomBodyDesc": "为 webhook 设定一个自定义 HTTP 请求体。可在模板内使用 {msg}、{heartbeat}和{monitor} 变量。",
"webhookBodyPresetOption": "预设 - {0}",
"Request Body": "请求体",
"jsonQueryDescription": "对响应结果执行一次 JSON 查询,其返回值将会被转换为字符串,再与期望值进行比较。可访问 <a href='https://jsonata.org/'>jsonata.org</a> 阅读 JSON 查询语言的文档,或在<a href='https://try.jsonata.org/'>此处</a>测试查询语句。",
"Json Query": "JSON 查询",
"twilioApiKey": "API Key可选",
"Expected Value": "预期值",
"Badge Duration (in hours)": "徽章时间范围(以小时为单位)",
"Badge Preview": "徽章预览",
"Notify Channel": "通知该频道",
"aboutNotifyChannel": "勾选“通知该频道”,会令该频道内所有成员都收到一条桌面端或移动端通知,无论其状态是在线或离开。",
"filterActive": "启用",
"filterActivePaused": "暂停",
"Kafka Topic Name": "Kafka 主题名称",
"Press Enter to add broker": "按回车键添加缓存代理broker",
"Enter the list of brokers": "输入缓存代理broker列表",
"Kafka Brokers": "Kafka 缓存代理Broker",
"Pick a SASL Mechanism...": "选择一种 SASL 鉴权方式……",
"Mechanism": "鉴权方式",
"Kafka SASL Options": "Kafka SASL 选项",
"AccessKey Id": "密钥 IDAccessKey Id",
"Authorization Identity": "授权实体Authorization Identity",
"Session Token": "会话令牌Session Token",
"Secret AccessKey": "访问密钥Secret AccessKey",
"Enable Kafka Producer Auto Topic Creation": "启用 Kafka 生成者Producer自动创建主题Topic功能",
"Enable Kafka SSL": "启用 Kafka SSL 功能",
"Kafka Producer Message": "Kafka 生成者Producer消息",
"tailscalePingWarning": "如需使用 Tailscale Ping 客户端,您需要以非 docker 方式安装 Uptime Kuma并同时安装 Tailscale 客户端。",
"Server URL should not contain the nfty topic": "服务器地址不应包含 ntfy主题",
"Select": "选择",
"selectedMonitorCount": "已选:{0}",
"noOrBadCertificate": "无证书或证书错误",
"showCertificateExpiry": "显示证书有效期",
"FlashDuty Severity": "严重程度",
"Check/Uncheck": "选中/取消选中",
"pushDeerServerDescription": "留空则使用官方服务器",
"PushDeer Server": "PushDeer 服务器",
"timeoutAfter": "{0} 秒后超时",
"Request Timeout": "请求超时"
}

@ -720,5 +720,15 @@
"sameAsServerTimezone": "使用伺服器時區",
"WeCom Bot Key": "WeCom 機器人 Key",
"pagertreeMedium": "中",
"pagertreeHigh": "高"
"pagertreeHigh": "高",
"Cannot connect to the socket server": "無法連線到 Socket 伺服器",
"Reconnecting...": "重新連線...",
"chromeExecutable": "Chrome/Chromium 執行檔",
"chromeExecutableAutoDetect": "自動偵測",
"chromeExecutableDescription": "如果您使用 Docker 且未安裝 Chromium可能要花數分鐘安裝後才能顯示測試結果。安裝會使用 1GB 的硬碟空間。",
"Edit Maintenance": "編輯維護",
"Invert Keyword": "以上關鍵字不能出現",
"Home": "首頁",
"Expected Value": "預期值",
"Json Query": "JSON 查询"
}

@ -675,22 +675,22 @@
"Custom": "自訂",
"sameAsServerTimezone": "使用服務器時區",
"cronExpression": "Cron 表達式",
"telegramSendSilently": "靜默發送到 Telegram",
"telegramSendSilentlyDescription": "靜默地發送消息。消息發布後用戶會收到無聲通知。",
"telegramSendSilently": "悄悄的發送",
"telegramSendSilentlyDescription": "悄悄的發送訊息。使用者會收到通知,但是是無聲的。",
"pagertreeDoNothing": "什麼都不做",
"Add New Tag": "添加新標籤",
"telegramMessageThreadIDDescription": "(可選) Telegram 話題描述",
"telegramMessageThreadID": "(可選)話題 ID",
"telegramMessageThreadID": "(選填) Telegram 話題 ID",
"startDateTime": "開始日期/時間",
"endDateTime": "結束日期/時間",
"cronSchedule": "計劃: ",
"invalidCronExpression": "無效的 Cron 表達式:{0}",
"telegramProtectContent": "阻止轉發/保存",
"telegramProtectContentDescription": "如果啟用Telegram 中的機器人息將受到保護,不會被轉發和保存。",
"telegramProtectContentDescription": "如果啟用Telegram 中的機器人息將受到保護,不會被轉發和保存。",
"installing": "安裝中",
"uninstall": "卸載",
"loadingError": "無法獲取數據, 請重試",
"markdownSupported": "支持Markdown語法",
"markdownSupported": "支援 Markdown 語法",
"Packet Size": "數據包大小",
"statusPageRefreshIn": "將於 {0} 後刷新",
"confirmUninstallPlugin": "是否要卸載這個插件?",
@ -742,5 +742,11 @@
"Expires": "過期",
"disableAPIKeyMsg": "您確定要停用這個 API 金鑰?",
"Monitor Setting": "{0} 的監視器設定",
"Guild ID": "Guild ID"
"Guild ID": "Guild ID",
"chromeExecutableDescription": "如果您使用 Docker 且未安裝 Chromium可能要花數分鐘安裝後才能顯示測試結果。安裝會使用 1GB 的硬碟空間。",
"promosmsAllowLongSMS": "允許長 SMS 訊息",
"Home": "首頁",
"chromeExecutable": "Chrome/Chromium 執行檔",
"chromeExecutableAutoDetect": "自動偵測",
"pagertreeCritical": "緊急"
}

@ -57,7 +57,8 @@ export default {
message: "",
errorMessage: "",
currentPassword: "",
}
},
faviconUpdateDebounce: null,
};
},
@ -186,16 +187,18 @@ export default {
// Also toast
if (data.important) {
if (data.status === 0) {
toast.error(`[${this.monitorList[data.monitorID].name}] [DOWN] ${data.msg}`, {
timeout: false,
});
} else if (data.status === 1) {
toast.success(`[${this.monitorList[data.monitorID].name}] [Up] ${data.msg}`, {
timeout: 20000,
});
} else {
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
if (this.monitorList[data.monitorID] !== undefined) {
if (data.status === 0) {
toast.error(`[${this.monitorList[data.monitorID].name}] [DOWN] ${data.msg}`, {
timeout: false,
});
} else if (data.status === 1) {
toast.success(`[${this.monitorList[data.monitorID].name}] [Up] ${data.msg}`, {
timeout: 20000,
});
} else {
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
}
}
if (! (data.monitorID in this.importantHeartbeatList)) {
@ -765,7 +768,12 @@ export default {
// Update Badge
"stats.down"(to, from) {
if (to !== from) {
favicon.badge(to);
if (this.faviconUpdateDebounce != null) {
clearTimeout(this.faviconUpdateDebounce);
}
this.faviconUpdateDebounce = setTimeout(() => {
favicon.badge(to);
}, 1000);
}
},

@ -5,6 +5,7 @@ export default {
system: (window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light",
userTheme: localStorage.theme,
userHeartbeatBar: localStorage.heartbeatBarTheme,
styleElapsedTime: localStorage.styleElapsedTime,
statusPageTheme: "light",
forceStatusPageTheme: false,
path: "",
@ -22,6 +23,11 @@ export default {
this.userHeartbeatBar = "normal";
}
// Default Elapsed Time Style
if (!this.styleElapsedTime) {
this.styleElapsedTime = "no-line";
}
document.body.classList.add(this.theme);
this.updateThemeColorMeta();
},
@ -68,6 +74,10 @@ export default {
localStorage.theme = to;
},
styleElapsedTime(to, from) {
localStorage.styleElapsedTime = to;
},
theme(to, from) {
document.body.classList.remove(from);
document.body.classList.add(this.theme);

@ -8,9 +8,9 @@
<MonitorList :scrollbar="true" />
</div>
<div class="col-12 col-md-7 col-xl-8 mb-3">
<div ref="container" class="col-12 col-md-7 col-xl-8 mb-3">
<!-- Add :key to disable vue router re-use the same component -->
<router-view :key="$route.fullPath" />
<router-view :key="$route.fullPath" :calculatedHeight="height" />
</div>
</div>
</div>
@ -25,7 +25,12 @@ export default {
MonitorList,
},
data() {
return {};
return {
height: 0
};
},
mounted() {
this.height = this.$refs.container.offsetHeight;
},
};
</script>

@ -1,5 +1,5 @@
<template>
<transition name="slide-fade" appear>
<transition ref="tableContainer" name="slide-fade" appear>
<div v-if="$route.name === 'DashboardHome'">
<h1 class="mb-3">
{{ $t("Quick Stats") }}
@ -81,10 +81,17 @@ export default {
Status,
Pagination,
},
props: {
calculatedHeight: {
type: Number,
default: 0
}
},
data() {
return {
page: 1,
perPage: 25,
initialPerPage: 25,
heartBeatList: [],
paginationConfig: {
hideCount: true,
@ -134,6 +141,37 @@ export default {
return this.heartBeatList.slice(startIndex, endIndex);
},
},
watch: {
importantHeartBeatList() {
this.$nextTick(() => {
this.updatePerPage();
});
},
},
mounted() {
this.initialPerPage = this.perPage;
window.addEventListener("resize", this.updatePerPage);
this.updatePerPage();
},
beforeUnmount() {
window.removeEventListener("resize", this.updatePerPage);
},
methods: {
updatePerPage() {
const tableContainer = this.$refs.tableContainer;
const tableContainerHeight = tableContainer.offsetHeight;
const availableHeight = window.innerHeight - tableContainerHeight;
const additionalPerPage = Math.floor(availableHeight / 58);
if (additionalPerPage > 0) {
this.perPage = Math.max(this.initialPerPage, this.perPage + additionalPerPage);
} else {
this.perPage = this.initialPerPage;
}
},
},
};
</script>

@ -102,11 +102,13 @@
<!-- Parent Monitor -->
<div class="my-3">
<label for="parent" class="form-label">{{ $t("Monitor Group") }}</label>
<select v-model="monitor.parent" class="form-select" :disabled="sortedMonitorList.length === 0">
<option v-if="sortedMonitorList.length === 0" :value="null" selected>{{ $t("noGroupMonitorMsg") }}</option>
<option v-else :value="null" selected>{{ $t("None") }}</option>
<option v-for="parentMonitor in sortedMonitorList" :key="parentMonitor.id" :value="parentMonitor.id">{{ parentMonitor.pathName }}</option>
</select>
<ActionSelect
v-model="monitor.parent"
:options="parentMonitorOptionsList"
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
:icon="'plus'"
:action="() => $refs.createGroupDialog.show()"
/>
</div>
<!-- URL -->
@ -373,49 +375,25 @@
</div>
</template>
<!-- SQL Server / PostgreSQL / MySQL -->
<template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres' || monitor.type === 'mysql'">
<div class="my-3">
<label for="sqlConnectionString" class="form-label">{{ $t("Connection String") }}</label>
<template v-if="monitor.type === 'sqlserver'">
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
</template>
<template v-if="monitor.type === 'postgres'">
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
</template>
<template v-if="monitor.type === 'mysql'">
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
</template>
</div>
<div class="my-3">
<label for="sqlQuery" class="form-label">{{ $t("Query") }}</label>
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" placeholder="Example: select getdate()"></textarea>
</div>
</template>
<!-- Redis -->
<template v-if="monitor.type === 'redis'">
<!-- SQL Server / PostgreSQL / MySQL / Redis / MongoDB -->
<template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres' || monitor.type === 'mysql' || monitor.type === 'redis' || monitor.type === 'mongodb'">
<div class="my-3">
<label for="redisConnectionString" class="form-label">{{ $t("Connection String") }}</label>
<input id="redisConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
<label for="connectionString" class="form-label">{{ $t("Connection String") }}</label>
<input id="connectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" required>
</div>
</template>
<!-- MongoDB -->
<template v-if="monitor.type === 'mongodb'">
<!-- SQL Server / PostgreSQL / MySQL -->
<template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres' || monitor.type === 'mysql'">
<div class="my-3">
<label for="sqlConnectionString" class="form-label">{{ $t("Connection String") }}</label>
<template v-if="monitor.type === 'mongodb'">
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
</template>
<label for="sqlQuery" class="form-label">{{ $t("Query") }}</label>
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" :placeholder="$t('Example:', [ 'select getdate()' ])" required></textarea>
</div>
</template>
<!-- Interval -->
<div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required :min="minInterval" step="1" :max="maxInterval">
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required :min="minInterval" step="1" :max="maxInterval" @blur="finishUpdateInterval">
</div>
<div class="my-3">
@ -434,6 +412,12 @@
<input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required :min="minInterval" step="1">
</div>
<!-- Timeout: HTTP / Keyword only -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword'" class="my-3">
<label for="timeout" class="form-label">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label>
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
</div>
<div class="my-3">
<label for="resend-interval" class="form-label">
{{ $t("Resend Notification if Down X times consecutively") }}
@ -471,6 +455,16 @@
</div>
</div>
<div v-if="monitor.type === 'gamedig'" class="my-3 form-check">
<input id="gamedig-guess-port" v-model="monitor.gamedigGivenPortOnly" :true-value="false" :false-value="true" class="form-check-input" type="checkbox">
<label class="form-check-label" for="gamedig-guess-port">
{{ $t("gamedigGuessPort") }}
</label>
<div class="form-text">
{{ $t("gamedigGuessPortDescription") }}
</div>
</div>
<!-- Ping packet size -->
<div v-if="monitor.type === 'ping'" class="my-3">
<label for="packet-size" class="form-label">{{ $t("Packet Size") }}</label>
@ -689,6 +683,9 @@
<option value="basic">
{{ $t("HTTP Basic Auth") }}
</option>
<option value="oauth2-cc">
{{ $t("OAuth2: Client Credentials") }}
</option>
<option value="ntlm">
NTLM
</option>
@ -712,6 +709,37 @@
<textarea id="tls-ca" v-model="monitor.tlsCa" class="form-control" :placeholder="$t('Server CA')"></textarea>
</div>
</template>
<template v-else-if="monitor.authMethod === 'oauth2-cc' ">
<div class="my-3">
<label for="oauth_auth_method" class="form-label">{{ $t("Authentication Method") }}</label>
<select id="oauth_auth_method" v-model="monitor.oauth_auth_method" class="form-select">
<option value="client_secret_basic">
{{ $t("Authorization Header") }}
</option>
<option value="client_secret_post">
{{ $t("Form Data Body") }}
</option>
</select>
</div>
<div class="my-3">
<label for="oauth_token_url" class="form-label">{{ $t("OAuth Token URL") }}</label>
<input id="oauth_token_url" v-model="monitor.oauth_token_url" type="text" class="form-control" :placeholder="$t('OAuth Token URL')" required>
</div>
<div class="my-3">
<label for="oauth_client_id" class="form-label">{{ $t("Client ID") }}</label>
<input id="oauth_client_id" v-model="monitor.oauth_client_id" type="text" class="form-control" :placeholder="$t('Client ID')" required>
</div>
<template v-if="monitor.oauth_auth_method === 'client_secret_post' || monitor.oauth_auth_method === 'client_secret_basic'">
<div class="my-3">
<label for="oauth_client_secret" class="form-label">{{ $t("Client Secret") }}</label>
<input id="oauth_client_secret" v-model="monitor.oauth_client_secret" type="password" class="form-control" :placeholder="$t('Client Secret')" required>
</div>
<div class="my-3">
<label for="oauth_scopes" class="form-label">{{ $t("OAuth Scope") }}</label>
<input id="oauth_scopes" v-model="monitor.oauth_scopes" type="text" class="form-control" :placeholder="$t('Optional: Space separated list of scopes')">
</div>
</template>
</template>
<template v-else>
<div class="my-3">
<label for="basicauth-user" class="form-label">{{ $t("Username") }}</label>
@ -797,6 +825,7 @@
<NotificationDialog ref="notificationDialog" @added="addedNotification" />
<DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
<ProxyDialog ref="proxyDialog" @added="addedProxy" />
<CreateGroupDialog ref="createGroupDialog" @added="addedDraftGroup" />
</div>
</transition>
</template>
@ -804,20 +833,62 @@
<script>
import VueMultiselect from "vue-multiselect";
import { useToast } from "vue-toastification";
import ActionSelect from "../components/ActionSelect.vue";
import CopyableInput from "../components/CopyableInput.vue";
import CreateGroupDialog from "../components/CreateGroupDialog.vue";
import NotificationDialog from "../components/NotificationDialog.vue";
import DockerHostDialog from "../components/DockerHostDialog.vue";
import ProxyDialog from "../components/ProxyDialog.vue";
import TagsManager from "../components/TagsManager.vue";
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
import { hostNameRegexPattern } from "../util-frontend";
import { sleep } from "../util";
const toast = useToast();
const monitorDefaults = {
type: "http",
name: "",
parent: null,
url: "https://",
method: "GET",
interval: 60,
retryInterval: 60,
resendInterval: 0,
maxretries: 1,
timeout: 48,
notificationIDList: {},
ignoreTls: false,
upsideDown: false,
packetSize: 56,
expiryNotification: false,
maxredirects: 10,
accepted_statuscodes: [ "200-299" ],
dns_resolve_type: "A",
dns_resolve_server: "1.1.1.1",
docker_container: "",
docker_host: null,
proxyId: null,
mqttUsername: "",
mqttPassword: "",
mqttTopic: "",
mqttSuccessMessage: "",
authMethod: null,
oauth_auth_method: "client_secret_basic",
httpBodyEncoding: "json",
kafkaProducerBrokers: [],
kafkaProducerSaslOptions: {
mechanism: "None",
},
gamedigGivenPortOnly: true,
};
export default {
components: {
ActionSelect,
ProxyDialog,
CopyableInput,
CreateGroupDialog,
NotificationDialog,
DockerHostDialog,
TagsManager,
@ -845,7 +916,8 @@ export default {
"mysql": "mysql://username:password@host:port/database",
"redis": "redis://user:password@host:port",
"mongodb": "mongodb://username:password@host:port/database",
}
},
draftGroupName: null,
};
},
@ -956,7 +1028,7 @@ message HealthCheckResponse {
// Filter result by active state, weight and alphabetical
// Only return groups which arent't itself and one of its decendants
sortedMonitorList() {
sortedGroupMonitorList() {
let result = Object.values(this.$root.monitorList);
// Only groups, not itself, not a decendant
@ -995,6 +1067,45 @@ message HealthCheckResponse {
return result;
},
/**
* Generates the parent monitor options list based on the sorted group monitor list and draft group name.
*
* @return {Array} The parent monitor options list.
*/
parentMonitorOptionsList() {
let list = [];
if (this.sortedGroupMonitorList.length === 0 && this.draftGroupName == null) {
list = [
{
label: this.$t("noGroupMonitorMsg"),
value: null
}
];
} else {
list = [
{
label: this.$t("None"),
value: null
},
... this.sortedGroupMonitorList.map(monitor => {
return {
label: monitor.pathName,
value: monitor.id,
};
}),
];
}
if (this.draftGroupName != null) {
list = [{
label: this.draftGroupName,
value: -1,
}].concat(list);
}
return list;
},
},
watch: {
"$root.proxyList"() {
@ -1020,6 +1131,13 @@ message HealthCheckResponse {
}
},
"monitor.timeout"(value, oldValue) {
// keep timeout within 80% range
if (value && value !== oldValue) {
this.monitor.timeout = this.clampTimeout(value);
}
},
"monitor.type"() {
if (this.monitor.type === "push") {
if (! this.monitor.pushToken) {
@ -1121,37 +1239,7 @@ message HealthCheckResponse {
if (this.isAdd) {
this.monitor = {
type: "http",
name: "",
parent: null,
url: "https://",
method: "GET",
interval: 60,
retryInterval: this.interval,
resendInterval: 0,
maxretries: 1,
notificationIDList: {},
ignoreTls: false,
upsideDown: false,
packetSize: 56,
expiryNotification: false,
maxredirects: 10,
accepted_statuscodes: [ "200-299" ],
dns_resolve_type: "A",
dns_resolve_server: "1.1.1.1",
docker_container: "",
docker_host: null,
proxyId: null,
mqttUsername: "",
mqttPassword: "",
mqttTopic: "",
mqttSuccessMessage: "",
authMethod: null,
httpBodyEncoding: "json",
kafkaProducerBrokers: [],
kafkaProducerSaslOptions: {
mechanism: "None",
},
...monitorDefaults
};
if (this.$root.proxyList && !this.monitor.proxyId) {
@ -1211,12 +1299,18 @@ message HealthCheckResponse {
if (this.monitor.retryInterval === 0) {
this.monitor.retryInterval = this.monitor.interval;
}
// Handling for monitors that are missing/zeroed timeout
if (!this.monitor.timeout) {
this.monitor.timeout = ~~(this.monitor.interval * 8) / 10;
}
} else {
toast.error(res.msg);
}
});
}
this.draftGroupName = null;
},
addKafkaProducerBroker(newBroker) {
@ -1265,7 +1359,8 @@ message HealthCheckResponse {
this.monitor.body = JSON.stringify(JSON.parse(this.monitor.body), null, 4);
}
if (this.monitor.type && this.monitor.type !== "http" && (this.monitor.type !== "keyword" || this.monitor.type !== "json-query")) {
const monitorTypesWithEncodingAllowed = [ "http", "keyword", "json-query" ];
if (this.monitor.type && !monitorTypesWithEncodingAllowed.includes(this.monitor.type)) {
this.monitor.httpBodyEncoding = null;
}
@ -1281,16 +1376,46 @@ message HealthCheckResponse {
this.monitor.url = this.monitor.url.trim();
}
let createdNewParent = false;
if (this.draftGroupName && this.monitor.parent === -1) {
// Create Monitor with name of draft group
const res = await new Promise((resolve) => {
this.$root.add({
...monitorDefaults,
type: "group",
name: this.draftGroupName,
interval: this.monitor.interval,
active: false,
}, resolve);
});
if (res.ok) {
createdNewParent = true;
this.monitor.parent = res.monitorID;
} else {
toast.error(res.msg);
this.processing = false;
return;
}
}
if (this.isAdd || this.isClone) {
this.$root.add(this.monitor, async (res) => {
if (res.ok) {
await this.$refs.tagsManager.submit(res.monitorID);
// Start the new parent monitor after edit is done
if (createdNewParent) {
this.startParentGroupMonitor();
}
toast.success(res.msg);
this.processing = false;
this.$root.getMonitorList();
this.$router.push("/dashboard/" + res.monitorID);
} else {
toast.error(res.msg);
this.processing = false;
@ -1304,10 +1429,20 @@ message HealthCheckResponse {
this.processing = false;
this.$root.toastRes(res);
this.init();
// Start the new parent monitor after edit is done
if (createdNewParent) {
this.startParentGroupMonitor();
}
});
}
},
async startParentGroupMonitor() {
await sleep(2000);
await this.$root.getSocket().emit("resumeMonitor", this.monitor.parent, () => {});
},
/**
* Added a Notification Event
* Enable it if the notification is added in EditMonitor.vue
@ -1331,6 +1466,35 @@ message HealthCheckResponse {
addedDockerHost(id) {
this.monitor.docker_host = id;
},
/**
* Adds a draft group.
*
* @param {string} draftGroupName - The name of the draft group.
*/
addedDraftGroup(draftGroupName) {
this.draftGroupName = draftGroupName;
this.monitor.parent = -1;
},
// Clamp timeout
clampTimeout(timeout) {
// limit to 80% of interval, narrowly avoiding epsilon bug
const maxTimeout = ~~(this.monitor.interval * 8 ) / 10;
const clamped = Math.max(0, Math.min(timeout, maxTimeout));
// 0 will be treated as 80% of interval
return Number.isFinite(clamped) ? clamped : maxTimeout;
},
finishUpdateInterval() {
// 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;
}
},
},
};
</script>

@ -54,6 +54,12 @@
<label class="form-check-label" for="show-powered-by">{{ $t("Show Powered By") }}</label>
</div>
<!-- Show certificate expiry -->
<div class="my-3 form-check form-switch">
<input id="show-certificate-expiry" v-model="config.showCertificateExpiry" class="form-check-input" type="checkbox">
<label class="form-check-label" for="show-certificate-expiry">{{ $t("showCertificateExpiry") }}</label>
</div>
<div v-if="false" class="my-3">
<label for="password" class="form-label">{{ $t("Password") }} <sup>{{ $t("Coming Soon") }}</sup></label>
<input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control">
@ -309,7 +315,7 @@
👀 {{ $t("statusPageNothing") }}
</div>
<PublicGroupList :edit-mode="enableEditMode" :show-tags="config.showTags" />
<PublicGroupList :edit-mode="enableEditMode" :show-tags="config.showTags" :show-certificate-expiry="config.showCertificateExpiry" />
</div>
<footer class="mt-5 mb-4">

@ -1,5 +1,5 @@
const { genSecret, DOWN, log} = require("../src/util");
const utilServerRewire = require("../server/util-server");
const utilServer = require("../server/util-server");
const Discord = require("../server/notification-providers/discord");
const axios = require("axios");
const { UptimeKumaServer } = require("../server/uptime-kuma-server");
@ -14,13 +14,13 @@ jest.mock("axios");
describe("Test parseCertificateInfo", () => {
it("should handle undefined", async () => {
const parseCertificateInfo = utilServerRewire.__get__("parseCertificateInfo");
const parseCertificateInfo = utilServer.__getPrivateFunction("parseCertificateInfo");
const info = parseCertificateInfo(undefined);
expect(info).toEqual(undefined);
}, 5000);
it("should handle normal cert chain", async () => {
const parseCertificateInfo = utilServerRewire.__get__("parseCertificateInfo");
const parseCertificateInfo = utilServer.__getPrivateFunction("parseCertificateInfo");
const chain1 = {
fingerprint: "CF:2C:F3:6A:FE:6B:10:EC:44:77:C8:95:BB:96:2E:06:1F:0E:15:DA",
@ -52,7 +52,7 @@ describe("Test parseCertificateInfo", () => {
}, 5000);
it("should handle cert chain with strange circle", async () => {
const parseCertificateInfo = utilServerRewire.__get__("parseCertificateInfo");
const parseCertificateInfo = utilServer.__getPrivateFunction("parseCertificateInfo");
const chain1 = {
fingerprint: "CF:2C:F3:6A:FE:6B:10:EC:44:77:C8:95:BB:96:2E:06:1F:0E:15:DA",
@ -92,7 +92,7 @@ describe("Test parseCertificateInfo", () => {
}, 5000);
it("should handle cert chain with last undefined (should be happen in real, but just in case)", async () => {
const parseCertificateInfo = utilServerRewire.__get__("parseCertificateInfo");
const parseCertificateInfo = utilServer.__getPrivateFunction("parseCertificateInfo");
const chain1 = {
fingerprint: "CF:2C:F3:6A:FE:6B:10:EC:44:77:C8:95:BB:96:2E:06:1F:0E:15:DA",
@ -213,22 +213,22 @@ describe("Test Discord Notification Provider", () => {
describe("The function filterAndJoin", () => {
it("should join and array of strings to one string", () => {
const result = utilServerRewire.filterAndJoin([ "one", "two", "three" ]);
const result = utilServer.filterAndJoin([ "one", "two", "three" ]);
expect(result).toBe("onetwothree");
});
it("should join strings using a given connector", () => {
const result = utilServerRewire.filterAndJoin([ "one", "two", "three" ], "-");
const result = utilServer.filterAndJoin([ "one", "two", "three" ], "-");
expect(result).toBe("one-two-three");
});
it("should filter null, undefined and empty strings before joining", () => {
const result = utilServerRewire.filterAndJoin([ undefined, "", "three" ], "--");
const result = utilServer.filterAndJoin([ undefined, "", "three" ], "--");
expect(result).toBe("three");
});
it("should return an empty string if all parts are filtered out", () => {
const result = utilServerRewire.filterAndJoin([ undefined, "", "" ], "--");
const result = utilServer.filterAndJoin([ undefined, "", "" ], "--");
expect(result).toBe("");
});
});

@ -0,0 +1,13 @@
# Container running a test radius server
# More instructions in https://github.com/louislam/uptime-kuma/pull/1635
FROM freeradius/freeradius-server:latest
RUN mkdir -p /etc/raddb/mods-config/files/
RUN echo "client net {" > /etc/raddb/clients.conf
RUN echo " ipaddr = 172.17.0.0/16" >> /etc/raddb/clients.conf
RUN echo " secret = testing123" >> /etc/raddb/clients.conf
RUN echo "}" >> /etc/raddb/clients.conf
RUN echo "bob Cleartext-Password := \"testpw\"" > /etc/raddb/mods-config/files/authorize
Loading…
Cancel
Save