Merge branch 'louislam:master' into badge-generator

pull/2915/head
Yoswaris Lawpaiboon 2 years ago committed by GitHub
commit 030faddd1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -26,6 +26,12 @@ body:
label: "📝 Describe your problem"
description: "Please walk us through it step by step."
placeholder: "Describe what are you asking for..."
- type: textarea
id: error-msg
validations:
required: false
attributes:
label: "📝 Error Message(s) or Log"
- type: input
id: uptime-kuma-version
attributes:

@ -0,0 +1 @@
# This is a .gitignore style file for 'GrantBirki/json-yaml-validate' Action workflow

@ -0,0 +1,26 @@
name: json-yaml-validate
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
permissions:
contents: read
pull-requests: write # enable write permissions for pull request comments
jobs:
json-yaml-validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: json-yaml-validate
id: json-yaml-validate
uses: GrantBirki/json-yaml-validate@v1.3.0
with:
comment: "true" # enable comment mode
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions

@ -0,0 +1,11 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
DROP TABLE maintenance_timeslot;
-- 999 characters. https://stackoverflow.com/questions/46134830/maximum-length-for-cron-job
ALTER TABLE maintenance ADD cron TEXT;
ALTER TABLE maintenance ADD timezone VARCHAR(255);
ALTER TABLE maintenance ADD duration INTEGER;
COMMIT;

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

@ -43,10 +43,11 @@ const prompt = (query) => new Promise((resolve) => rl.question(query, resolve));
});
console.log(result.stdout + result.stderr);
/*
result = await ssh.execCommand("pm2 restart 1", {
cwd,
});
console.log(result.stdout + result.stderr);
console.log(result.stdout + result.stderr);*/
} catch (e) {
console.log(e);

@ -13,7 +13,7 @@ lines = lines.filter((line) => line !== "");
lines = [ ...new Set(lines) ];
// Remove @weblate and @UptimeKumaBot
lines = lines.filter((line) => line !== "@weblate" && line !== "@UptimeKumaBot");
lines = lines.filter((line) => line !== "@weblate" && line !== "@UptimeKumaBot" && line !== "@louislam");
// Sort the lines
lines = lines.sort();

2241
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.21.0-beta.1",
"version": "1.21.2",
"license": "MIT",
"repository": {
"type": "git",
@ -39,7 +39,7 @@
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.20.2 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.21.2 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@ -64,13 +64,13 @@
"cy:run:unit": "npx cypress run --browser chrome --headless --config-file ./config/cypress.frontend.config.js",
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
"depoly-demo-server": "node extra/deploy-demo-server.js",
"deploy-demo-server": "node extra/deploy-demo-server.js",
"sort-contributors": "node extra/sort-contributors.js"
},
"dependencies": {
"@grpc/grpc-js": "~1.7.3",
"@louislam/ping": "~0.4.2-mod.2",
"@louislam/sqlite3": "15.1.2",
"@louislam/ping": "~0.4.4-mod.0",
"@louislam/sqlite3": "15.1.6",
"args-parser": "~1.3.0",
"axios": "~0.27.0",
"axios-ntlm": "1.3.0",
@ -85,6 +85,7 @@
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
"compression": "~1.7.4",
"croner": "^6.0.3",
"dayjs": "~1.11.5",
"dotenv": "~16.0.3",
"express": "~4.17.3",
@ -142,10 +143,11 @@
"aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0",
"bootstrap": "5.1.3",
"chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0",
"chart.js": "~4.2.1",
"chartjs-adapter-dayjs-4": "~1.0.4",
"concurrently": "^7.1.0",
"core-js": "~3.26.1",
"cronstrue": "~2.24.0",
"cross-env": "~7.0.3",
"cypress": "^10.1.0",
"delay": "^5.0.0",
@ -172,8 +174,8 @@
"v-pagination-3": "~0.1.7",
"vite": "~3.1.0",
"vite-plugin-compression": "^0.5.1",
"vue": "next",
"vue-chart-3": "3.0.9",
"vue": "~3.2.47",
"vue-chartjs": "~5.2.0",
"vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "~3.0.4",
"vue-i18n": "~9.2.2",

@ -30,11 +30,6 @@ class Database {
*/
static patched = false;
/**
* For Backup only
*/
static backupPath = null;
/**
* Add patch filename in key
* Values:
@ -73,6 +68,8 @@ class Database {
"patch-http-body-encoding.sql": true,
"patch-add-description-monitor.sql": true,
"patch-api-key-table.sql": true,
"patch-monitor-tls.sql": true,
"patch-maintenance-cron.sql": true,
};
/**
@ -197,15 +194,7 @@ class Database {
} else {
log.info("db", "Database patch is needed");
try {
this.backup(version);
} catch (e) {
log.error("db", e);
log.error("db", "Unable to create a backup before patching the database. Please make sure you have enough space and permission.");
process.exit(1);
}
// Try catch anything here, if gone wrong, restore the backup
// Try catch anything here
try {
for (let i = version + 1; i <= this.latestVersion; i++) {
const sqlFile = `./db/patch${i}.sql`;
@ -221,7 +210,6 @@ class Database {
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
log.error("db", "Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
this.restore();
process.exit(1);
}
}
@ -263,8 +251,6 @@ class Database {
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
log.error("db", "Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
this.restore();
process.exit(1);
}
@ -366,8 +352,6 @@ class Database {
}
}
this.backup(dayjs().format("YYYYMMDDHHmmss"));
log.info("db", sqlFilename + " is patching");
this.patched = true;
await this.importSQLFile("./db/" + sqlFilename);
@ -433,6 +417,9 @@ class Database {
log.info("db", "Closing the database");
// Flush WAL to main database
await R.exec("PRAGMA wal_checkpoint(TRUNCATE)");
while (true) {
Database.noReject = true;
await R.close();
@ -449,100 +436,6 @@ class Database {
process.removeListener("unhandledRejection", listener);
}
/**
* One backup one time in this process.
* Reset this.backupPath if you want to backup again
* @param {string} version Version code of backup
*/
static backup(version) {
if (! this.backupPath) {
log.info("db", "Backing up the database");
this.backupPath = this.dataDir + "kuma.db.bak" + version;
fs.copyFileSync(Database.path, this.backupPath);
const shmPath = Database.path + "-shm";
if (fs.existsSync(shmPath)) {
this.backupShmPath = shmPath + ".bak" + version;
fs.copyFileSync(shmPath, this.backupShmPath);
}
const walPath = Database.path + "-wal";
if (fs.existsSync(walPath)) {
this.backupWalPath = walPath + ".bak" + version;
fs.copyFileSync(walPath, this.backupWalPath);
}
// Double confirm if all files actually backup
if (!fs.existsSync(this.backupPath)) {
throw new Error("Backup failed! " + this.backupPath);
}
if (fs.existsSync(shmPath)) {
if (!fs.existsSync(this.backupShmPath)) {
throw new Error("Backup failed! " + this.backupShmPath);
}
}
if (fs.existsSync(walPath)) {
if (!fs.existsSync(this.backupWalPath)) {
throw new Error("Backup failed! " + this.backupWalPath);
}
}
}
}
/** Restore from most recent backup */
static restore() {
if (this.backupPath) {
log.error("db", "Patching the database failed!!! Restoring the backup");
const shmPath = Database.path + "-shm";
const walPath = Database.path + "-wal";
// Make sure we have a backup to restore before deleting old db
if (
!fs.existsSync(this.backupPath)
&& !fs.existsSync(shmPath)
&& !fs.existsSync(walPath)
) {
log.error("db", "Backup file not found! Leaving database in failed state.");
process.exit(1);
}
// Delete patch failed db
try {
if (fs.existsSync(Database.path)) {
fs.unlinkSync(Database.path);
}
if (fs.existsSync(shmPath)) {
fs.unlinkSync(shmPath);
}
if (fs.existsSync(walPath)) {
fs.unlinkSync(walPath);
}
} catch (e) {
log.error("db", "Restore failed; you may need to restore the backup manually");
process.exit(1);
}
// Restore backup
fs.copyFileSync(this.backupPath, Database.path);
if (this.backupShmPath) {
fs.copyFileSync(this.backupShmPath, shmPath);
}
if (this.backupWalPath) {
fs.copyFileSync(this.backupWalPath, walPath);
}
} else {
log.info("db", "Nothing to restore");
}
}
/** Get the size of the database */
static getSize() {
log.debug("db", "Database.getSize()");

@ -1,8 +1,10 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
const { parseTimeObject, parseTimeFromTimeObject, log } = require("../../src/util");
const { R } = require("redbean-node");
const dayjs = require("dayjs");
const Cron = require("croner");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const apicache = require("../modules/apicache");
class Maintenance extends BeanModel {
@ -15,16 +17,19 @@ class Maintenance extends BeanModel {
let dateRange = [];
if (this.start_date) {
dateRange.push(utcToLocal(this.start_date));
if (this.end_date) {
dateRange.push(utcToLocal(this.end_date));
dateRange.push(this.start_date);
} else {
dateRange.push(null);
}
if (this.end_date) {
dateRange.push(this.end_date);
}
let timeRange = [];
let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
let startTime = parseTimeObject(this.start_time);
timeRange.push(startTime);
let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
let endTime = parseTimeObject(this.end_time);
timeRange.push(endTime);
let obj = {
@ -39,68 +44,54 @@ class Maintenance extends BeanModel {
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
timeslotList: [],
cron: this.cron,
duration: this.duration,
durationMinutes: parseInt(this.duration / 60),
timezone: await this.getTimezone(),
timezoneOffset: await this.getTimezoneOffset(),
status: await this.getStatus(),
};
const timeslotList = await this.getTimeslotList();
for (let timeslot of timeslotList) {
obj.timeslotList.push(await timeslot.toPublicJSON());
}
if (!Array.isArray(obj.weekdays)) {
obj.weekdays = [];
}
if (this.strategy === "manual") {
// Do nothing, no timeslots
} else if (this.strategy === "single") {
obj.timeslotList.push({
startDate: this.start_date,
endDate: this.end_date,
});
} else {
// Should be cron or recurring here
if (this.beanMeta.job) {
let runningTimeslot = this.getRunningTimeslot();
if (!Array.isArray(obj.daysOfMonth)) {
obj.daysOfMonth = [];
if (runningTimeslot) {
obj.timeslotList.push(runningTimeslot);
}
// Maintenance Status
if (!obj.active) {
obj.status = "inactive";
} else if (obj.strategy === "manual") {
obj.status = "under-maintenance";
} else if (obj.timeslotList.length > 0) {
let currentTimestamp = dayjs().unix();
let nextRunDate = this.beanMeta.job.nextRun();
if (nextRunDate) {
let startDateDayjs = dayjs(nextRunDate);
for (let timeslot of obj.timeslotList) {
if (dayjs.utc(timeslot.startDate).unix() <= currentTimestamp && dayjs.utc(timeslot.endDate).unix() >= currentTimestamp) {
log.debug("timeslot", "Timeslot ID: " + timeslot.id);
log.debug("timeslot", "currentTimestamp:" + currentTimestamp);
log.debug("timeslot", "timeslot.start_date:" + dayjs.utc(timeslot.startDate).unix());
log.debug("timeslot", "timeslot.end_date:" + dayjs.utc(timeslot.endDate).unix());
let startDate = startDateDayjs.toISOString();
let endDate = startDateDayjs.add(this.duration, "second").toISOString();
obj.status = "under-maintenance";
break;
obj.timeslotList.push({
startDate,
endDate,
});
}
}
if (!obj.status) {
obj.status = "scheduled";
}
} else if (obj.timeslotList.length === 0) {
obj.status = "ended";
} else {
obj.status = "unknown";
if (!Array.isArray(obj.weekdays)) {
obj.weekdays = [];
}
return obj;
if (!Array.isArray(obj.daysOfMonth)) {
obj.daysOfMonth = [];
}
/**
* Only get future or current timeslots only
* @returns {Promise<[]>}
*/
async getTimeslotList() {
return R.convertToBeans("maintenance_timeslot", await R.getAll(`
SELECT maintenance_timeslot.*
FROM maintenance_timeslot, maintenance
WHERE maintenance_timeslot.maintenance_id = maintenance.id
AND maintenance.id = ?
AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
`, [
this.id
]));
return obj;
}
/**
@ -126,7 +117,7 @@ class Maintenance extends BeanModel {
/**
* Get a list of days in month that maintenance is active for
* @returns {number[]} Array of active days in month
* @returns {number[]|string[]} Array of active days in month
*/
getDayOfMonthList() {
return JSON.parse(this.days_of_month).sort(function (a, b) {
@ -135,26 +126,10 @@ class Maintenance extends BeanModel {
}
/**
* Get the start date and time for maintenance
* @returns {dayjs.Dayjs} Start date and time
*/
getStartDateTime() {
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
// Start Time
let startTimeSecond = dayjs.utc(this.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
log.debug("timeslot", "startTime: " + startTimeSecond);
// Bake StartDate + StartTime = Start DateTime
return dayjs.utc(this.start_date).add(startTimeSecond, "second");
}
/**
* Get the duraction of maintenance in seconds
* Get the duration of maintenance in seconds
* @returns {number} Duration of maintenance
*/
getDuration() {
calcDuration() {
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
// Add 24hours if it is across day
if (duration < 0) {
@ -169,71 +144,270 @@ class Maintenance extends BeanModel {
* @param {Object} obj Data to fill bean with
* @returns {Bean} Filled bean
*/
static jsonToBean(bean, obj) {
static async jsonToBean(bean, obj) {
if (obj.id) {
bean.id = obj.id;
}
// Apply timezone offset to timeRange, as it cannot apply automatically.
if (obj.timeRange[0]) {
timeObjectToUTC(obj.timeRange[0]);
if (obj.timeRange[1]) {
timeObjectToUTC(obj.timeRange[1]);
}
}
bean.title = obj.title;
bean.description = obj.description;
bean.strategy = obj.strategy;
bean.interval_day = obj.intervalDay;
bean.timezone = obj.timezone;
bean.active = obj.active;
if (obj.dateRange[0]) {
bean.start_date = localToUTC(obj.dateRange[0]);
bean.start_date = obj.dateRange[0];
} else {
bean.start_date = null;
}
if (obj.dateRange[1]) {
bean.end_date = localToUTC(obj.dateRange[1]);
bean.end_date = obj.dateRange[1];
} else {
bean.end_date = null;
}
if (bean.strategy === "cron") {
bean.duration = obj.durationMinutes * 60;
bean.cron = obj.cron;
this.validateCron(bean.cron);
}
if (bean.strategy.startsWith("recurring-")) {
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
bean.weekdays = JSON.stringify(obj.weekdays);
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
await bean.generateCron();
this.validateCron(bean.cron);
}
return bean;
}
/**
* SQL conditions for active maintenance
* @returns {string}
* Throw error if cron is invalid
* @param cron
* @returns {Promise<void>}
*/
static getActiveMaintenanceSQLCondition() {
return `
(
(maintenance_timeslot.start_date <= DATETIME('now')
AND maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1)
)
`;
static async validateCron(cron) {
let job = new Cron(cron, () => {});
job.stop();
}
/**
* SQL conditions for active and future maintenance
* @returns {string}
* Run the cron
*/
static getActiveAndFutureMaintenanceSQLCondition() {
return `
(
((maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1))
)
`;
async run(throwError = false) {
if (this.beanMeta.job) {
log.debug("maintenance", "Maintenance is already running, stop it first. id: " + this.id);
this.stop();
}
log.debug("maintenance", "Run maintenance id: " + this.id);
// 1.21.2 migration
if (!this.cron) {
await this.generateCron();
if (!this.timezone) {
this.timezone = "UTC";
}
if (this.cron) {
await R.store(this);
}
}
if (this.strategy === "manual") {
// Do nothing, because it is controlled by the user
} else if (this.strategy === "single") {
this.beanMeta.job = new Cron(this.start_date, { timezone: await this.getTimezone() }, () => {
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
apicache.clear();
});
} else if (this.cron != null) {
// Here should be cron or recurring
try {
this.beanMeta.status = "scheduled";
let startEvent = (customDuration = 0) => {
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
this.beanMeta.status = "under-maintenance";
clearTimeout(this.beanMeta.durationTimeout);
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
let duration;
if (customDuration > 0) {
duration = customDuration;
} else if (this.end_date) {
let d = dayjs(this.end_date).diff(dayjs(), "second");
if (d < this.duration) {
duration = d * 1000;
}
} else {
duration = this.duration * 1000;
}
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
this.beanMeta.durationTimeout = setTimeout(() => {
// End of maintenance for this timeslot
this.beanMeta.status = "scheduled";
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
}, duration);
};
// Create Cron
this.beanMeta.job = new Cron(this.cron, {
timezone: await this.getTimezone(),
}, startEvent);
// Continue if the maintenance is still in the window
let runningTimeslot = this.getRunningTimeslot();
let current = dayjs();
if (runningTimeslot) {
let duration = dayjs(runningTimeslot.endDate).diff(current, "second") * 1000;
log.debug("maintenance", "Maintenance id: " + this.id + " Remaining duration: " + duration + "ms");
startEvent(duration);
}
} catch (e) {
log.error("maintenance", "Error in maintenance id: " + this.id);
log.error("maintenance", "Cron: " + this.cron);
log.error("maintenance", e);
if (throwError) {
throw e;
}
}
} else {
log.error("maintenance", "Maintenance id: " + this.id + " has no cron");
}
}
getRunningTimeslot() {
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").format("YYYY-MM-DD HH:mm:ss")));
let end = start.add(this.duration, "second");
let current = dayjs();
if (current.isAfter(start) && current.isBefore(end)) {
return {
startDate: start.toISOString(),
endDate: end.toISOString(),
};
} else {
return null;
}
}
stop() {
if (this.beanMeta.job) {
this.beanMeta.job.stop();
delete this.beanMeta.job;
}
}
async isUnderMaintenance() {
return (await this.getStatus()) === "under-maintenance";
}
async getTimezone() {
if (!this.timezone) {
return await UptimeKumaServer.getInstance().getTimezone();
}
return this.timezone;
}
async getTimezoneOffset() {
return dayjs.tz(dayjs(), await this.getTimezone()).format("Z");
}
async getStatus() {
if (!this.active) {
return "inactive";
}
if (this.strategy === "manual") {
return "under-maintenance";
}
// Check if the maintenance is started
if (this.start_date && dayjs().isBefore(dayjs.tz(this.start_date, await this.getTimezone()))) {
return "scheduled";
}
// Check if the maintenance is ended
if (this.end_date && dayjs().isAfter(dayjs.tz(this.end_date, await this.getTimezone()))) {
return "ended";
}
if (this.strategy === "single") {
return "under-maintenance";
}
if (!this.beanMeta.status) {
return "unknown";
}
return this.beanMeta.status;
}
/**
* Generate Cron for recurring maintenance
* @returns {Promise<void>}
*/
async generateCron() {
log.info("maintenance", "Generate cron for maintenance id: " + this.id);
if (this.strategy === "cron") {
// Do nothing for cron
} else if (!this.strategy.startsWith("recurring-")) {
this.cron = "";
} else if (this.strategy === "recurring-interval") {
let array = this.start_time.split(":");
let hour = parseInt(array[0]);
let minute = parseInt(array[1]);
this.cron = minute + " " + hour + " */" + this.interval_day + " * *";
this.duration = this.calcDuration();
log.debug("maintenance", "Cron: " + this.cron);
log.debug("maintenance", "Duration: " + this.duration);
} else if (this.strategy === "recurring-weekday") {
let list = this.getDayOfWeekList();
let array = this.start_time.split(":");
let hour = parseInt(array[0]);
let minute = parseInt(array[1]);
this.cron = minute + " " + hour + " * * " + list.join(",");
this.duration = this.calcDuration();
} else if (this.strategy === "recurring-day-of-month") {
let list = this.getDayOfMonthList();
let array = this.start_time.split(":");
let hour = parseInt(array[0]);
let minute = parseInt(array[1]);
let dayList = [];
for (let day of list) {
if (typeof day === "string" && day.startsWith("lastDay")) {
if (day === "lastDay1") {
dayList.push("L");
}
// Unfortunately, lastDay2-4 is not supported by cron
} else {
dayList.push(day);
}
}
// Remove duplicate
dayList = [ ...new Set(dayList) ];
this.cron = minute + " " + hour + " " + dayList.join(",") + " * *";
this.duration = this.calcDuration();
}
}
}

@ -1,223 +0,0 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
const dayjs = require("dayjs");
const { log, utcToLocal, SQL_DATETIME_FORMAT_WITHOUT_SECOND, localToUTC } = require("../../src/util");
const { UptimeKumaServer } = require("../uptime-kuma-server");
class MaintenanceTimeslot extends BeanModel {
/**
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
* @returns {Object}
*/
async toPublicJSON() {
const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset();
const obj = {
id: this.id,
startDate: this.start_date,
endDate: this.end_date,
startDateServerTimezone: utcToLocal(this.start_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
endDateServerTimezone: utcToLocal(this.end_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
serverTimezoneOffset,
};
return obj;
}
/**
* Return an object that ready to parse to JSON
* @returns {Object}
*/
async toJSON() {
return await this.toPublicJSON();
}
/**
* @param {Maintenance} maintenance
* @param {dayjs} minDate (For recurring type only) Generate a next timeslot from this date.
* @param {boolean} removeExist Remove existing timeslot before create
* @returns {Promise<MaintenanceTimeslot>}
*/
static async generateTimeslot(maintenance, minDate = null, removeExist = false) {
log.info("maintenance", "Generate Timeslot for maintenance id: " + maintenance.id);
if (removeExist) {
await R.exec("DELETE FROM maintenance_timeslot WHERE maintenance_id = ? ", [
maintenance.id
]);
}
if (maintenance.strategy === "manual") {
log.debug("maintenance", "No need to generate timeslot for manual type");
} else if (maintenance.strategy === "single") {
let bean = R.dispense("maintenance_timeslot");
bean.maintenance_id = maintenance.id;
bean.start_date = maintenance.start_date;
bean.end_date = maintenance.end_date;
bean.generated_next = true;
if (!await this.isDuplicateTimeslot(bean)) {
await R.store(bean);
return bean;
} else {
log.debug("maintenance", "Duplicate timeslot, skip");
return null;
}
} else if (maintenance.strategy === "recurring-interval") {
// Prevent dead loop, in case interval_day is not set
if (!maintenance.interval_day || maintenance.interval_day <= 0) {
maintenance.interval_day = 1;
}
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
return startDateTime.add(maintenance.interval_day, "day");
}, () => {
return true;
});
} else if (maintenance.strategy === "recurring-weekday") {
let dayOfWeekList = maintenance.getDayOfWeekList();
log.debug("timeslot", dayOfWeekList);
if (dayOfWeekList.length <= 0) {
log.debug("timeslot", "No weekdays selected?");
return null;
}
const isValid = (startDateTime) => {
log.debug("timeslot", "nextDateTime: " + startDateTime);
let day = startDateTime.local().day();
log.debug("timeslot", "nextDateTime.day(): " + day);
return dayOfWeekList.includes(day);
};
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
while (true) {
startDateTime = startDateTime.add(1, "day");
if (isValid(startDateTime)) {
return startDateTime;
}
}
}, isValid);
} else if (maintenance.strategy === "recurring-day-of-month") {
let dayOfMonthList = maintenance.getDayOfMonthList();
if (dayOfMonthList.length <= 0) {
log.debug("timeslot", "No day selected?");
return null;
}
const isValid = (startDateTime) => {
let day = parseInt(startDateTime.local().format("D"));
log.debug("timeslot", "day: " + day);
// Check 1-31
if (dayOfMonthList.includes(day)) {
return startDateTime;
}
// Check "lastDay1","lastDay2"...
let daysInMonth = startDateTime.daysInMonth();
let lastDayList = [];
// Small first, e.g. 28 > 29 > 30 > 31
for (let i = 4; i >= 1; i--) {
if (dayOfMonthList.includes("lastDay" + i)) {
lastDayList.push(daysInMonth - i + 1);
}
}
log.debug("timeslot", lastDayList);
return lastDayList.includes(day);
};
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
while (true) {
startDateTime = startDateTime.add(1, "day");
if (isValid(startDateTime)) {
return startDateTime;
}
}
}, isValid);
} else {
throw new Error("Unknown maintenance strategy");
}
}
static async isDuplicateTimeslot(timeslot) {
let bean = await R.findOne("maintenance_timeslot", "maintenance_id = ? AND start_date = ? AND end_date = ?", [
timeslot.maintenance_id,
timeslot.start_date,
timeslot.end_date
]);
return bean !== null;
}
/**
* Generate a next timeslot for all recurring types
* @param maintenance
* @param minDate
* @param {function} nextDayCallback The logic how to get the next possible day
* @param {function} isValidCallback Check the day whether is matched the current strategy
* @returns {Promise<null|MaintenanceTimeslot>}
*/
static async handleRecurringType(maintenance, minDate, nextDayCallback, isValidCallback) {
let bean = R.dispense("maintenance_timeslot");
let duration = maintenance.getDuration();
let startDateTime = maintenance.getStartDateTime();
let endDateTime;
// Keep generating from the first possible date, until it is ok
while (true) {
//log.debug("timeslot", "startDateTime: " + startDateTime.format());
// Handling out of effective date range
if (startDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
log.debug("timeslot", "Out of effective date range");
return null;
}
endDateTime = startDateTime.add(duration, "second");
// If endDateTime is out of effective date range, use the end datetime from effective date range
if (endDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
endDateTime = dayjs.utc(maintenance.end_date);
}
// If minDate is set, the endDateTime must be bigger than it.
// And the endDateTime must be bigger current time
// Is valid under current recurring strategy
if (
(!minDate || endDateTime.diff(minDate) > 0) &&
endDateTime.diff(dayjs()) > 0 &&
isValidCallback(startDateTime)
) {
break;
}
startDateTime = nextDayCallback(startDateTime);
}
bean.maintenance_id = maintenance.id;
bean.start_date = localToUTC(startDateTime);
bean.end_date = localToUTC(endDateTime);
bean.generated_next = false;
if (!await this.isDuplicateTimeslot(bean)) {
await R.store(bean);
return bean;
} else {
log.debug("maintenance", "Duplicate timeslot, skip");
return null;
}
}
}
module.exports = MaintenanceTimeslot;

@ -2,7 +2,9 @@ const https = require("https");
const dayjs = require("dayjs");
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } = require("../../src/util");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
SQL_DATETIME_FORMAT
} = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, mongodbPing,
} = require("../util-server");
@ -16,7 +18,6 @@ const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
const { DockerHost } = require("../docker");
const Maintenance = require("./maintenance");
const { UptimeCacheList } = require("../uptime-cache-list");
const Gamedig = require("gamedig");
@ -133,6 +134,9 @@ class Monitor extends BeanModel {
mqttPassword: this.mqttPassword,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
tlsCa: this.tlsCa,
tlsCert: this.tlsCert,
tlsKey: this.tlsKey,
};
}
@ -331,6 +335,18 @@ class Monitor extends BeanModel {
options.httpsAgent = new https.Agent(httpsAgentOptions);
}
if (this.auth_method === "mtls") {
if (this.tlsCert !== null && this.tlsCert !== "") {
options.httpsAgent.options.cert = Buffer.from(this.tlsCert);
}
if (this.tlsCa !== null && this.tlsCa !== "") {
options.httpsAgent.options.ca = Buffer.from(this.tlsCa);
}
if (this.tlsKey !== null && this.tlsKey !== "") {
options.httpsAgent.options.key = Buffer.from(this.tlsKey);
}
}
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
log.debug("monitor", `[${this.name}] Axios Request`);
@ -622,9 +638,7 @@ class Monitor extends BeanModel {
} else if (this.type === "mysql") {
let startTime = dayjs().valueOf();
await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "mongodb") {
@ -836,7 +850,6 @@ class Monitor extends BeanModel {
domain: this.authDomain,
workstation: this.authWorkstation ? this.authWorkstation : undefined
});
} else {
res = await axios.request(options);
}
@ -1165,12 +1178,18 @@ class Monitor extends BeanModel {
for (let notification of notificationList) {
try {
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
const heartbeatJSON = bean.toJSON();
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
if (!heartbeatJSON["msg"]) {
heartbeatJSON["msg"] = "N/A";
}
// Also provide the time in server timezone
heartbeatJSON["timezone"] = await UptimeKumaServer.getInstance().getTimezone();
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
} catch (e) {
log.error("monitor", "Cannot send notification to " + notification.name);
@ -1233,7 +1252,7 @@ class Monitor extends BeanModel {
if (notificationList.length > 0) {
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days = ?", [
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [
"certificate",
this.id,
targetDays,
@ -1251,7 +1270,7 @@ class Monitor extends BeanModel {
for (let notification of notificationList) {
try {
log.debug("monitor", "Sending to " + notification.name);
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] Certificate will be expired in ${daysRemaining} days`);
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] Certificate will expire in ${daysRemaining} days`);
sent = true;
} catch (e) {
log.error("monitor", "Cannot send cert notification to " + notification.name);
@ -1291,18 +1310,19 @@ class Monitor extends BeanModel {
* @returns {Promise<boolean>}
*/
static async isUnderMaintenance(monitorID) {
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
const maintenance = await R.getRow(`
SELECT COUNT(*) AS count
FROM monitor_maintenance mm
JOIN maintenance
ON mm.maintenance_id = maintenance.id
AND mm.monitor_id = ?
LEFT JOIN maintenance_timeslot
ON maintenance_timeslot.maintenance_id = maintenance.id
WHERE ${activeCondition}
LIMIT 1`, [ monitorID ]);
return maintenance.count !== 0;
const maintenanceIDList = await R.getCol(`
SELECT maintenance_id FROM monitor_maintenance
WHERE monitor_id = ?
`, [ monitorID ]);
for (const maintenanceID of maintenanceIDList) {
const maintenance = await UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
if (maintenance && await maintenance.isUnderMaintenance()) {
return true;
}
}
return false;
}
/** Make sure monitor interval is between bounds */

@ -3,7 +3,6 @@ const { R } = require("redbean-node");
const cheerio = require("cheerio");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const jsesc = require("jsesc");
const Maintenance = require("./maintenance");
const googleAnalytics = require("../google-analytics");
class StatusPage extends BeanModel {
@ -290,21 +289,17 @@ class StatusPage extends BeanModel {
try {
const publicMaintenanceList = [];
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
SELECT DISTINCT maintenance.*
FROM maintenance
JOIN maintenance_status_page
ON maintenance_status_page.maintenance_id = maintenance.id
AND maintenance_status_page.status_page_id = ?
LEFT JOIN maintenance_timeslot
ON maintenance_timeslot.maintenance_id = maintenance.id
WHERE ${activeCondition}
ORDER BY maintenance.end_date
`, [ statusPageId ]));
for (const bean of maintenanceBeanList) {
publicMaintenanceList.push(await bean.toPublicJSON());
let maintenanceIDList = await R.getCol(`
SELECT DISTINCT maintenance_id
FROM maintenance_status_page
WHERE status_page_id = ?
`, [ statusPageId ]);
for (const maintenanceID of maintenanceIDList) {
let maintenance = UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
if (maintenance && await maintenance.isUnderMaintenance()) {
publicMaintenanceList.push(await maintenance.toPublicJSON());
}
}
return publicMaintenanceList;

@ -10,7 +10,7 @@ class Mattermost extends NotificationProvider {
let okMsg = "Sent Successfully.";
try {
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
// If heartbeatJSON is null, assume we're testing.
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
if (heartbeatJSON == null) {
let mattermostTestData = {
username: mattermostUserName,
@ -27,86 +27,69 @@ class Mattermost extends NotificationProvider {
}
const mattermostIconEmoji = notification.mattermosticonemo;
const mattermostIconUrl = notification.mattermosticonurl;
let mattermostIconEmojiOnline = "";
let mattermostIconEmojiOffline = "";
if (heartbeatJSON["status"] === DOWN) {
let mattermostdowndata = {
username: mattermostUserName,
text: "Uptime Kuma Alert",
channel: mattermostChannel,
icon_emoji: mattermostIconEmoji,
icon_url: mattermostIconUrl,
attachments: [
{
fallback:
"Your " +
monitorJSON["name"] +
" service went down.",
color: "#FF0000",
title:
"❌ " +
monitorJSON["name"] +
" service went down. ❌",
title_link: monitorJSON["url"],
fields: [
{
short: true,
title: "Service Name",
value: monitorJSON["name"],
},
{
short: true,
title: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
if (mattermostIconEmoji && typeof mattermostIconEmoji === "string") {
const emojiArray = mattermostIconEmoji.split(" ");
if (emojiArray.length >= 2) {
mattermostIconEmojiOnline = emojiArray[0];
mattermostIconEmojiOffline = emojiArray[1];
}
}
const mattermostIconUrl = notification.mattermosticonurl;
let iconEmoji = mattermostIconEmoji;
let statusField = {
short: false,
title: "Error",
value: heartbeatJSON["msg"],
},
],
},
],
value: heartbeatJSON.msg,
};
await axios.post(
notification.mattermostWebhookUrl,
mattermostdowndata
);
return okMsg;
} else if (heartbeatJSON["status"] === UP) {
let mattermostupdata = {
username: mattermostUserName,
text: "Uptime Kuma Alert",
let statusText = "unknown";
let color = "#000000";
if (heartbeatJSON.status === DOWN) {
iconEmoji = mattermostIconEmojiOffline || mattermostIconEmoji;
statusField = {
short: false,
title: "Error",
value: heartbeatJSON.msg,
};
statusText = "down.";
color = "#FF0000";
} else if (heartbeatJSON.status === UP) {
iconEmoji = mattermostIconEmojiOnline || mattermostIconEmoji;
statusField = {
short: false,
title: "Ping",
value: heartbeatJSON.ping + "ms",
};
statusText = "up!";
color = "#32CD32";
}
let mattermostdata = {
username: monitorJSON.name + " " + mattermostUserName,
channel: mattermostChannel,
icon_emoji: mattermostIconEmoji,
icon_emoji: iconEmoji,
icon_url: mattermostIconUrl,
attachments: [
{
fallback:
"Your " +
monitorJSON["name"] +
" service went up!",
color: "#32CD32",
monitorJSON.name +
" service went " +
statusText,
color: color,
title:
"✅ " +
monitorJSON["name"] +
" service went up! ✅",
title_link: monitorJSON["url"],
monitorJSON.name +
" service went " +
statusText,
title_link: monitorJSON.url,
fields: [
{
short: true,
title: "Service Name",
value: monitorJSON["name"],
},
statusField,
{
short: true,
title: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
short: false,
title: "Ping",
value: heartbeatJSON["ping"] + "ms",
value: heartbeatJSON.time,
},
],
},
@ -114,10 +97,9 @@ class Mattermost extends NotificationProvider {
};
await axios.post(
notification.mattermostWebhookUrl,
mattermostupdata
mattermostdata
);
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error);
}

@ -1,5 +1,6 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Ntfy extends NotificationProvider {
@ -14,11 +15,45 @@ class Ntfy extends NotificationProvider {
"Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"),
};
}
let data = {
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
if (heartbeatJSON == null) {
let ntfyTestData = {
"topic": notification.ntfytopic,
"title": (monitorJSON?.name || notification.ntfytopic) + " [Uptime-Kuma]",
"message": msg,
"priority": notification.ntfyPriority || 4,
"title": "Uptime-Kuma",
"priority": notification.ntfyPriority,
"tags": [ "test_tube" ],
};
await axios.post(`${notification.ntfyserverurl}`, ntfyTestData, { headers: headers });
return okMsg;
}
let tags = [];
let status = "unknown";
let priority = notification.ntfyPriority || 4;
if ("status" in heartbeatJSON) {
if (heartbeatJSON.status === DOWN) {
tags = [ "red_circle" ];
status = "Down";
// if priority is not 5, increase priority for down alerts
priority = priority === 5 ? priority : priority + 1;
} else if (heartbeatJSON["status"] === UP) {
tags = [ "green_circle" ];
status = "Up";
}
}
let data = {
"topic": notification.ntfytopic,
"message": heartbeatJSON.msg,
"priority": priority,
"title": monitorJSON.name + " " + status + " [Uptime-Kuma]",
"tags": tags,
"actions": [
{
"action": "view",
"label": "Open " + monitorJSON.name,
"url": monitorJSON.url,
}
]
};
if (notification.ntfyIcon) {

@ -0,0 +1,97 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN } = require("../../src/util");
const opsgenieAlertsUrlEU = "https://api.eu.opsgenie.com/v2/alerts";
const opsgenieAlertsUrlUS = "https://api.opsgenie.com/v2/alerts";
let okMsg = "Sent Successfully.";
class Opsgenie extends NotificationProvider {
name = "Opsgenie";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let opsgenieAlertsUrl;
let priority = (notification.opsgeniePriority == "") ? 3 : notification.opsgeniePriority;
const textMsg = "Uptime Kuma Alert";
try {
switch (notification.opsgenieRegion) {
case "US":
opsgenieAlertsUrl = opsgenieAlertsUrlUS;
break;
case "EU":
opsgenieAlertsUrl = opsgenieAlertsUrlEU;
break;
default:
opsgenieAlertsUrl = opsgenieAlertsUrlUS;
}
if (heartbeatJSON == null) {
let notificationTestAlias = "uptime-kuma-notification-test";
let data = {
"message": msg,
"alias": notificationTestAlias,
"source": "Uptime Kuma",
"priority": "P5"
};
return this.post(notification, opsgenieAlertsUrl, data);
}
if (heartbeatJSON.status === DOWN) {
let data = {
"message": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
"alias": monitorJSON.name,
"description": msg,
"source": "Uptime Kuma",
"priority": `P${priority}`
};
return this.post(notification, opsgenieAlertsUrl, data);
}
if (heartbeatJSON.status === UP) {
let opsgenieAlertsCloseUrl = `${opsgenieAlertsUrl}/${encodeURIComponent(monitorJSON.name)}/close?identifierType=alias`;
let data = {
"source": "Uptime Kuma",
};
return this.post(notification, opsgenieAlertsCloseUrl, data);
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
/**
*
* @param {BeanModel} notification
* @param {string} url Request url
* @param {Object} data Request body
* @returns {Promise<string>}
*/
async post(notification, url, data) {
let config = {
headers: {
"Content-Type": "application/json",
"Authorization": `GenieKey ${notification.opsgenieApiKey}`,
}
};
let res = await axios.post(url, data, config);
if (res.status == null) {
return "Opsgenie notification failed with invalid response!";
}
if (res.status < 200 || res.status >= 300) {
return `Opsgenie notification failed with status code ${res.status}`;
}
return okMsg;
}
}
module.exports = Opsgenie;

@ -0,0 +1,41 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Twilio extends NotificationProvider {
name = "twilio";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
let accountSID = notification.twilioAccountSID;
let authToken = notification.twilioAuthToken;
try {
let config = {
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
"Authorization": "Basic " + Buffer.from(accountSID + ":" + authToken).toString("base64"),
}
};
let data = new URLSearchParams();
data.append("To", notification.twilioToNumber);
data.append("From", notification.twilioFromNumber);
data.append("Body", msg);
let url = "https://api.twilio.com/2010-04-01/Accounts/" + accountSID + "/Messages.json";
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Twilio;

@ -23,6 +23,7 @@ const Mattermost = require("./notification-providers/mattermost");
const Ntfy = require("./notification-providers/ntfy");
const Octopush = require("./notification-providers/octopush");
const OneBot = require("./notification-providers/onebot");
const Opsgenie = require("./notification-providers/opsgenie");
const PagerDuty = require("./notification-providers/pagerduty");
const PagerTree = require("./notification-providers/pagertree");
const PromoSMS = require("./notification-providers/promosms");
@ -41,6 +42,7 @@ const Stackfield = require("./notification-providers/stackfield");
const Teams = require("./notification-providers/teams");
const TechulusPush = require("./notification-providers/techulus-push");
const Telegram = require("./notification-providers/telegram");
const Twilio = require("./notification-providers/twilio");
const Splunk = require("./notification-providers/splunk");
const Webhook = require("./notification-providers/webhook");
const WeCom = require("./notification-providers/wecom");
@ -83,6 +85,7 @@ class Notification {
new Ntfy(),
new Octopush(),
new OneBot(),
new Opsgenie(),
new PagerDuty(),
new PagerTree(),
new PromoSMS(),
@ -103,6 +106,7 @@ class Notification {
new Teams(),
new TechulusPush(),
new Telegram(),
new Twilio(),
new Splunk(),
new Webhook(),
new WeCom(),

@ -132,6 +132,9 @@ class Proxy {
...httpAgentOptions,
...httpsAgentOptions,
...proxyOptions,
tls: {
rejectUnauthorized: httpsAgentOptions.rejectUnauthorized,
},
});
httpAgent = agent;

@ -10,6 +10,7 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
const { UptimeCacheList } = require("../uptime-cache-list");
const { makeBadge } = require("badge-maker");
const { badgeConstants } = require("../config");
const { Prometheus } = require("../prometheus");
let router = express.Router();
@ -37,7 +38,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
let pushToken = request.params.pushToken;
let msg = request.query.msg || "OK";
let ping = request.query.ping || null;
let ping = parseInt(request.query.ping) || null;
let statusString = request.query.status || "up";
let status = (statusString === "up") ? UP : DOWN;
@ -89,6 +90,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
io.to(monitor.user_id).emit("heartbeat", bean.toJSON());
UptimeCacheList.clearCache(monitor.id);
Monitor.sendStats(io, monitor.id, monitor.user_id);
new Prometheus(monitor).update(bean, undefined);
response.json({
ok: true,
@ -147,7 +149,11 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response
const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId);
const state = overrideValue !== undefined ? overrideValue : heartbeat.status;
badgeValues.label = label ?? "Status";
if (label === undefined) {
badgeValues.label = "Status";
} else {
badgeValues.label = label;
}
switch (state) {
case DOWN:
badgeValues.color = downColor;
@ -224,7 +230,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
);
// limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits
const cleanUptime = parseFloat(uptime.toPrecision(4));
const cleanUptime = (uptime * 100).toPrecision(4);
// use a given, custom color or calculate one based on the uptime value
badgeValues.color = color ?? percentageToColor(uptime);
@ -235,7 +241,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
labelPrefix,
label ?? `Uptime (${requestedDuration}${labelSuffix})`,
]);
badgeValues.message = filterAndJoin([ prefix, `${cleanUptime * 100}`, suffix ]);
badgeValues.message = filterAndJoin([ prefix, cleanUptime, suffix ]);
}
// build the SVG based on given values

@ -688,6 +688,9 @@ let needSetup = false;
bean.headers = monitor.headers;
bean.basic_auth_user = monitor.basic_auth_user;
bean.basic_auth_pass = monitor.basic_auth_pass;
bean.tlsCa = monitor.tlsCa;
bean.tlsCert = monitor.tlsCert;
bean.tlsKey = monitor.tlsKey;
bean.interval = monitor.interval;
bean.retryInterval = monitor.retryInterval;
bean.resendInterval = monitor.resendInterval;

@ -5,7 +5,6 @@ const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const Maintenance = require("../model/maintenance");
const server = UptimeKumaServer.getInstance();
const MaintenanceTimeslot = require("../model/maintenance_timeslot");
/**
* Handlers for Maintenance
@ -19,10 +18,12 @@ module.exports.maintenanceSocketHandler = (socket) => {
log.debug("maintenance", maintenance);
let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
let bean = await Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
bean.user_id = socket.userID;
let maintenanceID = await R.store(bean);
await MaintenanceTimeslot.generateTimeslot(bean);
server.maintenanceList[maintenanceID] = bean;
await bean.run(true);
await server.sendMaintenanceList(socket);
@ -45,17 +46,15 @@ module.exports.maintenanceSocketHandler = (socket) => {
try {
checkLogin(socket);
let bean = await R.findOne("maintenance", " id = ? ", [ maintenance.id ]);
let bean = server.getMaintenance(maintenance.id);
if (bean.user_id !== socket.userID) {
throw new Error("Permission denied.");
}
Maintenance.jsonToBean(bean, maintenance);
await Maintenance.jsonToBean(bean, maintenance);
await R.store(bean);
await MaintenanceTimeslot.generateTimeslot(bean, null, true);
await bean.run(true);
await server.sendMaintenanceList(socket);
callback({
@ -236,6 +235,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
if (maintenanceID in server.maintenanceList) {
server.maintenanceList[maintenanceID].stop();
delete server.maintenanceList[maintenanceID];
}
@ -267,9 +267,15 @@ module.exports.maintenanceSocketHandler = (socket) => {
log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
await R.exec("UPDATE maintenance SET active = 0 WHERE id = ? ", [
maintenanceID,
]);
let maintenance = server.getMaintenance(maintenanceID);
if (!maintenance) {
throw new Error("Maintenance not found");
}
maintenance.active = false;
await R.store(maintenance);
maintenance.stop();
apicache.clear();
@ -294,9 +300,15 @@ module.exports.maintenanceSocketHandler = (socket) => {
log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
await R.exec("UPDATE maintenance SET active = 1 WHERE id = ? ", [
maintenanceID,
]);
let maintenance = server.getMaintenance(maintenanceID);
if (!maintenance) {
throw new Error("Maintenance not found");
}
maintenance.active = true;
await R.store(maintenance);
await maintenance.run();
apicache.clear();

@ -276,7 +276,7 @@ module.exports.statusPageSocketHandler = (socket) => {
let statusPage = R.dispense("status_page");
statusPage.slug = slug;
statusPage.title = title;
statusPage.theme = "light";
statusPage.theme = "auto";
statusPage.icon = "";
await R.store(statusPage);

@ -47,8 +47,6 @@ class UptimeKumaServer {
*/
indexHTML = "";
generateMaintenanceTimeslotsInterval = undefined;
/**
* Plugins Manager
* @type {PluginsManager}
@ -74,6 +72,7 @@ class UptimeKumaServer {
// SSL
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
log.info("server", "Creating express and socket.io instance");
this.app = express();
@ -81,7 +80,8 @@ class UptimeKumaServer {
log.info("server", "Server Type: HTTPS");
this.httpServer = https.createServer({
key: fs.readFileSync(sslKey),
cert: fs.readFileSync(sslCert)
cert: fs.readFileSync(sslCert),
passphrase: sslKeyPassphrase,
}, this.app);
} else {
log.info("server", "Server Type: HTTP");
@ -110,8 +110,7 @@ class UptimeKumaServer {
log.debug("DEBUG", "Timezone: " + process.env.TZ);
log.debug("DEBUG", "Current Time: " + dayjs.tz().format());
await this.generateMaintenanceTimeslots();
this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
await this.loadMaintenanceList();
}
/**
@ -173,16 +172,33 @@ class UptimeKumaServer {
*/
async getMaintenanceJSONList(userID) {
let result = {};
for (let maintenanceID in this.maintenanceList) {
result[maintenanceID] = await this.maintenanceList[maintenanceID].toJSON();
}
return result;
}
/**
* Load maintenance list and run
* @param userID
* @returns {Promise<void>}
*/
async loadMaintenanceList(userID) {
let maintenanceList = await R.findAll("maintenance", " ORDER BY end_date DESC, title", [
let maintenanceList = await R.find("maintenance", " user_id = ? ORDER BY end_date DESC, title", [
userID,
]);
for (let maintenance of maintenanceList) {
result[maintenance.id] = await maintenance.toJSON();
this.maintenanceList[maintenance.id] = maintenance;
maintenance.run(this);
}
}
return result;
getMaintenance(maintenanceID) {
if (this.maintenanceList[maintenanceID]) {
return this.maintenanceList[maintenanceID];
}
return null;
}
/**
@ -238,7 +254,7 @@ class UptimeKumaServer {
* Attempt to get the current server timezone
* If this fails, fall back to environment variables and then make a
* guess.
* @returns {string}
* @returns {Promise<string>}
*/
async getTimezone() {
let timezone = await Settings.get("serverTimezone");
@ -269,28 +285,9 @@ class UptimeKumaServer {
dayjs.tz.setDefault(timezone);
}
/** Load the timeslots for maintenance */
async generateMaintenanceTimeslots() {
log.debug("maintenance", "Routine: Generating Maintenance Timeslots");
// Prevent #2776
// Remove duplicate maintenance_timeslot with same start_date, end_date and maintenance_id
await R.exec("DELETE FROM maintenance_timeslot WHERE id NOT IN (SELECT MIN(id) FROM maintenance_timeslot GROUP BY start_date, end_date, maintenance_id)");
let list = await R.find("maintenance_timeslot", " generated_next = 0 AND start_date <= DATETIME('now') ");
for (let maintenanceTimeslot of list) {
let maintenance = await maintenanceTimeslot.maintenance;
await MaintenanceTimeslot.generateTimeslot(maintenance, maintenanceTimeslot.end_date, false);
maintenanceTimeslot.generated_next = true;
await R.store(maintenanceTimeslot);
}
}
/** Stop the server */
async stop() {
clearTimeout(this.generateMaintenanceTimeslotsInterval);
}
loadPlugins() {
@ -339,5 +336,4 @@ module.exports = {
};
// Must be at the end
const MaintenanceTimeslot = require("./model/maintenance_timeslot");
const { MonitorType } = require("./monitor-types/monitor-type");

@ -322,19 +322,26 @@ exports.postgresQuery = function (connectionString, query) {
* Run a query on MySQL/MariaDB
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
* @returns {Promise<(string)>}
*/
exports.mysqlQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
const connection = mysql.createConnection(connectionString);
connection.promise().query(query)
.then(res => {
resolve(res);
})
.catch(err => {
connection.on("error", (err) => {
reject(err);
})
.finally(() => {
});
connection.query(query, (err, res) => {
if (err) {
reject(err);
} else {
if (Array.isArray(res)) {
resolve("Rows: " + res.length);
} else {
resolve("No Error, but the result is not an array. Type: " + typeof res);
}
}
connection.destroy();
});
});

@ -556,6 +556,31 @@ h5.settings-subheading::after {
border-bottom: 1px solid $dark-border-color;
}
$shadow-box-padding: 20px;
.shadow-box-with-fixed-bottom-bar {
padding-top: $shadow-box-padding;
padding-bottom: 0;
padding-right: $shadow-box-padding;
padding-left: $shadow-box-padding;
}
.fixed-bottom-bar {
position: sticky;
bottom: 0;
margin-left: -$shadow-box-padding;
margin-right: -$shadow-box-padding;
z-index: 100;
background-color: rgba(white, 0.2);
backdrop-filter: blur(2px);
border-radius: 0 0 10px 10px;
.dark & {
background-color: rgba($dark-header-bg, 0.9);
}
}
// Localization
@import "localization.scss";

@ -159,6 +159,16 @@ export default {
}
});
},
/** Clear Form inputs */
clearForm() {
this.key = {
name: "",
expires: this.minDate,
active: 1,
};
this.noExpire = false;
},
}
};
</script>

@ -13,6 +13,9 @@
:disabled="disabled"
>
<!-- A hidden textarea for copying text on non-https -->
<textarea ref="hiddenTextarea" style="position: fixed; left: -999999px; top: -999999px;"></textarea>
<a class="btn btn-outline-primary" @click="copyToClipboard(model)">
<font-awesome-icon :icon="icon" />
</a>
@ -111,24 +114,19 @@ export default {
}, 3000);
// navigator clipboard api needs a secure context (https)
// For http, use the text area method (else part)
if (navigator.clipboard && window.isSecureContext) {
// navigator clipboard api method'
return navigator.clipboard.writeText(textToCopy);
} else {
// text area method
let textArea = document.createElement("textarea");
let textArea = this.$refs.hiddenTextarea;
textArea.value = textToCopy;
// make the textarea out of viewport
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
textArea.style.top = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return new Promise((res, rej) => {
// here the magic happens
document.execCommand("copy") ? res() : rej();
textArea.remove();
});
}
}

@ -3,16 +3,23 @@
<div v-if="maintenance.strategy === 'manual'" class="timeslot">
{{ $t("Manual") }}
</div>
<div v-else-if="maintenance.timeslotList.length > 0" class="timeslot">
{{ maintenance.timeslotList[0].startDateServerTimezone }}
<div v-else-if="maintenance.timeslotList.length > 0">
<div class="timeslot">
{{ startDateTime }}
<span class="to">-</span>
{{ maintenance.timeslotList[0].endDateServerTimezone }}
(UTC{{ maintenance.timeslotList[0].serverTimezoneOffset }})
{{ endDateTime }}
</div>
<div class="timeslot">
UTC{{ maintenance.timezoneOffset }} <span v-if="maintenance.timezone !== 'UTC'">{{ maintenance.timezone }}</span>
</div>
</div>
</div>
</template>
<script>
import dayjs from "dayjs";
import { SQL_DATETIME_FORMAT_WITHOUT_SECOND } from "../util.ts";
export default {
props: {
maintenance: {
@ -20,6 +27,14 @@ export default {
required: true
},
},
computed: {
startDateTime() {
return dayjs(this.maintenance.timeslotList[0].startDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
},
endDateTime() {
return dayjs(this.maintenance.timeslotList[0].endDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
}
},
};
</script>
@ -31,6 +46,7 @@ export default {
background-color: rgba(255, 255, 255, 0.5);
border-radius: 20px;
padding: 0 10px;
margin-right: 5px;
.to {
margin: 0 6px;

@ -129,6 +129,7 @@ export default {
"ntfy": "Ntfy",
"octopush": "Octopush",
"OneBot": "OneBot",
"Opsgenie": "Opsgenie",
"PagerDuty": "PagerDuty",
"pushbullet": "Pushbullet",
"PushByTechulus": "Push by Techulus",
@ -143,6 +144,7 @@ export default {
"stackfield": "Stackfield",
"teams": "Microsoft Teams",
"telegram": "Telegram",
"twilio": "Twilio",
"Splunk": "Splunk",
"webhook": "Webhook",
"GoAlert": "GoAlert",

@ -11,16 +11,16 @@
</ul>
</div>
<div class="chart-wrapper" :class="{ loading : loading}">
<LineChart :chart-data="chartData" :options="chartOptions" />
<Line :data="chartData" :options="chartOptions" />
</div>
</div>
</template>
<script lang="js">
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
import "chartjs-adapter-dayjs";
import "chartjs-adapter-dayjs-4";
import dayjs from "dayjs";
import { LineChart } from "vue-chart-3";
import { Line } from "vue-chartjs";
import { useToast } from "vue-toastification";
import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts";
@ -29,7 +29,7 @@ const toast = useToast();
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
export default {
components: { LineChart },
components: { Line },
props: {
/** ID of monitor */
monitorId: {
@ -104,8 +104,10 @@ export default {
}
},
ticks: {
sampleSize: 3,
maxRotation: 0,
autoSkipPadding: 30,
padding: 3,
},
grid: {
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)",
@ -197,6 +199,7 @@ export default {
borderColor: "#5CDD8B",
backgroundColor: "#5CDD8B38",
yAxisID: "y",
label: "ping",
},
{
// Bar Chart
@ -208,6 +211,8 @@ export default {
barThickness: "flex",
barPercentage: 1,
categoryPercentage: 1,
inflateAmount: 0.05,
label: "status",
},
],
};

@ -0,0 +1,36 @@
<template>
<div class="mb-3">
<label for="opsgenie-region" class="form-label">{{ $t("Region") }}<span style="color: red;"><sup>*</sup></span></label>
<select id="opsgenie-region" v-model="$parent.notification.opsgenieRegion" class="form-select" required>
<option value="us">
US (Default)
</option>
<option value="eu">
EU
</option>
</select>
</div>
<div class="mb-3">
<label for="opsgenie-apikey" class="form-label">{{ $t("API Key") }}<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="opsgenie-apikey" v-model="$parent.notification.opsgenieApiKey" required="true" autocomplete="false"></HiddenInput>
</div>
<div class="mb-3">
<label for="opsgenie-priority" class="form-label">{{ $t("Priority") }}</label>
<input id="opsgenie-priority" v-model="$parent.notification.opsgeniePriority" type="number" class="form-control" min="1" max="5" step="1">
</div>
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
<a href="https://docs.opsgenie.com/docs/alert-api" target="_blank">https://docs.opsgenie.com/docs/alert-api</a>
</i18n-t>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

@ -97,7 +97,6 @@
(leave blank for default one)<br />
{{NAME}}: Service Name<br />
{{HOSTNAME_OR_URL}}: Hostname or URL<br />
{{URL}}: URL<br />
{{STATUS}}: Status<br />
</div>
</div>

@ -0,0 +1,27 @@
<template>
<div class="mb-3">
<label for="twilio-account-sid" class="form-label">{{ $t("Account SID") }}</label>
<input id="twilio-account-sid" v-model="$parent.notification.twilioAccountSID" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="twilio-auth-token" class="form-label">{{ $t("Auth Token") }}</label>
<input id="twilio-auth-token" v-model="$parent.notification.twilioAuthToken" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="twilio-from-number" class="form-label">{{ $t("From Number") }}</label>
<input id="twilio-from-number" v-model="$parent.notification.twilioFromNumber" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="twilio-to-number" class="form-label">{{ $t("To Number") }}</label>
<input id="twilio-to-number" v-model="$parent.notification.twilioToNumber" type="text" class="form-control" required>
</div>
<div class="mb-3">
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://www.twilio.com/docs/sms" target="_blank">https://www.twilio.com/docs/sms</a>
</i18n-t>
</div>
</template>

@ -21,6 +21,7 @@ import Mattermost from "./Mattermost.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 PagerTree from "./PagerTree.vue";
import PromoSMS from "./PromoSMS.vue";
@ -41,6 +42,7 @@ import STMP from "./SMTP.vue";
import Teams from "./Teams.vue";
import TechulusPush from "./TechulusPush.vue";
import Telegram from "./Telegram.vue";
import Twilio from "./Twilio.vue";
import Webhook from "./Webhook.vue";
import WeCom from "./WeCom.vue";
import GoAlert from "./GoAlert.vue";
@ -76,6 +78,7 @@ const NotificationFormList = {
"ntfy": Ntfy,
"octopush": Octopush,
"OneBot": OneBot,
"Opsgenie": Opsgenie,
"PagerDuty": PagerDuty,
"PagerTree": PagerTree,
"promosms": PromoSMS,
@ -95,6 +98,7 @@ const NotificationFormList = {
"stackfield": Stackfield,
"teams": Teams,
"telegram": Telegram,
"twilio": Twilio,
"Splunk": Splunk,
"webhook": Webhook,
"WeCom": WeCom,

@ -41,7 +41,8 @@ const languageList = {
"el-GR": "Ελληνικά",
"yue": "繁體中文 (廣東話 / 粵語)",
"ro": "Limba română",
"ur": "Urdu"
"ur": "Urdu",
"ge": "ქართული"
};
let messages = {

@ -681,7 +681,7 @@
"infiniteRetention": "Задайте стойност 0 за безкрайно съхранение.",
"Monitor": "Монитор | Монитори",
"dataRetentionTimeError": "Периодът на съхранение трябва да е 0 или по-голям",
"confirmDeleteTagMsg": "Сигурни ли сте, че желаете да изтриете този таг? Мониторите, свързани с него, няма да бъдат изтрити.",
"confirmDeleteTagMsg": "Сигурни ли сте, че желаете да изтриете този етикет? Мониторите, свързани с него, няма да бъдат изтрити.",
"promosmsAllowLongSMS": "Позволи дълъг SMS",
"Packet Size": "Размер на пакет",
"Custom Monitor Type": "Потребителски тип монитор",
@ -694,7 +694,7 @@
"confirmUninstallPlugin": "Сигурни ли сте, че желаете да деинсталирате този плъгин?",
"markdownSupported": "Поддържа се Markdown синтаксис",
"Google Analytics ID": "Google Analytics ID",
"Edit Tag": "Редактиране на таг",
"Edit Tag": "Редактиране на етикет",
"Learn More": "Научете повече",
"Server Address": "Сървър адрес",
"notificationRegional": "Регионални",
@ -734,5 +734,9 @@
"wayToGetPagerTreeIntegrationURL": "След като създадете интеграция на Uptime Kuma в PagerTree, копирайте крайната точка. За пълни подробности вижте {0}",
"pagertreeIntegrationUrl": "URL Адрес за интеграция",
"pagertreeMedium": "Средна",
"pagertreeCritical": "Критична"
"pagertreeCritical": "Критична",
"Add New Tag": "Добави нов етикет",
"lunaseaTarget": "Цел",
"lunaseaDeviceID": "ID на устройството",
"lunaseaUserID": "ID на потребител"
}

@ -542,7 +542,7 @@
"promosmsPassword": "API Password",
"pushoversounds pushover": "Pushover (výchozí)",
"pushoversounds bike": "Kolo",
"pushoversounds bugle": "Bugle",
"pushoversounds bugle": "Trumpeta",
"pushoversounds cashregister": "Pokladna",
"pushoversounds classical": "Classical",
"pushoversounds cosmic": "Kosmický",
@ -554,9 +554,9 @@
"pushoversounds mechanical": "Mechanika",
"pushoversounds pianobar": "Barové piano",
"pushoversounds siren": "Siréna",
"pushoversounds spacealarm": "Space Alarm",
"pushoversounds tugboat": "Tug Boat",
"pushoversounds alien": "Alien Alarm (dlouhý)",
"pushoversounds spacealarm": "Vesmírný alarm",
"pushoversounds tugboat": "Remorkér",
"pushoversounds alien": "Mimozemský poplach (dlouhý)",
"pushoversounds climb": "Climb (dlouhý)",
"pushoversounds persistent": "Persistent (dlouhý)",
"pushoversounds echo": "Pushover Echo (dlouhý)",
@ -661,7 +661,7 @@
"dnsCacheDescription": "V některých IPv6 prostředích nemusí fungovat. Pokud narazíte na nějaké problémy, vypněte jej.",
"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í",
"Effective Date Range": "Časové období (volitelné)",
"Schedule Maintenance": "Naplánovat údržbu",
"Date and Time": "Datum a čas",
"DateTime Range": "Rozsah data a času",
@ -736,7 +736,18 @@
"pagertreeHigh": "Nahlas",
"wayToGetPagerTreeIntegrationURL": "Po vytvoření integrace Uptime Kuma v aplikaci PagerTree zkopírujte koncový bod. Zobrazit všechny podrobnosti {0}",
"Add New Tag": "Přidat nový štítek",
"lunaseaTarget": "cíl",
"lunaseaTarget": "Cíl",
"lunaseaDeviceID": "ID zařízení",
"lunaseaUserID": "ID uživatele"
"lunaseaUserID": "ID uživatele",
"statusPageRefreshIn": "Obnovení za: {0}",
"twilioAccountSID": "SID účtu",
"twilioFromNumber": "Číslo odesílatele",
"twilioToNumber": "Číslo příjemce",
"twilioAuthToken": "Autorizační token",
"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",
"endDateTime": "Datum/čas konce"
}

@ -102,7 +102,7 @@
"deleteNotificationMsg": "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?",
"resolverserverDescription": "Cloudflare ist als der Standardserver festgelegt. Dieser kann jederzeit geändert werden.",
"Resolver Server": "Auflösungsserver",
"rrtypeDescription": "Wähle den RR-Typ aus, welchen du überwachen möchtest.",
"rrtypeDescription": "Wähle den RR Typ aus, welchen du überwachen möchtest",
"Last Result": "Letztes Ergebnis",
"pauseMonitorMsg": "Bist du sicher, dass du den Monitor pausieren möchtest?",
"clearEventsMsg": "Bist du sicher, dass du alle Ereignisse für diesen Monitor löschen möchtest?",
@ -206,7 +206,7 @@
"mattermost": "Mattermost",
"Primary Base URL": "Primär URL",
"Push URL": "Push URL",
"needPushEvery": "Du solltest diese URL alle {0} Sekunden aufrufen",
"needPushEvery": "Du solltest diese URL alle {0} Sekunden aufrufen.",
"pushOptionalParams": "Optionale Parameter: {0}",
"defaultNotificationName": "Mein {notification} Alarm ({number})",
"here": "hier",
@ -276,10 +276,10 @@
"appriseInstalled": "Apprise ist installiert.",
"appriseNotInstalled": "Apprise ist nicht installiert. {0}",
"Access Token": "Access Token",
"Channel access token": "Channel access token",
"Channel access token": "Channel Access Token",
"Line Developers Console": "Line Developers Console",
"lineDevConsoleTo": "Line Developers Console - {0}",
"Basic Settings": "Basic Settings",
"Basic Settings": "Grundeinstellungen",
"User ID": "User ID",
"Messaging API": "Messaging API",
"wayToGetLineChannelToken": "Rufe zuerst {0} auf, erstelle dann einen Provider und Channel (Messaging API). Als nächstes kannst du den Channel access token und die User ID aus den oben genannten Menüpunkten abrufen.",
@ -298,7 +298,7 @@
"Internal Room Id": "Interne Raum-ID",
"matrixDesc1": "Die interne Raum-ID findest du im erweiterten Bereich der Raumeinstellungen im Matrix-Client. Es sollte aussehen wie z.B. !QMdRCpUIfLwsfjxye6:home.server.",
"matrixDesc2": "Es wird dringend empfohlen einen neuen Benutzer anzulegen und nicht den Zugriffstoken deines eigenen Matrix-Benutzers zu verwenden. Anderenfalls ermöglicht es vollen Zugriff auf dein Konto und alle Räume, denen du beigetreten bist. Erstelle stattdessen einen neuen Benutzer und lade ihn nur in den Raum ein, in dem du die Benachrichtigung erhalten möchtest. Du kannst den Zugriffstoken erhalten, indem du Folgendes ausführst {0}",
"Method": "Method",
"Method": "Methode",
"Body": "Body",
"Headers": "Headers",
"PushUrl": "Push URL",
@ -348,7 +348,7 @@
"Services": "Dienste",
"Discard": "Verwerfen",
"Cancel": "Abbrechen",
"Powered by": "Powered by",
"Powered by": "Erstellt mit",
"shrinkDatabaseDescription": "Löse VACUUM für die SQLite Datenbank aus. Wenn die Datenbank nach 1.10.0 erstellt wurde, ist AUTO_VACUUM bereits aktiviert und diese Aktion ist nicht erforderlich.",
"serwersms": "SerwerSMS.pl",
"serwersmsAPIUser": "API Benutzername (inkl. webapi_ prefix)",
@ -533,7 +533,7 @@
"Also check beta release": "Auch nach beta Versionen schauen",
"Using a Reverse Proxy?": "Wird ein Reverse Proxy genutzt?",
"Check how to config it for WebSocket": "Prüfen, wie er für die Nutzung mit WebSocket konfiguriert wird",
"Steam Game Server": "Steam Game Server",
"Steam Game Server": "Steam Spielserver",
"Most likely causes:": "Wahrscheinliche Ursachen:",
"The resource is no longer available.": "Die Quelle ist nicht mehr verfügbar.",
"There might be a typing error in the address.": "Es gibt einen Tippfehler in der Adresse.",
@ -590,7 +590,7 @@
"atLeastOneMonitor": "Wähle mindestens einen Monitor",
"deleteMaintenanceMsg": "Möchtest du diese Wartung löschen?",
"Base URL": "Basis URL",
"goAlertInfo": "GoAlert ist eine Open-Source Applikation für Rufbereitschaftsplanung, automatische Eskalation und Benachrichtigung (z.B. SMS oder Telefonanrufe). Beauftragen Sie automatisch die richtige Person, auf die richtige Art und Weise und zum richtigen Zeitpunkt. {0}",
"goAlertInfo": "GoAlert ist eine Open-Source Applikation für Rufbereitschaftsplanung, automatische Eskalation und Benachrichtigung (z.B. SMS oder Telefonanrufe). Engagiere automatisch die richtige Person, auf die richtige Art und Weise und zum richtigen Zeitpunkt! {0}",
"goAlertIntegrationKeyInfo": "Bekommt einen generischen API Schlüssel in folgenden Format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\". Normalerweise entspricht dies dem Wert des Token aus der URL.",
"goAlert": "GoAlert",
"backupOutdatedWarning": "Veraltet: Da viele Funktionen hinzugefügt wurden und die Backupfunktion nicht mehr gepflegt wird, kann keine vollständige Sicherung erstellt oder wiederhergestellt werden.",
@ -599,13 +599,13 @@
"squadcast": "Squadcast",
"SendKey": "SendKey",
"SMSManager API Docs": "SMSManager API Dokumente ",
"Gateway Type": "Gateway Type",
"Gateway Type": "Gateway Typ",
"SMSManager": "SMSManager",
"You can divide numbers with": "Du kannst Zahlen teilen mit",
"or": "oder",
"recurringInterval": "Intervall",
"Recurring": "Wiederkehrend",
"strategyManual": "Active/Inactive Manually",
"strategyManual": "Aktiv/Inaktiv Manuell",
"warningTimezone": "Es wird die Zeitzone des Servers genutzt",
"weekdayShortMon": "Mo",
"weekdayShortTue": "Di",
@ -640,12 +640,12 @@
"installing": "Installiere",
"uninstall": "Deinstallieren",
"uninstalling": "Deinstalliere",
"confirmUninstallPlugin": "Möchten Sie dieses Plugin wirklich deinstallieren?",
"confirmUninstallPlugin": "Möchtest du dieses Plugin wirklich deinstallieren?",
"notificationRegional": "Regional",
"Single Maintenance Window": "Einmaliges Wartungsfenster",
"dnsCacheDescription": "In einigen IPv6-Umgebungen funktioniert es möglicherweise nicht. Deaktivieren Sie es, wenn Sie auf Probleme stossen.",
"dnsCacheDescription": "In einigen IPv6-Umgebungen funktioniert es möglicherweise nicht. Deaktiviere es, wenn Probleme auftreten.",
"Maintenance Time Window of a Day": "Wartungszeitfenster eines Tages",
"Effective Date Range": "Gültigkeitsbereich",
"Effective Date Range": "Gültigkeitsbereich (Optional)",
"Schedule Maintenance": "Wartung planen",
"Date and Time": "Datum und Uhrzeit",
"DateTime Range": "Datums- und Zeitbereich",
@ -674,15 +674,15 @@
"Don't expire": "Nicht ablaufen",
"Add Another": "Hinzufügen",
"Key Added": "Schlüssel hinzugefügt",
"apiKeyAddedMsg": "Ihr API Schlüssel wurde hinzugefügt. Bitte notieren Sie Ihn, da er nicht erneut angezeigt wird.",
"apiKeyAddedMsg": "API Schlüssel wurde hinzugefügt. Bitte notiere den Schlüssel, da er nicht erneut angezeigt wird.",
"Add API Key": "API Schlüssel hinzufügen",
"No API Keys": "Kein API Schlüssel",
"apiKey-active": "Aktiv",
"apiKey-expired": "Abgelaufen",
"apiKey-inactive": "Inaktiv",
"Expires": "Läuft ab",
"disableAPIKeyMsg": "Sind Sie sicher, dass Sie diesen API Schlüssel deaktivieren möchten?",
"deleteAPIKeyMsg": "Sind Sie sicher, dass Sie diesen API Schlüssel löschen möchten?",
"disableAPIKeyMsg": "Bist du sicher, dass du diesen API Schlüssel deaktivieren willst?",
"deleteAPIKeyMsg": "Bist du sicher, dass du diesen API Schlüssel löschen willst?",
"Generate": "Generieren",
"infiniteRetention": "Für unendliche Speicherung auf 0 setzen.",
"dataRetentionTimeError": "Aufbewahrungsfrist muss grösser oder gleich 0 sein",
@ -691,8 +691,8 @@
"cloneOf": "Klon von {0}",
"wayToGetZohoCliqURL": "Wie eine Webhook URL erstellt werden kann, erfährst du {0}.",
"enableGRPCTls": "Senden von gRPC Anforderungen mit TLS Verbindung zulassen",
"grpcMethodDescription": "Der Name der Methode wird in das \"cammelCase \"-Format konvertiert (z.B. sayHello, check, etc.)",
"wayToGetKookGuildID": "Schalten Sie den „Entwicklermodus“ in den Kook-Einstellungen ein und klicken Sie mit der rechten Maustaste auf die Gilde, um ihre ID zu erhalten",
"grpcMethodDescription": "Der Name der Methode wird in das \"cammelCase\" Format konvertiert (z.B. sayHello, check, etc.)",
"wayToGetKookGuildID": "Schalte den „Entwicklermodus“ in den Kook-Einstellungen ein und klicke mit der rechten Maustaste auf die Gilde, um die ID zu erhalten",
"Guild ID": "Gilde ID",
"Lowcost": "Kostengünstig",
"high": "hoch",
@ -712,7 +712,7 @@
"pagertreeCritical": "Kritisch",
"pagertreeResolve": "Automatisch auflösen",
"pagertreeDoNothing": "Nichts tun",
"wayToGetPagerTreeIntegrationURL": "Nachdem Sie die Uptime Kuma Integration in PagerTree erstellt haben, kopieren Sie den Endpunkt. Siehe vollständige Details {0}",
"wayToGetPagerTreeIntegrationURL": "Nachdem du die Uptime Kuma Integration in PagerTree erstellt hast, kopiere den Endpunkt. Siehe details {0}",
"Server Address": "Serveradresse",
"Learn More": "Erfahre mehr",
"Edit Tag": "Tag editieren",
@ -724,12 +724,27 @@
"smseagleEncoding": "Als Unicode senden",
"smseaglePriority": "Nachrichtenpriorität (0-9, Standard = 0)",
"smseagleContact": "Telefonbuch Kontaktname(n)",
"confirmDeleteTagMsg": "Sind Sie sicher, dass Sie diesen Tag löschen möchten? Monitore, die mit diesem Tag verknüpft sind, werden nicht gelöscht.",
"wayToGetKookBotToken": "Erstellen Sie eine Anwendung und erhalten Sie Ihren Bot-Token unter {0}",
"confirmDeleteTagMsg": "Möchtest du dieses Tag wirklich löschen? Monitore, die mit diesem Tag verknüpft sind, werden nicht gelöscht.",
"wayToGetKookBotToken": "Erstelle eine Anwendung und erhalte den Bot-Token unter {0}",
"Strategy": "Strategie",
"Free Mobile User Identifier": "Kostenlose mobile Benutzerkennung",
"smseagleGroup": "Telefonbuch Gruppenname(n)",
"smseagleRecipient": "Empfänger (mehrere müssen durch Komma getrennt werden)",
"API Keys": "API Schlüssel",
"Continue": "Weiter"
"Continue": "Weiter",
"Add New Tag": "Neuen Tag hinzufügen",
"lunaseaTarget": "Ziel",
"lunaseaDeviceID": "Geräte-ID",
"lunaseaUserID": "Benutzer-ID",
"twilioAccountSID": "Account SID",
"twilioFromNumber": "Absender",
"twilioToNumber": "Empfänger",
"twilioAuthToken": "Auth Token",
"statusPageRefreshIn": "Aktualisierung in: {0}",
"sameAsServerTimezone": "Gleiche Zeitzone wie Server",
"startDateTime": "Start Datum/Uhrzeit",
"endDateTime": "Ende Datum/Uhrzeit",
"cronExpression": "Cron-Ausdruck",
"cronSchedule": "Zeitplan: ",
"invalidCronExpression": "Ungültiger Cron-Ausdruck: {0}"
}

@ -590,7 +590,7 @@
"atLeastOneMonitor": "Wähle mindestens einen Monitor",
"deleteMaintenanceMsg": "Möchtest du diese Wartung löschen?",
"Base URL": "Basis URL",
"goAlertInfo": "GoAlert ist eine Open-Source Applikation für Rufbereitschaftsplanung, automatische Eskalation und Benachrichtigung (z.B. SMS oder Telefonanrufe). Beauftragen Sie automatisch die richtige Person, auf die richtige Art und Weise und zum richtigen Zeitpunkt. {0}",
"goAlertInfo": "GoAlert ist eine Open-Source Applikation für Rufbereitschaftsplanung, automatische Eskalation und Benachrichtigung (z.B. SMS oder Telefonanrufe). Engagiere automatisch die richtige Person, auf die richtige Art und Weise und zum richtigen Zeitpunkt! {0}",
"goAlertIntegrationKeyInfo": "Bekommt einen generischen API Schlüssel in folgenden Format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\". Normalerweise entspricht dies dem Wert des Token aus der URL.",
"goAlert": "GoAlert",
"backupOutdatedWarning": "Veraltet: Da viele Funktionen hinzugefügt wurden und diese Sicherungsfunktion nicht mehr gepflegt wird, kann keine vollständige Sicherung erstellen oder wiederherstellen werden.",
@ -605,9 +605,9 @@
"or": "oder",
"recurringInterval": "Intervall",
"Recurring": "Wiederkehrend",
"Single Maintenance Window": "Einzigartiges Wartungsfenster",
"Single Maintenance Window": "Einmaliges Wartungsfenster",
"Maintenance Time Window of a Day": "Zeitfenster für die Wartung",
"Effective Date Range": "Bereich der Wirksamkeitsdaten",
"Effective Date Range": "Bereich der Wirksamkeitsdaten (Optional)",
"strategyManual": "Aktiv/Inaktiv Manuell",
"warningTimezone": "Es wird die Zeitzone des Servers verwendet",
"weekdayShortMon": "Mo",
@ -646,19 +646,19 @@
"Disable": "Deaktivieren",
"Custom Monitor Type": "Benutzerdefinierter Monitortyp",
"webhookAdditionalHeadersDesc": "Legt zusätzliche Header fest, die mit der Webhook gesendet wurden.",
"dnsCacheDescription": "In einigen IPv6-Umgebungen funktioniert es möglicherweise nicht. Deaktivieren Sie es, wenn Sie auf Probleme stoßen.",
"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öchten Sie dieses Plugin wirklich deinstallieren?",
"grpcMethodDescription": "Der Name der Methode wird in das \"cammelCase \"-Format konvertiert (z.B. sayHello, check, etc.)",
"confirmUninstallPlugin": "Möchtest du dieses Plugin wirklich deinstallieren?",
"grpcMethodDescription": "Der Name der Methode wird in das \"cammelCase\"-Format konvertiert (z.B. sayHello, check, etc.)",
"Passive Monitor Type": "Passiver Monitortyp",
"Specific Monitor Type": "Spezifischer Monitortyp",
"webhookAdditionalHeadersTitle": "Zusätzliche Header",
"Packet Size": "Paketgröße",
"IconUrl": "Symbol-URL",
"wayToGetZohoCliqURL": "Erfahren Sie, wie Sie eine Webhook-URL {0} erstellen.",
"wayToGetZohoCliqURL": "Wie eine Webhook URL erstellt werden kann, erfährst du {0}.",
"dataRetentionTimeError": "Aufbewahrungszeit muss 0 oder größer sein",
"infiniteRetention": "Für unendliche Aufbewahrung auf 0 setzen.",
"confirmDeleteTagMsg": "Möchten Sie dieses Tag wirklich löschen? Mit diesem Tag verknüpfte Monitore werden nicht gelöscht.",
"confirmDeleteTagMsg": "Möchtest du dieses Tag wirklich löschen? Monitore, die mit diesem Tag verknüpft sind, werden nicht gelöscht.",
"enableGRPCTls": "Senden von gRPC-Anforderungen mit TLS-Verbindung zulassen",
"ZohoCliq": "ZohoCliq",
"Monitor": "Überwachung | Monitore",
@ -668,8 +668,8 @@
"uninstall": "Deinstallieren",
"uninstalling": "Deinstallation",
"markdownSupported": "Markdown-Syntax unterstützt",
"wayToGetKookBotToken": "Erstellen Sie eine Anwendung und erhalten Sie Ihren Bot-Token unter {0}",
"wayToGetKookGuildID": "Schalten Sie den „Entwicklermodus“ in den Kook-Einstellungen ein und klicken Sie mit der rechten Maustaste auf die Gilde, um ihre ID zu erhalten",
"wayToGetKookBotToken": "Erstelle eine Anwendung und erhalte den Bot-Token unter {0}",
"wayToGetKookGuildID": "Schalte den „Entwicklermodus“ in den Kook-Einstellungen ein und klicke mit der rechten Maustaste auf die Gilde, um die ID zu erhalten",
"Guild ID": "Guild-ID",
"Free Mobile User Identifier": "Kostenlose mobile Benutzerkennung",
"Free Mobile API Key": "Kostenloser Mobile API-Schlüssel",
@ -687,7 +687,7 @@
"smseagleGroup": "Telefonbuch Gruppenname(n)",
"smseagleContact": "Telefonbuch Kontaktname(n)",
"smseagleRecipientType": "Empfängertyp",
"smseagleRecipient": "Empfänger (mehrere müssen mit Komma getrennt werden)",
"smseagleRecipient": "Empfänger (mehrere müssen durch Komma getrennt werden)",
"smseagleToken": "API-Zugriffstoken",
"smseagleUrl": "Ihre SMSEagle-Geräte-URL",
"Kook": "Kook",
@ -730,9 +730,24 @@
"telegramProtectContentDescription": "Die Bot-Nachrichten in Telegram sind gegen Weiterleitung und Speichern geschützt.",
"notificationRegional": "Regional",
"Key Added": "Schlüssel hinzugefügt",
"apiKeyAddedMsg": "Ihr API Schlüssel wurde hinzugefügt. Bitte notieren Sie Ihn, da er nicht erneut angezeigt wird.",
"apiKeyAddedMsg": "API Schlüssel wurde hinzugefügt. Bitte notiere den Schlüssel, da er nicht erneut angezeigt wird.",
"telegramMessageThreadID": "(Optional) Nachrichten Thread ID",
"telegramMessageThreadIDDescription": "Optionale eindeutige Kennung für den Ziel-Thread (Thema) des Forums; nur für Forum-Supergroups",
"telegramSendSilently": "Stumm Senden",
"telegramSendSilentlyDescription": "Sende die Nachricht stumm. Nutzer bekommen eine Benachrichtigung ohne Ton."
"telegramSendSilentlyDescription": "Sende die Nachricht stumm. Nutzer bekommen eine Benachrichtigung ohne Ton.",
"Add New Tag": "Neuen Tag hinzufügen",
"lunaseaDeviceID": "Geräte-ID",
"lunaseaTarget": "Ziel",
"lunaseaUserID": "Benutzer-ID",
"twilioAccountSID": "Account SID",
"twilioFromNumber": "Absender",
"twilioToNumber": "Empfänger",
"twilioAuthToken": "Auth Token",
"statusPageRefreshIn": "Aktualisierung in: {0}",
"sameAsServerTimezone": "Gleiche Zeitzone wie Server",
"startDateTime": "Start Datum/Uhrzeit",
"endDateTime": "Ende Datum/Uhrzeit",
"cronExpression": "Cron-Ausdruck",
"cronSchedule": "Zeitplan: ",
"invalidCronExpression": "Ungültiger Cron-Ausdruck: {0}"
}

@ -174,6 +174,7 @@
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
"statusPageNothing": "Nothing here, please add a group or a monitor.",
"statusPageRefreshIn": "Refresh in: {0}",
"No Services": "No Services",
"All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service",
@ -393,6 +394,12 @@
"backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.",
"Optional": "Optional",
"or": "or",
"sameAsServerTimezone": "Same as Server Timezone",
"startDateTime": "Start Date/Time",
"endDateTime": "End Date/Time",
"cronExpression": "Cron Expression",
"cronSchedule": "Schedule: ",
"invalidCronExpression": "Invalid Cron Expression: {0}",
"recurringInterval": "Interval",
"Recurring": "Recurring",
"strategyManual": "Active/Inactive Manually",
@ -428,7 +435,7 @@
"dnsCacheDescription": "It may be not working in some IPv6 environments, disable it if you encounter any issues.",
"Single Maintenance Window": "Single Maintenance Window",
"Maintenance Time Window of a Day": "Maintenance Time Window of a Day",
"Effective Date Range": "Effective Date Range",
"Effective Date Range": "Effective Date Range (Optional)",
"Schedule Maintenance": "Schedule Maintenance",
"Date and Time": "Date and Time",
"DateTime Range": "DateTime Range",
@ -706,5 +713,9 @@
"wayToGetPagerTreeIntegrationURL": "After creating the Uptime Kuma integration in PagerTree, copy the Endpoint. See full details {0}",
"lunaseaTarget": "Target",
"lunaseaDeviceID": "Device ID",
"lunaseaUserID": "User ID"
"lunaseaUserID": "User ID",
"twilioAccountSID": "Account SID",
"twilioAuthToken": "Auth Token",
"twilioFromNumber": "From Number",
"twilioToNumber": "To Number"
}

@ -303,7 +303,7 @@
"Maintenance": "Mantenimiento",
"General Monitor Type": "Monitor Tipo General",
"Specific Monitor Type": "Monitor Tipo Específico",
"Monitor": "Monitores",
"Monitor": "Monitor | Monitores",
"Resend Notification if Down X times consecutively": "Reenviar Notificación si Caído X veces consecutivamente",
"resendEveryXTimes": "Reenviar cada {0} veces",
"resendDisabled": "Reenvío deshabilitado",
@ -679,15 +679,15 @@
"serwersms": "SerwerSMS.pl",
"serwersmsAPIUser": "Nombre de usuario de API (inc. webapi_ prefix)",
"smseagleGroup": "Nombre(s) de grupo de Guía Telefónica",
"Unpin": "Quitar de destacados",
"Unpin": "Dejar de Fijar",
"Prefix Custom Message": "Prefijo personalizado",
"markdownSupported": "Soporta sintaxis Markdown",
"markdownSupported": "Sintaxis de Markdown soportada",
"Server Address": "Dirección del Servidor",
"Learn More": "Aprende Más",
"Pick a RR-Type...": "Seleccione un Tipo RR",
"onebotHttpAddress": "Dirección HTTP OneBot",
"SendKey": "Clave de Envío",
"octopushAPIKey": "\"Clave API\" de las credenciales HTTP API en el panel de control",
"octopushAPIKey": "\"Clave API\" desde credenciales API HTTP en panel de control",
"octopushLogin": "\"Inicio de Sesión\" a partir de las credenciales API HTTP en el panel de control",
"ntfy Topic": "Tema ntfy",
"Google Analytics ID": "ID Analíticas de Google",
@ -696,5 +696,57 @@
"Bark Endpoint": "Endpoint Bark",
"WebHookUrl": "WebHookUrl",
"High": "Alto",
"alertaApiEndpoint": "Endpoint API"
"alertaApiEndpoint": "Endpoint API",
"Body Encoding": "Codificación del cuerpo",
"Expiry date": "Fecha de expiración",
"Expiry": "Expiración",
"API Keys": "Claves API",
"Key Added": "Clave añadida",
"Add Another": "Añadir otro",
"Continue": "Continuar",
"Don't expire": "No caduca",
"apiKey-inactive": "Inactivo",
"apiKey-expired": "Expirado",
"apiKey-active": "Activo",
"No API Keys": "No hay claves API",
"Add API Key": "Añadir clave API",
"apiKeyAddedMsg": "Su clave API ha sido añadida. Anótala, ya que no se volverá a mostrar.",
"Clone": "Clonar",
"cloneOf": "Clon de {0}",
"pagertreeDoNothing": "No hacer nada",
"pagertreeResolve": "Resolución automática",
"pagertreeCritical": "Crítico",
"pagertreeHigh": "Alto",
"pagertreeMedium": "Medio",
"pagertreeLow": "Bajo",
"pagertreeSilent": "Silencio",
"pagertreeUrgency": "Urgencia",
"pagertreeIntegrationUrl": "URL de integración",
"lunaseaTarget": "Objetivo",
"wayToGetPagerTreeIntegrationURL": "Después de crear la integración Uptime Kuma en PagerTree, copie el Endpoint. Ver todos los detalles {0}",
"Generate": "Generar",
"deleteAPIKeyMsg": "¿Está seguro de que desea eliminar esta clave API?",
"telegramMessageThreadID": "(Opcional) ID del hilo de mensajes",
"telegramMessageThreadIDDescription": "Opcional Identificador único para el hilo de mensajes de destino (asunto) del foro; solo para supergrupos de foros",
"telegramProtectContent": "Proteger Forwarding/Saving",
"telegramProtectContentDescription": "Si se activa, los mensajes del bot en Telegram estarán protegidos contra el reenvío y el guardado.",
"notificationRegional": "Regional",
"Clone Monitor": "Clonar Monitor",
"telegramSendSilently": "Enviar en silencio",
"telegramSendSilentlyDescription": "Envía el mensaje en silencio. Los usuarios recibirán una notificación sin sonido.",
"Add New Tag": "Añadir nueva etiqueta",
"lunaseaUserID": "ID Usuario",
"lunaseaDeviceID": "ID Dispositivo",
"disableAPIKeyMsg": "¿Está seguro de que desea desactivar esta clave API?",
"Expires": "Expira",
"twilioAccountSID": "SID de Cuenta",
"twilioFromNumber": "Desde el numero",
"twilioToNumber": "Hasta el numero",
"startDateTime": "Fecha/Hora Inicio",
"sameAsServerTimezone": "Igual a Zona horaria del Servidor",
"endDateTime": "Fecha/Hora Fin",
"cronExpression": "Expresión Cron",
"cronSchedule": "Cronograma: ",
"invalidCronExpression": "Expresión Cron invalida:{0}",
"statusPageRefreshIn": "Reinicio en: {0}"
}

@ -1,34 +1,34 @@
{
"languageName": "Farsi",
"checkEverySecond": "بررسی هر {0} ثانیه.",
"retryCheckEverySecond": "تکرار مجدد هر {0} ثانیه.",
"retriesDescription": "حداکثر تعداد تکرار پیش از علامت گذاری وب‌سایت بعنوان خارج از دسترس و ارسال اطلاع‌رسانی.",
"languageName": "فارسی",
"checkEverySecond": "بررسی هر {0} ثانیه",
"retryCheckEverySecond": "تکرار مجدد هر {0} ثانیه",
"retriesDescription": "حداکثر تعداد تکرار پیش از علامت گذاری وب‌سایت بعنوان خارج از دسترس و ارسال اطلاع‌رسانی",
"ignoreTLSError": "بی‌خیال ارور TLS/SSL برای سایت‌های HTTPS",
"upsideDownModeDescription": "نتیجه وضعیت را برعکس کن، مثلا اگر سرویس در دسترس بود فرض کن که سرویس پایین است!",
"upsideDownModeDescription": "نتیجه وضعیت را برعکس کن، مثلا اگر سرویس در دسترس بود فرض کن که سرویس پایین است.",
"maxRedirectDescription": "حداکثر تعداد ریدایرکتی که سرویس پشتیبانی کند. برای اینکه ری‌دایرکت‌ها پشتیبانی نشوند، عدد 0 را وارد کنید.",
"acceptedStatusCodesDescription": "لطفا HTTP Status Code هایی که میخواهید به عنوان پاسخ موفقیت آمیز در نظر گرفته شود را انتخاب کنید.",
"passwordNotMatchMsg": "تکرار رمز عبور مطابقت ندارد!",
"passwordNotMatchMsg": "تکرار رمز عبور مطابقت ندارد.",
"notificationDescription": "برای اینکه سرویس اطلاع‌رسانی کار کند، آنرا به یکی از مانیتور‌ها متصل کنید.",
"keywordDescription": "در نتیجه درخواست (اهمیتی ندارد پاسخ JSON است یا HTML) بدنبال این کلمه بگرد (حساس به کوچک/بزرگ بودن حروف).",
"pauseDashboardHome": "متوقف شده",
"deleteMonitorMsg": "آیا از حذف این مانیتور مطمئن هستید؟",
"deleteNotificationMsg": "آیا مطمئن هستید که میخواهید این سرویس اطلاع‌رسانی را برای تمامی مانیتورها حذف کنید؟",
"resolverserverDescription": "سرویس CloudFlare به عنوان سرور پیش‌فرض استفاده می‌شود، شما میتوانید آنرا به هر سرور دیگری بعدا تغییر دهید.",
"rrtypeDescription": "لطفا نوع Resource Record را انتخاب کنید.",
"rrtypeDescription": "لطفا نوع Resource Record را انتخاب کنید",
"pauseMonitorMsg": "آیا مطمئن هستید که میخواهید این مانیتور را متوقف کنید ؟",
"enableDefaultNotificationDescription": "برای هر مانیتور جدید، این سرویس اطلاع‌رسانی به صورت پیش‌فرض فعال خواهد شد. البته که شما میتوانید به صورت دستی آنرا برای هر مانیتور به صورت جداگانه غیر فعال کنید.",
"clearEventsMsg": "آیا از اینکه تمامی تاریخچه رویداد‌های این مانیتور حذف شود مطمئن هستید؟",
"clearHeartbeatsMsg": "آیا از اینکه تاریخچه تمامی Heartbeat های این مانیتور حذف شود مطمئن هستید؟ ",
"clearHeartbeatsMsg": "آیا از اینکه تاریخچه تمامی ضربان قلب های این مانیتور حذف شود مطمئن هستید؟",
"confirmClearStatisticsMsg": "آیا از حذف تمامی آمار و ارقام مطمئن هستید؟",
"importHandleDescription": " اگر که میخواهید بیخیال مانیتورها و یا سرویس‌های اطلاع‌رسانی که با نام مشابه از قبل موجود هستند شوید، گزینه 'بی‌خیال موارد ..' را انتخاب کنید. توجه کنید که گزینه 'بازنویسی' تمامی موارد موجود با نام مشابه را از بین خواهد برد.",
"confirmImportMsg": "آیا از بازگردانی بک آپ مطمئن هستید؟ لطفا از اینکه نوع بازگردانی درستی را انتخاب کرده‌اید اطمینان حاصل کنید!",
"twoFAVerifyLabel": "لطفا جهت اطمینان از عملکرد احراز هویت دو مرحله‌ای توکن خود را وارد کنید!",
"tokenValidSettingsMsg": "توکن شما معتبر است، هم اکنون میتوانید احراز هویت دو مرحله‌ای را فعال کنید!",
"confirmEnableTwoFAMsg": " آیا از فعال سازی احراز هویت دو مرحله‌ای مطمئن هستید؟",
"importHandleDescription": "اگر که میخواهید بیخیال مانیتورها و یا سرویس‌های اطلاع‌رسانی که با نام مشابه از قبل موجود هستند شوید، گزینه 'بی‌خیال موارد ..' را انتخاب کنید. توجه کنید که گزینه 'بازنویسی' تمامی موارد موجود با نام مشابه را از بین خواهد برد.",
"confirmImportMsg": "آیا از بازگردانی بک آپ مطمئن هستید؟ لطفا از اینکه نوع بازگردانی درستی را انتخاب کرده‌اید اطمینان حاصل کنید.",
"twoFAVerifyLabel": "لطفا جهت اطمینان از عملکرد احراز هویت دو مرحله‌ای توکن خود را وارد کنید:",
"tokenValidSettingsMsg": "توکن شما معتبر است، هم اکنون میتوانید احراز هویت دو مرحله‌ای را فعال کنید.",
"confirmEnableTwoFAMsg": "آیا از فعال سازی احراز هویت دو مرحله‌ای مطمئن هستید؟",
"confirmDisableTwoFAMsg": "آیا از غیرفعال سازی احراز هویت دومرحله‌ای مطمئن هستید؟",
"Settings": "تنظیمات",
"Dashboard": "پیشخوان",
"New Update": "بروزرسانی جدید!",
"New Update": "بروزرسانی جدید",
"Language": "زبان",
"Appearance": "ظاهر",
"Theme": "پوسته",
@ -48,13 +48,13 @@
"Status": "وضعیت",
"DateTime": "تاریخ و زمان",
"Message": "پیام",
"No important events": "رخداد جدیدی موجود نیست.",
"No important events": "رخداد جدیدی موجود نیست",
"Resume": "ادامه",
"Edit": "ویرایش",
"Delete": "حذف",
"Current": "فعلی",
"Uptime": "آپتایم",
"Cert Exp.": "تاریخ انقضای SSL",
"Cert Exp.": "تاریخ انقضای SSL.",
"day": "روز",
"-day": "-روز",
"hour": "ساعت",
@ -76,7 +76,7 @@
"Accepted Status Codes": "وضعیت‌های (Status Code) های قابل قبول",
"Save": "ذخیره",
"Notifications": "اطلاع‌رسانی‌ها",
"Not available, please setup.": "هیچ موردی موجود نیست، اولین مورد را راه اندازی کنید!",
"Not available, please setup.": "هیچ موردی موجود نیست، اولین مورد را راه اندازی کنید.",
"Setup Notification": "راه اندازی اطلاع‌رسانی‌",
"Light": "روشن",
"Dark": "تاریک",
@ -87,8 +87,8 @@
"None": "هیچ کدام",
"Timezone": "موقعیت زمانی",
"Search Engine Visibility": "قابلیت دسترسی برای موتورهای جستجو",
"Allow indexing": "اجازه ایندکس شدن را بده.",
"Discourage search engines from indexing site": "به موتورهای جستجو اجازه ایندکس کردن این سامانه را نده.",
"Allow indexing": "اجازه ایندکس شدن در موتور های جستجو را بده",
"Discourage search engines from indexing site": "به موتورهای جستجو اجازه ایندکس کردن این سامانه را نده",
"Change Password": "تغییر رمزعبور",
"Current Password": "رمزعبور فعلی",
"New Password": "رمزعبور جدید",
@ -98,10 +98,10 @@
"Enable Auth": "فعال سازی تایید هویت",
"disableauth.message1": "آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?",
"disableauth.message2": "این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کرده‌اند</strong>، مانند Cloudflare Access.",
"Please use this option carefully!": "لطفا از این امکان با دقت استفاده کنید.",
"Please use this option carefully!": "لطفا از این امکان با دقت استفاده کنید!",
"Logout": "خروج",
"Leave": "منصرف شدم",
"I understand, please disable": "متوجه هستم، لطفا غیرفعال کنید!",
"I understand, please disable": "متوجه هستم، غیرفعال کن",
"Confirm": "تایید",
"Yes": "بلی",
"No": "خیر",
@ -126,12 +126,12 @@
"Import": "ورود اطلاعات",
"respTime": "زمان پاسخگویی (میلی‌ثانیه)",
"notAvailableShort": "ناموجود",
"Default enabled": "به صورت پیش‌فرض فعال باشد.",
"Apply on all existing monitors": "بر روی تمامی مانیتور‌های فعلی اعمال شود.",
"Default enabled": "به صورت پیش‌فرض فعال باشد",
"Apply on all existing monitors": "بر روی تمامی مانیتور‌های فعلی اعمال شود",
"Create": "ایجاد",
"Clear Data": "پاکسازی داده‌ها",
"Events": "رخداد‌ها",
"Heartbeats": "Heartbeats",
"Heartbeats": "ضربان قلب",
"Auto Get": "Auto Get",
"backupDescription": "شما میتوانید تمامی مانیتورها و تنظیمات اطلاع‌رسانی‌ها را در قالب یه فایل JSON دریافت کنید.",
"backupDescription2": "البته تاریخچه رخدادها دراین فایل قرار نخواهند داشت.",
@ -152,10 +152,10 @@
"Active": "فعال",
"Inactive": "غیرفعال",
"Token": "توکن",
"Show URI": "نمایش آدرس (URI) ",
"Show URI": "نمایش آدرس (URI)",
"Tags": "برچسب‌ها",
"Add New below or Select...": "یک مورد جدید اضافه کنید و یا از لیست انتخاب کنید…",
"Tag with this name already exist.": "یک برچسب با این «نام» از قبل وجود دارد",
"Tag with this name already exist.": "یک برچسب با این «نام» از قبل وجود دارد.",
"Tag with this value already exist.": "یک برچسب با این «مقدار» از قبل وجود دارد.",
"color": "رنگ",
"value (optional)": "مقدار (اختیاری)",
@ -167,13 +167,13 @@
"Indigo": "نیلی",
"Purple": "بنفش",
"Pink": "صورتی",
"Search...": "جستجو...",
"Search...": "جستجو",
"Avg. Ping": "متوسط پینگ",
"Avg. Response": "متوسط زمان پاسخ",
"Entry Page": "صفحه ورودی",
"statusPageNothing": "چیزی اینجا نیست، لطفا یک گروه و یا یک مانیتور اضافه کنید!",
"statusPageNothing": "چیزی اینجا نیست، لطفا یک گروه و یا یک مانیتور اضافه کنید.",
"No Services": "هیچ سرویسی موجود نیست",
"All Systems Operational": "تمامی سیستم‌ها عملیاتی هستند!",
"All Systems Operational": "تمامی سیستم‌ها عملیاتی هستند",
"Partially Degraded Service": "افت نسبی کیفیت سرویس",
"Degraded Service": "افت کامل کیفیت سرویس",
"Add Group": "اضافه کردن گروه",
@ -187,7 +187,7 @@
"One record": "یک مورد",
"Info": "اطلاعات",
"Powered by": "نیرو گرفته از",
"apprise": "Apprise (Support 50+ Notification services)",
"apprise": "Apprise (پشتیبانی از 50+ خدمات اعلان)",
"Monitor": "مانیتور | مانتیور ها",
"Help": "کمک",
"Game": "بازی",
@ -197,5 +197,526 @@
"statusMaintenance": "در دست تعمیر",
"Maintenance": "در حال تعمیر",
"General Monitor Type": "حالت مانیتور عمومی",
"markdownSupported": "شیوه نگارشی Markdown پشتیبانی می شود"
"markdownSupported": "شیوه نگارشی Markdown پشتیبانی می شود",
"Body Encoding": "انکودینگ محتوا",
"twilioFromNumber": "از شماره",
"twilioToNumber": "به شماره",
"Resend Notification if Down X times consecutively": "اگر X بار متوالی غیرفعال بود، مجددا اطلاع بده",
"successMessageExplanation": "پیام MQTT موفقیت آمیز به نظر نمیرسد",
"Create Incident": "یک حادثه را اطلاع دهید",
"Switch to Light Theme": "تغییر به حالت روشن",
"No monitors available.": "هیچ مانیتوری در دسترس نیست.",
"deleteProxyMsg": "آیا مطمئن هستید که میخواهید پروکسی را برای همه مانیتور ها غیرفعال کنید؟",
"enableProxyDescription": "این پروکسی تا زمانی که فعال نشود روی درخواست های مانیتور اثری نخواهد داشت. می‌توانید با توجه به وضعیت فعال‌سازی، پروکسی را از همه مانیتورها به طور موقت غیرفعال کنید.",
"supportTelegramChatID": "پشتیبانی از چت مستقیم / گروه / کانال",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "توکن دسترسی طولانی مدت (Long-Lived Access Token) را می توان با کلیک بر روی نام پروفایل خود (پایین سمت چپ) و اسکرول کردن به پایین و سپس روی Create Token ایجاد کرد. ",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "فهرستی از سرویس‌های اعلان را می‌توانید در هوم اسیستنت در قسمت «ابزارهای برنامه‌نویس > خدمات» برای «اعلان» جستجو کنید تا نام دستگاه/تلفن خود را پیدا کنید.",
"lastDay4": "چهارمین روز آخر ماه",
"dnsCacheDescription": "ممکن است در برخی از محیط های IPv6 کار نکند، اگر با مشکلی مواجه شدید آن را غیرفعال کنید.",
"Maintenance Time Window of a Day": "صفحه نگه داری در روز",
"Messaging API": "API پیام (Messaging API)",
"wayToGetLineChannelToken": "ابتدا به {0} دسترسی پیدا کنید، یک ارائه دهنده و کانال ایجاد کنید (API پیام)، سپس می توانید رمز توکن کانال و آیدی کاربری را از آیتم های منوی ذکر شده در بالا دریافت کنید.",
"aboutMattermostChannelName": "می‌توانید با وارد کردن نام کانال در قسمت «نام کانال»، کانال پیش‌فرضی را که وب هوک به آن پست می‌کند لغو کنید. این باید در تنظیمات Mattermost Webhook فعال شود. مثال: #other-channel",
"dnsPortDescription": "پورت سرور DNS. پیش فرض ۵۳. می توانید این عدد را در هر زمانی عوض کنید.",
"affectedStatusPages": "نمایش این پیام تعمیر و نگه داری در صفحات استاتوس انتخاب شده",
"octopushSMSSender": "نام فرستنده پیامک: 3-11 الفبای انگلیسی، حروف و فاصله (a-zA-Z0-9)",
"Lowcost": "کم هزینه",
"You can divide numbers with": "می توانید اعداد را با آن تقسیم کنید",
"goAlertInfo": "GoAlert یک برنامه اوپن سورس برای زمان‌بندی تماس، افزایش خودکار و اعلان‌ها (مانند پیامک یا تماس‌های صوتی) است. به طور خودکار شخص مناسب، راه درست و در زمان مناسب را درگیر کنید! {0}",
"API Keys": "کلید های API",
"Expiry": "انقضا",
"Expiry date": "انقضا در تاریخ",
"Don't expire": "بدون انقضا (منقضی نمی شود)",
"For safety, must use secret key": "برای امنیت، میببایستی از SecretKey استفاده کنید",
"promosmsTypeFlash": "SMS FLASH - پیام به طور خودکار در دستگاه گیرنده نشان داده می شود. فقط به گیرندگان لهستانی محدود می شود.",
"promosmsTypeFull": "SMS FULL - پیامک پریموم، می توانید از نام فرستنده خود استفاده کنید (ابتدا باید نام خود را ثبت کنید). قابل اعتماد برای هشدار.",
"matrixHomeserverURL": "URL هوم سرور (با http(s):// و پورت اختیاری)",
"matrixDesc1": "با مراجعه به بخش پیشرفته تنظیمات اتاق در کلاینت Matrix خود می توانید آیدی داخلی اتاق را بیابید. باید شبیه \"!QMdRCpUIfLwsfjxye6:home.server\" باشد.",
"wayToGetPagerDutyKey": "با رفتن به Service -> Service Directory -> (Select a Service) -> Integrations -> Add integration می توانید این مورد را دریافت کنید. در اینجا می توانید \"Events API V2\" را جستجو کنید. اطلاعات بیشتر در {0}",
"smseagleRecipientType": "نوع گیرنده",
"smseagleEncoding": "ارسال به صورت یونیکد",
"Leave blank to use a shared sender number.": "برای استفاده از شماره فرستنده مشترک، آن را خالی بگذارید.",
"onebotSafetyTips": "برای امنیت، میبایستی توکن دسترسی اضافه کنید",
"Custom Monitor Type": "نوع مانیتور سفارشی",
"apiKeyAddedMsg": "کلید API شما اضافه شده است. لطفاً آن را یادداشت کنید زیرا دیگر نمایش داده نخواهد شد.",
"deleteAPIKeyMsg": "آیا مطمئن هستید که می خواهید این کلید API را غیرفعال کنید؟",
"twilioAccountSID": "SID حساب",
"twilioAuthToken": "توکن اعتبارسنجی",
"appriseNotInstalled": "Apprise نصب نشده است. {0}",
"trustProxyDescription": "به هدرهای «X-Forwarded-*» اعتماد کن. اگر می‌خواهید IP مشتری صحیح را دریافت کنید و آپ‌تایم کومای شما پشت پروکسی مانند Nginx یا Apache قرار دارد، باید این گزینه را فعال کنید.",
"matrixDesc2": "اکیداً توصیه می‌شود که یک کاربر جدید ایجاد کنید و از رمز دسترسی کاربر Matrix خود استفاده نکنید زیرا امکان دسترسی کامل به حساب شما و تمام اتاق‌هایی را که به آنها ملحق شده‌اید می‌دهد. در عوض، یک کاربر جدید ایجاد کنید و فقط او را به اتاقی دعوت کنید که می‌خواهید اعلان را دریافت کنید. می‌توانید با اجرای {0} توکن دسترسی را دریافت کنید",
"Certificate Chain": "زنجیره گواهی (Certificate Chain)",
"telegramMessageThreadID": "(اختیاری) آیدی Thread پیام",
"telegramMessageThreadIDDescription": "(اختیاری) شناسه منحصر به فرد برای موضوع پیام هدف در انجمن. فقط برای سوپر گروه های انجمن",
"Channel Name": "نام کانال",
"auto acknowledged": "تصدیق خودکار",
"needPushEvery": "هر {0} ثانیه، URL زیر را صدا بزن.",
"pushOptionalParams": "پارامترهای اختیاری: {0}",
"Affected Monitors": "مانیتورهای تحت تأثیر",
"Pick Affected Monitors...": "انتخاب مانیتورهای تحت تأثیر…",
"Start of maintenance": "زمان شروع نگهداری",
"All Status Pages": "همه صفحات مشاهده وضعیت",
"Select status pages...": "انتخاب صفحه مشاهده وضعیت…",
"here": "اینجا",
"Required": "اجباری",
"Post URL": "URL بعدی",
"defaultNotificationName": "هشدار {notification} در ({number})",
"Add one": "اضافه کردن",
"Page Not Found": "صفحه درخواستی پیدا نشد",
"Reverse Proxy": "ریورس پروکسی",
"Backup": "پشتیبان گیری",
"API Key": "کلید API",
"Show update if available": "نمایش بروز رسانی اگر موجود بود",
"Check how to config it for WebSocket": "بررسی چگونگی پیکربندی برای وب سوکت",
"Steam Game Server": "سرور گیم استیم",
"Most likely causes:": "به احتمال زیاد بخاطر:",
"The resource is no longer available.": "منبع دیگر در دسترس نیست.",
"Docker Container": "کانتینر داکر",
"Container Name / ID": "نام / آیدی کانتینر",
"Docker Host": "هاست داکر",
"Docker Hosts": "هاست های داکر",
"Domain": "دامنه",
"Clone Monitor": "تکثیر",
"Clone": "تکثیر",
"cloneOf": "تکثیر {0}",
"Prefix Custom Message": "پیشوند پیام سفارشی",
"enableGRPCTls": "امکان ارسال درخواست gRPC با اتصال TLS",
"pushoversounds classical": "کلاسیک",
"smtpDkimSettings": "تنظیمات DKIM",
"aboutChannelName": "اگر می‌خواهید کانال وب هوک را دور بزنید، نام کانال را در قسمت {0} نام کانال وارد کنید. مثال: #other-channel",
"aboutKumaURL": "اگر قسمت URL آپ‌تایم کوما را خالی بگذارید، به طور پیش‌فرض به صفحه پروژه گیت هاب تبدیل می‌شود.",
"smtpDkimDesc": "لطفاً برای استفاده به Nodemailer DKIM {0} مراجعه کنید.",
"alertaApiEndpoint": "اند پوینت API",
"serwersmsAPIUser": "نام کاربری API (شامل پیشوند webapi_)",
"serwersmsAPIPassword": "رمز عبور API",
"serwersmsPhoneNumber": "شماره موبایل",
"serwersmsSenderName": "نام فرستنده پیامک (ثبت شده از طریق پورتال مشتری)",
"alertaRecoverState": "حالت ریکاور (Recover State)",
"smseagleToken": "توکن دسترسی API",
"Google Analytics ID": "آیدی گوگل آنالیتیکس",
"pagertreeLow": "کم",
"pagertreeMedium": "متوسط",
"pagertreeHigh": "زیاد",
"pagertreeCritical": "حساس - خیلی مهم",
"pagertreeIntegrationUrl": "URL یکپارچه سازی",
"pagertreeUrgency": "اهمیت",
"pagertreeSilent": "بی صدا",
"pagertreeResolve": "Resolve اتوماتیک",
"pagertreeDoNothing": "هیچ کاری نکن",
"wayToGetPagerTreeIntegrationURL": "پس از ایجاد ادغام آپ‌تایم کوما در PagerTree، اند پوینت را کپی کنید. مشاهده جزئیات کامل در {0}",
"telegramProtectContent": "محافظت از ارسال/ذخیره",
"telegramProtectContentDescription": "در صورت فعال بودن، پیام‌های ربات در تلگرام از ارسال و ذخیره محافظت می‌شوند.",
"wayToGetTelegramChatID": "برای مشاهده chat_id می توانید شناسه چت خود را با ارسال یک پیام به ربات و رفتن به این URL دریافت کنید:",
"YOUR BOT TOKEN HERE": "شناسه ربات خود را اینجا وارد کنید",
"chatIDNotFound": "شناسه چت یافت نشد. لطفا ابتدا به ربات پیام دهید",
"disableCloudflaredNoAuthMsg": "شما در حالت بدون احراز هویت هستید، رمز عبور در این حالت لازم نیست.",
"Trigger type:": "نوع راه اندازی:",
"DateTime Range": "محدوده تاریخ",
"loadingError": "نمی توان داده ها را دریافت کرد، لطفاً بعداً دوباره امتحان کنید.",
"High": "زیاد",
"Retry": "تلاش مجدد",
"Topic": "موضوع",
"Integration Key": "کلید یکپارچه سازی",
"Edit Tag": "ویرایش تگ",
"Server Address": "آدرس سرور",
"Learn More": "بیشتر بدانید",
"Customize": "شخصی سازی",
"Custom Footer": "فوتر اختصاصی",
"No Proxy": "بدون پروکسی",
"Authentication": "احراز هویت",
"steamApiKeyDescription": "برای مانیتورینگ یک سرور استیم،‌ شما نیاز به یک \"Steam Web-API key\" دارید. برای دریافت کلید میتوانید از اینجا اقدام کنید: ",
"No Monitors": "بدون مانیتور",
"Untitled Group": "دسته بنده نشده",
"Services": "سرویس ها",
"Discard": "دست کشیدن",
"Cancel": "انصراف",
"About": "درباره آپ‌تایم کوما",
"wayToGetCloudflaredURL": "(دریافت Cloudflared از {0})",
"cloudflareWebsite": "وب سایت کلادفلر",
"shrinkDatabaseDescription": "تریگر VACUUM برای SQLite. اگر دیتابیس شما بعد از 1.10.0 ایجاد شده باشد، AUTO_VACUUM قبلاً فعال شده است و لازم نیست این عمل انجام شود. (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.).",
"Message:": "پیام:",
"HTTP Headers": "هدر های HTTP",
"Bot Token": "توکن بات",
"SecretKey": "کلید محرمانه (SecretKey)",
"telegramSendSilently": "ارسال بی صدا",
"telegramSendSilentlyDescription": "پیام را بی صدا ارسال کن. در این حالت کاربران یک اعلان بدون صدا دریافت خواهند کرد.",
"install": "نصب",
"Icon URL": "URL آیکون",
"Steam API Key": "کلید API استیم",
"Security": "امنیت",
"light": "روشن",
"Query": "کوئری",
"Effective Date Range": "محدوده تاریخ مورد تاثیر (اختیاری)",
"statusPageRefreshIn": "بارگذاری مجدد در هر: {0}",
"Content Type": "نوع محتوا (Content Type)",
"Server URL": "آدرس سرور",
"Priority": "اهمیت",
"emojiCheatSheet": "چیت شیت ایموجی ها: {0}",
"Read more": "بیشتر بدانید",
"webhookJsonDesc": "{0} برای هر HTTP سرور جدیدی مانند Express.js مناسب است",
"Method": "متد",
"Headers": "هدر ها",
"PushUrl": "URL پوش",
"HeadersInvalidFormat": "هدر ریکوئست یک JSON درست نیست: ",
"BodyInvalidFormat": "هدر ریکوئست یک JSON درست نیست: ",
"Monitor History": "گزارش مانیتورینگ",
"clearDataOlderThan": "گزارشات مانیتورینگ را برای {0} روز نگه دار.",
"PasswordsDoNotMatch": "رمز عبور وارد شده درست نیست.",
"topic": "موضوع",
"topicExplanation": "موضوع MQTT برای مانیتور",
"successMessage": "پیام موفقیت آمیز",
"recent": "اخیر",
"Done": "انجام شده",
"Shrink Database": "فشرده سازی دیتابیس",
"Pick a RR-Type...": "یک تایپ RR انتخاب کنید…",
"Pick Accepted Status Codes...": "یک استاتوس کد قابل قبول انتخاب کنید…",
"Default": "پیش فرض",
"HTTP Options": "آپشن های HTTP",
"Title": "عنوان",
"Content": "محتوا",
"primary": "اولیه",
"dark": "تیره",
"Post": "اطلاع بده",
"Please input title and content": "لطفا یک عنوان و محتوا وارد کنید",
"Created": "ساخته شده در",
"Last Updated": "ویرایش شده در",
"Unpin": "برداشتن",
"Switch to Dark Theme": "تغییر به حالت تیره",
"Show Tags": "نمایش تگ ها",
"Hide Tags": "مخفی سازی تگ ها",
"Description": "توضحیات",
"Custom CSS": "CSS اختصاصی",
"deleteStatusPageMsg": "آیا بابت حذف این استاتوس پیچ مطمئن هستید؟",
"Proxies": "پروکسی ها",
"appriseInstalled": "Apprise نصب شده است.",
"Body": "متن",
"Start": "شروع",
"Stop": "توقف",
"Add New Status Page": "افزودن صفحه استاتوس جدید",
"Slug": "لینک",
"Accept characters:": "کاراکتر های مورد تایید:",
"startOrEndWithOnly": "شروع یا پایان فقط با {0}",
"No consecutive dashes": "بدون خط تیره متوالی",
"Next": "بعدی",
"The slug is already taken. Please choose another slug.": "این لینک قبلا گرفته شده است. لطفا لینک دیگری را انتخاب کنید.",
"New Status Page": "صفحه استاتوس جدید",
"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.": "اگر در حال حاضر از طریق تونل به کلادفلر متصل می شوید، ممکن است اتصال فعلی قطع شود. آیا مطمئن هستید که می خواهید کلادفلر را متوقف کنید؟ رمز عبور خود را برای تایید این دستور تایپ کنید.",
"Trust Proxy": "پروکسی مورد اعتماد",
"Other Software": "برنامه های دیگر",
"For example: nginx, Apache and Traefik.": "برای مثال: Nginx ،Apache و Traefik.",
"signedInDispDisabled": "اعتبارسنجی غیرفعال شده است.",
"RadiusCallingStationIdDescription": "شناسه دستگاه تماس گیرنده",
"Certificate Expiry Notification": "اطلاعیه انقضای گواهی",
"RadiusSecret": "کلید Radius",
"API Username": "نام کاربری API",
"Also check beta release": "همچنین برای نسخه های بتا نیز جستجو کن",
"Using a Reverse Proxy?": "استفاده از ریورس پروکسی؟",
"There might be a typing error in the address.": "ممکن است یک خطای تایپ در آدرس وجود داشته باشد.",
"What you can try:": "آنچه می توانید امتحان کنید:",
"Go back to the previous page.": "بازگشت به صفحه قبلی.",
"Coming Soon": "به زودی",
"Connection String": "رشته اتصال‌ (Connection String)",
"settingsCertificateExpiry": "انقضای گواهی TLS",
"certificationExpiryDescription": "مانیتور های HTTPS راه اندازی میشود زمانی که گواهی TLS منقضی شود در:",
"Retype the address.": "آدرس را دوباره تایپ کنید.",
"Setup Docker Host": "راه اندازی هاست داکر",
"Connection Type": "نوع اتصال",
"Docker Daemon": "Daemon داکر",
"deleteDockerHostMsg": "آیا مطمئن هستید که می خواهید این هاست داکر را برای همه مانیتورها حذف کنید؟",
"Workstation": "محل کار (Workstation)",
"Packet Size": "سایز پکت",
"wayToGetTelegramToken": "شما میتوانید توکن خود را از {0} دریافت کنید.",
"Chat ID": "آیدی چت",
"wayToGetLineNotifyToken": "می‌توانید یک توکن جهت دسترسی از {0} دریافت کنید",
"Examples": "مثال ها",
"Home Assistant URL": "URL هوم اسیستنت شما",
"Long-Lived Access Token": "توکن دسترسی طولانی مدت",
"Notification Service": "سرویس اطلاع رسانی",
"default: notify all devices": "پیش فرض: اطلاع به همه دستگاه ها",
"Automations can optionally be triggered in Home Assistant:": "اتوماسیون ها می توانند به صورت اختیاری در هوم اسیستنت فعال شوند:",
"Event type:": "نوع ایونت:",
"Event data:": "نوع دیتا:",
"Then choose an action, for example switch the scene to where an RGB light is red.": "سپس یک عمل را انتخاب کنید، برای مثال صحنه را به جایی که نور RGB قرمز است تغییر دهید.",
"Optional": "اختیاری",
"recurringInterval": "وقفه",
"Recurring": "مکرر",
"strategyManual": "فعال/غیرفعال سازی به صورت دستی",
"warningTimezone": "این از منطقه زمانی سرور استفاده می کند",
"weekdayShortMon": "دوشنبه",
"weekdayShortTue": "سه شنبه",
"weekdayShortWed": "چهارشنبه",
"weekdayShortThu": "پنجشنبه",
"weekdayShortFri": "جمعه",
"weekdayShortSat": "شنبه",
"weekdayShortSun": "یکشنبه",
"dayOfWeek": "روز های هفته",
"dayOfMonth": "روز های ماه",
"lastDay": "روز آخر",
"lastDay1": "روز آخر ماه",
"lastDay2": "دومین روز آخر ماه",
"lastDay3": "سومین روز آخر ماه",
"Enable": "فعال سازی",
"Single Maintenance Window": "تعمیر و نگه داری تک صفحه",
"Schedule Maintenance": "زمانبندی تعمیر و نگهداری",
"Date and Time": "زمان و تاریخ",
"plugin": "پلاگین | پلاگین ها",
"installing": "در حال نصب",
"uninstall": "حذف از نصب",
"uninstalling": "درحال حذف",
"confirmUninstallPlugin": "آیا مطمئن هستید که می خواهید این پلاگین را حذف از نصب کنید؟",
"notificationRegional": "منطقه ای",
"secureOptionNone": "None / STARTTLS (25, 587)",
"secureOptionTLS": "TLS (465)",
"Ignore TLS Error": "خطای TLS را نادیده بگیر",
"From Email": "از ایمیل",
"emailCustomSubject": "موضوع سفارشی",
"To Email": "به ایمیل",
"smtpBCC": "BCC",
"Discord Webhook URL": "URL وب هوک دیسکورد",
"Bot Display Name": "نام نمایشی ربات",
"Hello @everyone is...": "سلام {'@'} همه…",
"wayToGetTeamsURL": "می‌توانید نحوه ایجاد وب هوک را در {0} بیاموزید.",
"wayToGetZohoCliqURL": "می‌توانید نحوه ایجاد وب هوک را در {0} بیاموزید.",
"needSignalAPI": "شما باید یک Signal Client با REST API داشته باشید.",
"wayToCheckSignalURL": "برای مشاهده نحوه تنظیم آن می توانید این URL را بررسی کنید:",
"Number": "عدد",
"Recipients": "گیرندگان",
"Channel access token": "توکن دسترسی به کانال",
"Line Developers Console": "کنسول توسعه دهندگان لاین (Line Developers Console)",
"lineDevConsoleTo": "کنسول توسعه دهندگان لاین (Line Developers Console) - {0}",
"Basic Settings": "تنظیمات پایه",
"User ID": "آیدی کاربر",
"aboutIconURL": "می‌توانید پیوندی به یک عکس در \"URL آیکون \" ارائه دهید تا عکس نمایه پیش‌فرض را لغو کنید. اگر نماد Emoji تنظیم شده باشد، این مورد استفاده نخواهد شد.",
"dataRetentionTimeError": "دوره نگهداری باید 0 یا بیشتر باشد",
"wayToGetDiscordURL": "شما می توانید این را با رفتن به تنظیمات سرور -> ادغام -> مشاهده وب هوک -> وب هوک جدید (Settings -> Integrations -> View Webhooks -> New Webhook) دریافت کنید",
"infiniteRetention": "برای دوره بی نهایت 0 را وارد تنظیم کنید.",
"confirmDeleteTagMsg": "آیا مطمئن هستید که می خواهید این تگ را حذف کنید؟ مانیتورهای مرتبط با این تگ حذف نخواهند شد.",
"grpcMethodDescription": "نام روش تبدیل به فرمت cammelCase مانند sayHello، check و غیره.",
"deleteMaintenanceMsg": "آیا مطمئن هستید که می خواهید این تعمیر و نگهداری را حذف کنید؟",
"recurringIntervalMessage": "یکبار اجرا برای هر روز | یکبار اجرا در هر {0} روز",
"affectedMonitorsDescription": "مانیتورهایی را انتخاب کنید که تحت تأثیر تعمیر و نگهداری فعلی هستند",
"atLeastOneMonitor": "حداقل یک مانیتور مورد تاثیر را انتخاب کنید",
"octopushAPIKey": "\"کلید API\" از اعتبارنامه های HTTP API در کنترل پنل",
"octopushLogin": "\"ورود\" از اعتبار HTTP API در کنترل پنل",
"promosmsLogin": "نام ورود API",
"pushoversounds cashregister": "صندوق فروش",
"pushoversounds falling": "رها کردن",
"pushoversounds incoming": "ورودی",
"pushoversounds intermission": "وقفه",
"pushoversounds magic": "سحر آمیز",
"pushoversounds mechanical": "مکانیکی",
"pushoversounds pianobar": "پیانو بار",
"pushoversounds siren": "آژیر",
"pushoversounds spacealarm": "هشدار فضایی",
"pushoversounds gamelan": "گیم لن (Gamelan)",
"Current User": "کاربر فعلی",
"pushoversounds none": "بی صدا",
"pushoversounds tugboat": "قایق یدک کش",
"pushoversounds alien": "هشدار بیگانه (طولانی)",
"pushoversounds climb": "صعود (طولانی)",
"pushoversounds persistent": "پایدار (طولانی)",
"pushoversounds echo": "اکو (طولانی)",
"pushoversounds updown": "بالا پایین (طولانی)",
"pushoversounds vibrate": "فقط ویبره",
"pushyToken": "توکن دستگاه",
"GoogleChat": "Google Chat (فقط Google Workspace)",
"wayToGetKookBotToken": "یک برنامه ایجاد کنید و توکن ربات خود را از {0} دریافت کنید",
"User Key": "کلید کاربر",
"Message Title": "عنوان پیام",
"Notification Sound": "صدای اعلان",
"More info on:": "اطلاعات بیشتر در مورد: {0}",
"pushoverDesc1": "اولویت اضطراری (2) دارای وقفه پیش‌فرض 30 ثانیه بین تلاش‌های مجدد است و پس از 1 ساعت منقضی می‌شود.",
"pushoverDesc2": "اگر می‌خواهید اعلان‌ها را به دستگاه‌های مختلف ارسال کنید، قسمت دستگاه را پر کنید.",
"pushyAPIKey": "کلید Secret API",
"wayToGetKookGuildID": "«حالت توسعه‌دهنده» را در تنظیمات کوک روشن کنید و روی انجمن کلیک راست کنید تا شناسه آن را دریافت کنید",
"Guild ID": "گیلد آیدی (Guild ID)",
"SMS Type": "نوع پیامک",
"octopushTypePremium": "پرمیوم (سریع - پیشنهاد شده برای هشدار ها)",
"octopushTypeLowCost": "کم هزینه (آهسته - گاهی اوقات توسط اپراتور مسدود می شود)",
"checkPrice": "بررسی قیمت‌های {0} :",
"apiCredentials": "اطلاعات API",
"octopushLegacyHint": "آیا از نسخه قدیمی Octopush (1387-1400) استفاده می کنید یا از نسخه جدید؟",
"octopushPhoneNumber": "شماره تلفن (حالت بین المللی مانند 989121234567+) ",
"LunaSea Device ID": "شناسه دستگاه LunaSea",
"Apprise URL": "آدرس Apprise",
"Example:": "مثال: {0}",
"Read more:": "بیشتر بخوانید: {0}",
"Free Mobile User Identifier": "شناسه کاربری Free Mobile",
"Free Mobile API Key": "کلید API در Free Mobile",
"Enable TLS": "فعال کردن TLS",
"Proto Service Name": "نام Proto Service",
"Proto Method": "متد Proto",
"Proto Content": "محتوای Proto",
"Economy": "اقتصاد",
"high": "زیاد",
"SMSManager API Docs": "مستندات SMSManager API ",
"Gateway Type": "نوع Gateway",
"Base URL": "URL پایه",
"goAlertIntegrationKeyInfo": "کلید ادغام API عمومی را برای سرویس در این قالب دریافت کنید \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee\" معمولاً مقدار پارامتر توکن URL کپی شده است.",
"AccessKeyId": "آیدی AccessKey",
"PhoneNumbers": "شماره های موبایل",
"TemplateCode": "کد تمپلیت",
"Sms template must contain parameters: ": "قالب پیامک باید دارای پارامترهای زیر باشد: ",
"Bark Endpoint": "اند پوینت Bark",
"Bark Group": "گروه Bark",
"Bark Sound": "صدای Bark",
"WebHookUrl": "آدرس وب هوک",
"Device Token": "توکن دستگاه",
"Platform": "پلتفرم",
"Check octopush prices": "بررسی قیمت های octopush {0}.",
"SendKey": "کلید ارسال (SendKey)",
"SecretAccessKey": "کلید دسترسی مخفی (AccessKey Secret)",
"SignName": "نام امضا (SignName)",
"Android": "اندروید",
"Huawei": "هواوی",
"WeCom Bot Key": "کلید ربات WeCom",
"Setup Proxy": "تنظیم پروکسی",
"Proxy Protocol": "پروتکل پروکسی",
"Proxy Server": "پروتکل سرور",
"promosmsTypeEco": "SMS ECO - ارزان اما کند و اغلب بارگذاری شده است. فقط به گیرندگان لهستانی محدود می شود.",
"promosmsTypeSpeed": "SPEED SMS - بالاترین اولویت در سیستم. بسیار سریع و قابل اعتماد اما پرهزینه (حدود دو برابر قیمت SMS FULL).",
"promosmsPhoneNumber": "شماره تلفن (برای گیرنده لهستانی می توانید کدهای منطقه را نادیده بگیرید)",
"promosmsSMSSender": "نام فرستنده پیامک: نام از پیش ثبت شده یا یکی از پیش فرض ها: InfoSMS، SMS Info، MaxSMS، INFO، SMS",
"promosmsAllowLongSMS": "اجازه برای پیامک طولانی",
"Feishu WebHookUrl": "آدرس وب هوک Feishu",
"Internal Room Id": "آیدی اتاق داخلی",
"Uptime Kuma URL": "URL آپ‌تایم کوما",
"signalImportant": "مهم: شما نمی توانید گروه ها و اعداد را در گیرندگان ترکیب کنید!",
"aboutWebhooks": "اطلاعات بیشتر درباره وب هوک در: {0}",
"documentation": "مستندات",
"smtpDkimDomain": "نام دامنه",
"smtpDkimHashAlgo": "الگوریتم رمزگذاری (اختیاری)",
"smtpDkimheaderFieldNames": "کلیدهای هدر برای امضا (اختیاری)",
"smtpDkimskipFields": "کلیدهای هدر برای عدم امضا (اختیاری)",
"Integration URL": "URL یکپارچه سازی",
"smtpDkimKeySelector": "انتخابگر کلید (SecretKey)",
"smtpDkimPrivateKey": "کلید محرمانه (Private Key)",
"socket": "سوکت",
"do nothing": "هیچ کاری نکن",
"auto resolve": "حل خودکار",
"alertaEnvironment": "محیط",
"alertaApiKey": "کلید API",
"alertaAlertState": "وضعیت هشدار",
"smseagleTo": "شماره تلفن(ها)",
"smseagleGroup": "نام(های) گروه دفترچه تلفن",
"smseagleContact": "نام(های) تماس دفترچه تلفن",
"smseagleRecipient": "گیرنده(های) (چند مورد باید با کاما از هم جدا شوند)",
"smseagleUrl": "URL دستگاه SMSEagle شما",
"smseaglePriority": "اولویت پیام (0-9، پیش فرض = 0)",
"Recipient Number": "شماره گیرنده",
"From Name/Number": "از نام/شماره",
"Octopush API Version": "نسخه Octopush API",
"ntfy Topic": "موضوع ntfy",
"onebotHttpAddress": "آدرس HTTP OneBot",
"onebotMessageType": "نوع پیام OneBot",
"onebotGroupMessage": "گروه",
"onebotPrivateMessage": "خصوصی",
"onebotUserOrGroupId": "آیدی گروه/کاربر",
"PushDeer Key": "کلید PushDeer",
"wayToGetClickSendSMSToken": "می‌توانید نام کاربری و کلید API را از {0} دریافت کنید.",
"Continue": "ادامه",
"Add Another": "افزودن یکی دیگر",
"Key Added": "کلید API اضافه شد",
"Add API Key": "افزودن کلید API",
"No API Keys": "بدون کلید API",
"apiKey-active": "فعال",
"apiKey-expired": "منقضی شده",
"apiKey-inactive": "غیرفعال",
"Expires": "انقضا",
"disableAPIKeyMsg": "آیا مطمئن هستید که می خواهید این کلید API را غیرفعال کنید؟",
"Generate": "ایجاد یک کلید API جدید",
"lunaseaTarget": "هدف",
"lunaseaDeviceID": "آيدی دستگاه",
"lunaseaUserID": "آیدی کاربر",
"Auto resolve or acknowledged": "حل خودکار یا اعلام اطلاع یافته (Auto resolve or acknowledged)",
"Legacy Octopush-DM": "(Legacy Octopush-DM)",
"smtpCC": "ارسال نسخه به",
"promosmsPassword": "رمز عبور API",
"pushoversounds pushover": "Pushover (پیش‌فرض)",
"pushoversounds bike": "دوچرخه",
"pushoversounds bugle": "بوق",
"pushoversounds cosmic": "کیهانی",
"resendEveryXTimes": "پیام را هر {0} بار دوباره ارسال کن",
"resendDisabled": "ارسال مجدد غیرفعال است",
"Push URL": "URL پوش",
"Schedule maintenance": "زمانبندی نگهداری (غیرفعال سازی دستی)",
"webhookFormDataDesc": "{multipart} برای PHP مناسب است. آرایه JSON نیاز است تا به این شکل باز شود {decodeFunction}",
"webhookAdditionalHeadersTitle": "هدر اضافی",
"webhookAdditionalHeadersDesc": "تنظیم هدر های اضافی که نیاز است با وب هوک ارسال شود.",
"Webhook URL": "آدرس وب هوک",
"Application Token": "توکن اپلیکیشن",
"Style": "حالت ها",
"info": "اطلاعات",
"warning": "هشدار",
"danger": "خطر",
"error": "خطا",
"critical": "اهمیت ویژه",
"HTTP Basic Auth": "حالت پایه احراز هویت (HTTP Basic Auth)",
"RadiusSecretDescription": "اشتراک گذاری Secret بین کاربر و سرور",
"RadiusCalledStationId": "نام Station Id",
"RadiusCalledStationIdDescription": "شناسه دستگاه فراخوانی شده",
"RadiusCallingStationId": "آیدی ایستگاه تماس (Calling Station Id)",
"tcp": "TCP / HTTP",
"Frontend Version": "نسخه فرانت اند",
"Frontend Version do not match backend version!": "نسخه فرانت اند با نسخه بک اند مطابقت ندارد!",
"backupOutdatedWarning": "منسوخ شده: از آنجایی که بسیاری از ویژگی ها اضافه شده اند و این ویژگی پشتیبان گیری کمی حفظ نشده است، نمی تواند یک نسخه پشتیبان کامل ایجاد یا بازیابی شود.",
"backupRecommend": "لطفاً مستقیماً از Volume یا پوشه داده (./data/) نسخه پشتیبان تهیه کنید.",
"No Maintenance": "بدون تعمیر و نگهداری",
"pauseMaintenanceMsg": "آیا مطمئن هستید که می خواهید توقف کنید؟",
"maintenanceStatus-under-maintenance": "تحت تعمیر و نگهداری",
"maintenanceStatus-inactive": "غیرفعال",
"maintenanceStatus-scheduled": "برنامه ریزی شده",
"maintenanceStatus-ended": "پایان یافته",
"maintenanceStatus-unknown": "ناشناخته",
"Display Timezone": "منطقه زمانی برای نمایش",
"Server Timezone": "منطقه زمانی در سرور",
"statusPageMaintenanceEndDate": "پایان",
"IconUrl": "URL آیکون",
"Enable DNS Cache": "فعال سازی کش DNS",
"Access Token": "توکن دسترسی",
"smtp": "ایمیل (SMTP)",
"Device": "دستگاه",
"Proxy server has authentication": "پروکسی سرور دارای احراز هویت",
"Add New Tag": "اضافه کردن تگ جدید",
"Custom": "غیره",
"default": "پیش فرض",
"enabled": "فعال",
"setAsDefault": "ذخیره به عنوان پیش فرض",
"proxyDescription": "پروکسی برای راه اندازی این مانیتور اجباری است.",
"setAsDefaultProxyDescription": "این پروکسی به طور پیش فرض برای مانیتورهای جدید فعال می شود. همچنان می توانید پروکسی را به طور جداگانه برای هر مانیتور غیرفعال کنید.",
"Valid": "درست",
"Invalid": "نادرست",
"User": "کاربر",
"Installed": "نصب شده",
"Not installed": "نصب نشده",
"Running": "در حال اجرا",
"Not running": "اجرا نشده",
"Remove Token": "حذف توکن",
"Please read": "لطفا بخوانید",
"Subject:": "موضوع:",
"Valid To:": "معتبر تا:",
"Days Remaining:": "روز های باقی مانده:",
"Fingerprint:": "اثرانگشت (Fingerprint):",
"No status pages": "بدون صفحات استاتوس",
"Domain Name Expiry Notification": "اعلان انقضای نام دامنه",
"Issuer:": "صادرکننده:",
"Date Created": "ایجاد شده در",
"Footer Text": "متن فوتر",
"Show Powered By": "نمایش قدرت گرفته از آپ‌تایم کوما",
"Domain Names": "نام دامنه ها",
"Proxy": "پروکسی",
"signedInDisp": "وارد شده به عنوان {0}",
"or": "یا",
"Disable": "غیرفعال سازی",
"endpoint": "نقطه پایانی",
"Status:": "وضعیت: {0}",
"Strategy": "استراتژی",
"Icon Emoji": "ایموجی آیکون",
"sameAsServerTimezone": "مشابه با منطقه زمانی سرور",
"startDateTime": "ساعت/روز شروع",
"endDateTime": "ساعت/روز پایان",
"cronSchedule": "برنامه زمانی: ",
"invalidCronExpression": "حالت کرون نامعتبر است: {0}",
"cronExpression": "حالت کرون"
}

@ -73,11 +73,11 @@
"Delete": "Supprimer",
"Current": "Actuellement",
"Uptime": "Disponibilité",
"Cert Exp.": "Expiration SSL.",
"Cert Exp.": "Expiration SSL",
"day": "jour | jours",
"-day": "-jour",
"hour": "heure",
"-hour": "-heure",
"-hour": "heures",
"Response": "Temps de réponse",
"Ping": "Ping",
"Monitor Type": "Type de sonde",
@ -658,7 +658,7 @@
"dnsCacheDescription": "Il peut ne pas fonctionner dans certains environnements IPv6, désactivez-le si vous rencontrez des problèmes.",
"Single Maintenance Window": "Créneau de maintenance unique",
"Maintenance Time Window of a Day": "Créneau de la maintenance",
"Effective Date Range": "Plage de dates d'effet",
"Effective Date Range": "Plage de dates d'effet (facultatif)",
"Schedule Maintenance": "Créer une maintenance",
"Date and Time": "Date et heure",
"DateTime Range": "Plage de dates et d'heures",
@ -734,5 +734,20 @@
"pagertreeDoNothing": "Ne fais rien",
"pagertreeIntegrationUrl": "URL d'intégration",
"pagertreeCritical": "Critique",
"wayToGetPagerTreeIntegrationURL": "Après avoir créé l'intégration Uptime Kuma dans PagerTree, copiez le fichier Endpoint. Voir tous les détails {0}"
"wayToGetPagerTreeIntegrationURL": "Après avoir créé l'intégration Uptime Kuma dans PagerTree, copiez le fichier Endpoint. Voir tous les détails {0}",
"lunaseaDeviceID": "Identifiant de l'appareil",
"lunaseaUserID": "Identifiant de l'utilisateur",
"Add New Tag": "Ajouter une étiquette",
"lunaseaTarget": "Cible",
"statusPageRefreshIn": "Actualisation dans: {0}",
"twilioFromNumber": "Du Nombre",
"twilioToNumber": "Au Nombre",
"twilioAccountSID": "ID du compte",
"twilioAuthToken": "Jeton d'authentification",
"sameAsServerTimezone": "Identique au fuseau horaire du serveur",
"startDateTime": "Date/heure de début",
"endDateTime": "Date/heure de fin",
"cronExpression": "Expression cron",
"cronSchedule": "Calendrier: ",
"invalidCronExpression": "Expression Cron non valide : {0}"
}

@ -510,5 +510,7 @@
"Android": "Android",
"Huawei": "Huawei",
"Device Token": "デバイストークン",
"recurringIntervalMessage": "毎日1回実行する{0} 日に1回実行する"
"recurringIntervalMessage": "毎日1回実行する{0} 日に1回実行する",
"Add New Tag": "新しいタグを追加",
"statusPageMaintenanceEndDate": "終了日"
}

@ -0,0 +1,21 @@
{
"Dashboard": "დაფა",
"Help": "დახმარება",
"New Update": "განახლება",
"Language": "ენა",
"Appearance": "ვიზუალი",
"Theme": "სტილი",
"Game": "თამაში",
"Version": "ვერსია",
"Quick Stats": "თვალის გადავლება",
"Up": "მაღლა",
"Pending": "მოლოდინი",
"languageName": "Georgian",
"Settings": "კონფიგურაცია",
"General": "ძირითადი",
"Check Update On GitHub": "GitHub_ზე განახლების შემოწმება",
"List": "სია",
"Add": "დამატება",
"Add New Monitor": "ახალი მონიტორის დამატება",
"Down": "დაბლა"
}

@ -208,7 +208,7 @@
"smtpBCC": "숨은 참조",
"discord": "Discord",
"Discord Webhook URL": "Discord 웹훅 URL",
"wayToGetDiscordURL": "서버 설정 -> 연동 -> 웹후크 보기 -> 새 웹후크에서 얻을 수 있어요",
"wayToGetDiscordURL": "서버 설정 -> 연동 -> 웹훅 보기 -> 새 웹훅 에서 얻을 수 있어요",
"Bot Display Name": "표시 이름",
"Prefix Custom Message": "접두사 메시지",
"Hello @everyone is...": "{'@'}everyone 서버 상태 알림이에요…",
@ -660,7 +660,7 @@
"Disable": "비활성화",
"Single Maintenance Window": "단일 점검",
"Maintenance Time Window of a Day": "점검 시간",
"Effective Date Range": "유효 날짜 범위",
"Effective Date Range": "유효 날짜 범위 (옵션)",
"Schedule Maintenance": "점검 예약하기",
"Date and Time": "날짜 및 시간",
"DateTime Range": "날짜 시간 범위",
@ -691,5 +691,62 @@
"webhookAdditionalHeadersTitle": "추가 헤더",
"webhookAdditionalHeadersDesc": "웹훅과 함께 전송될 추가 헤더를 설정해요.",
"HTTP Headers": "HTTP 헤더",
"Trust Proxy": "프록시 신뢰"
"Trust Proxy": "프록시 신뢰",
"API Keys": "API 키",
"markdownSupported": "Markdown 문법이 지원됨",
"telegramMessageThreadID": "(선택) 메시지 스레드 ID",
"Clone": "복제",
"cloneOf": "{0}의 복제본",
"Clone Monitor": "모니터링 복제",
"telegramProtectContent": "포워딩/저장 보호",
"telegramProtectContentDescription": "활성화 할경우 텔레그램 봇 메시지는 포워딩 및 저장으로부터 보호됩니다.",
"telegramSendSilentlyDescription": "조용히 메시지를 보냅니다. 사용자들은 무음으로 알림을 받습니다.",
"telegramSendSilently": "무음 알림",
"Add New Tag": "태그 추가",
"Edit Tag": "태그 수정",
"Server Address": "서버 주소",
"Learn More": "자세히 알아보기",
"Continue": "계속",
"Key Added": "키 추가됨",
"No API Keys": "API 키 없음",
"disableAPIKeyMsg": "이 API키를 정말로 비활성화하시겠습니까?",
"deleteAPIKeyMsg": "이 API키를 정말로 삭제하시겠습니까?",
"Generate": "생성",
"Body Encoding": "Body 인코딩",
"Expiry": "만료",
"Expiry date": "만료 날짜",
"Don't expire": "만료되지 않음",
"notificationRegional": "지역별",
"Google Analytics ID": "Google Analytics ID",
"Add API Key": "API 키 추가",
"apiKeyAddedMsg": "API 키가 추가되었습니다. 다시 표시되지 않을 것이므로 메모해 두세요.",
"pagertreeCritical": "긴급",
"apiKey-active": "사용 가능",
"lunaseaUserID": "사용자 ID",
"apiKey-expired": "만료됨",
"Expires": "만료일",
"twilioAuthToken": "인증 토큰",
"twilioFromNumber": "번호에서",
"twilioToNumber": "번호에서",
"twilioAccountSID": "계정 SID",
"pagertreeUrgency": "긴급",
"sameAsServerTimezone": "서버 시간대로 설정하기",
"startDateTime": "시작 시간",
"endDateTime": "종료 시간",
"cronExpression": "Cron 값",
"cronSchedule": "스케줄: ",
"invalidCronExpression": "알수없는 Cron 값입니다: {0}",
"Add Another": "다른 항목 추가",
"apiKey-inactive": "비활성화",
"pagertreeIntegrationUrl": "Integration 링크",
"pagertreeLow": "낮음",
"pagertreeMedium": "중간",
"pagertreeHigh": "높음",
"pagertreeResolve": "자동으로 해결하기",
"pagertreeDoNothing": "아무것도 하지 않음",
"wayToGetPagerTreeIntegrationURL": "PagerTree에서 Uptime Kuma 통합을 생성한 후 Endpoint를 복사합니다. 전체 세부 정보 보기 {0}",
"lunaseaTarget": "대상",
"lunaseaDeviceID": "기기 ID",
"statusPageRefreshIn": "{0} 후 새로고침",
"telegramMessageThreadIDDescription": "포럼의 대상 메시지 쓰레드(주제)에 대한 선택적 고유 식별인, 포럼 관리자 그룹에만 해당"
}

@ -696,5 +696,12 @@
"markdownSupported": "Markdown syntax ondersteund",
"Resend Notification if Down X times consecutively": "Melding x keer opnieuw sturen als monitor offline is",
"loadingError": "Kan de data niet ophalen, probeer het later opnieuw.",
"smseagleContact": "Telefoonboek contact namen"
"smseagleContact": "Telefoonboek contact namen",
"apiKey-active": "Actief",
"apiKey-expired": "Verlopen",
"pagertreeLow": "Laag",
"pagertreeHigh": "Hoog",
"Clone": "Dupliceer",
"cloneOf": "Duplicaat van {0}",
"Add New Tag": "Voeg nieuw label toe"
}

@ -727,5 +727,21 @@
"Key Added": "Klucz dodany",
"pagertreeDoNothing": "Nie rób nic",
"wayToGetPagerTreeIntegrationURL": "Po utworzeniu integracji Uptime Kuma w PagerTree, należy skopiować Endpoint. Zobacz pełne szczegóły {0}",
"notificationRegional": "Regionalne"
"notificationRegional": "Regionalne",
"twilioFromNumber": "Z numeru",
"twilioToNumber": "Do numeru",
"lunaseaTarget": "Cel",
"twilioAccountSID": "SID konta",
"twilioAuthToken": "Token autoryzacyjny",
"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",
"telegramProtectContent": "Ochrona przekazywania/zapisywania",
"telegramProtectContentDescription": "Jeśli włączona, wiadomości bota w Telegramie będą chronione przed przekazywaniem i zapisywaniem.",
"telegramSendSilently": "Wyślij po cichu",
"telegramSendSilentlyDescription": "Wysyła wiadomość po cichu. Użytkownicy otrzymają powiadomienie bez dźwięku.",
"statusPageRefreshIn": "Odświeżenie w ciągu: {0}",
"lunaseaDeviceID": "ID urządzenia",
"lunaseaUserID": "ID użytkownika",
"Add New Tag": "Dodaj nowy tag"
}

@ -8,7 +8,7 @@
"acceptedStatusCodesDescription": "Выберите коды статусов для определения доступности сервиса.",
"passwordNotMatchMsg": "Повтор пароля не совпадает.",
"notificationDescription": "Привяжите уведомления к мониторам.",
"keywordDescription": "Поиск слова в чистом HTML или в JSON-ответе (чувствительно к регистру)",
"keywordDescription": "Поиск слова в чистом HTML или в JSON-ответе (чувствительно к регистру).",
"pauseDashboardHome": "Пауза",
"deleteMonitorMsg": "Вы действительно хотите удалить данный монитор?",
"deleteNotificationMsg": "Вы действительно хотите удалить это уведомление для всех мониторов?",
@ -45,9 +45,9 @@
"Uptime": "Аптайм",
"Cert Exp.": "Сертификат истекает.",
"day": "день | дней",
"-day": " дней",
"-day": "-дней",
"hour": "час",
"-hour": " часа",
"-hour": "-часа",
"Response": "Ответ",
"Ping": "Пинг",
"Monitor Type": "Тип монитора",
@ -124,12 +124,12 @@
"Also apply to existing monitors": "Применить к существующим мониторам",
"Export": "Экспорт",
"Import": "Импорт",
"backupDescription": "Вы можете сохранить резервную копию всех мониторов и уведомлений в виде JSON-файла",
"backupDescription2": "P.S. История и события сохранены не будут",
"backupDescription3": "Важные данные, такие как токены уведомлений, добавляются при экспорте, поэтому храните файлы в безопасном месте",
"backupDescription": "Вы можете сохранить резервную копию всех мониторов и уведомлений в виде JSON-файла.",
"backupDescription2": "Важно: история и события сохранены не будут.",
"backupDescription3": "Важные данные, такие как токены уведомлений, добавляются при экспорте, поэтому храните файлы в безопасном месте.",
"alertNoFile": "Выберите файл для импорта.",
"alertWrongFileType": "Выберите JSON-файл.",
"twoFAVerifyLabel": "Пожалуйста, введите свой токен, чтобы проверить работу 2FA",
"twoFAVerifyLabel": "Пожалуйста, введите свой токен, чтобы проверить работу 2FA:",
"tokenValidSettingsMsg": "Токен действителен! Теперь вы можете сохранить настройки 2FA.",
"confirmEnableTwoFAMsg": "Вы действительно хотите включить 2FA?",
"confirmDisableTwoFAMsg": "Вы действительно хотите выключить 2FA?",
@ -441,14 +441,14 @@
"Accept characters:": "Принимаемые символы:",
"startOrEndWithOnly": "Начинается или кончается только {0}",
"No consecutive dashes": "Без последовательных тире",
"The slug is already taken. Please choose another slug.": "The slug is already taken. Please choose another slug.",
"The slug is already taken. Please choose another slug.": "Слово уже занято. Пожалуйста, выберите другой вариант.",
"Page Not Found": "Страница не найдена",
"wayToGetCloudflaredURL": "(Скачать cloudflared с {0})",
"cloudflareWebsite": "Cloudflare Website",
"cloudflareWebsite": "Веб-сайт Cloudflare",
"Message:": "Сообщение:",
"Don't know how to get the token? Please read the guide:": "Don't know how to get the token? Please read the guide:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.",
"HTTP Headers": "HTTP заголовки",
"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",
"Trust Proxy": "Доверять прокси",
"Other Software": "Другое программное обеспечение",
"For example: nginx, Apache and Traefik.": "К примеру: nginx, Apache и Traefik.",
@ -463,13 +463,13 @@
"Proxy": "Прокси",
"Date Created": "Дата создания",
"HomeAssistant": "Home Assistant",
"onebotHttpAddress": "OneBot HTTP Address",
"onebotMessageType": "OneBot Message Type",
"onebotHttpAddress": "HTTP-адрес OneBot",
"onebotMessageType": "Тип сообщения OneBot",
"onebotGroupMessage": "Группа",
"onebotPrivateMessage": "Private",
"onebotUserOrGroupId": "ID группы или пользователя",
"onebotSafetyTips": "В целях безопасности необходимо установить токен доступа",
"PushDeer Key": "PushDeer Key",
"PushDeer Key": "ключ PushDeer",
"Footer Text": "Текст нижнего колонтитула",
"Show Powered By": "Показывать на чем создано",
"Domain Names": "Доменные имена",
@ -488,40 +488,40 @@
"From Name/Number": "Имя/номер отправителя",
"Leave blank to use a shared sender number.": "Оставьте пустым, чтобы использовать общий номер отправителя.",
"Octopush API Version": "Версия API Octopush",
"Legacy Octopush-DM": "Legacy Octopush-DM",
"endpoint": "endpoint",
"Legacy Octopush-DM": "устаревший Octopush-DM",
"endpoint": "конечная точка",
"octopushAPIKey": "\"API key\" из учетных данных HTTP API в панели управления",
"octopushLogin": "\"Login\" из учетных данных HTTP API в панели управления",
"promosmsLogin": "Логин API",
"promosmsPassword": "Пароль API",
"pushoversounds pushover": "Pushover (default)",
"pushoversounds bike": "Bike",
"pushoversounds bugle": "Bugle",
"pushoversounds cashregister": "Cash Register",
"pushoversounds pushover": "Pushover (по умолчанию)",
"pushoversounds bike": "Велосипед",
"pushoversounds bugle": "Горн",
"pushoversounds cashregister": "Кассовый аппарат",
"pushoversounds classical": "Classical",
"pushoversounds cosmic": "Cosmic",
"pushoversounds falling": "Falling",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Incoming",
"pushoversounds intermission": "Intermission",
"pushoversounds magic": "Magic",
"pushoversounds mechanical": "Mechanical",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Siren",
"pushoversounds spacealarm": "Space Alarm",
"pushoversounds tugboat": "Tug Boat",
"pushoversounds alien": "Alien Alarm (long)",
"pushoversounds climb": "Climb (long)",
"pushoversounds persistent": "Persistent (long)",
"pushoversounds echo": "Pushover Echo (long)",
"pushoversounds updown": "Up Down (long)",
"pushoversounds vibrate": "Vibrate Only",
"pushoversounds none": "None (silent)",
"pushyAPIKey": "Secret API Key",
"pushoversounds cosmic": "Космический",
"pushoversounds falling": "Падающий",
"pushoversounds gamelan": "Гамелан",
"pushoversounds incoming": "Входящий",
"pushoversounds intermission": "Антракт",
"pushoversounds magic": "Магия",
"pushoversounds mechanical": "Механический",
"pushoversounds pianobar": "Пиано-бар",
"pushoversounds siren": "Сирена",
"pushoversounds spacealarm": "Космическая сигнализация",
"pushoversounds tugboat": "Буксирное судно",
"pushoversounds alien": "Инопланетная тревога (долгое)",
"pushoversounds climb": "Подъем (долгое)",
"pushoversounds persistent": "Стойкий (долгое)",
"pushoversounds echo": "Pushover Эхо (долгое)",
"pushoversounds updown": "Вверх вниз (долгое)",
"pushoversounds vibrate": "Только вибрация",
"pushoversounds none": "Нет (тихо)",
"pushyAPIKey": "Секретный ключ API",
"pushyToken": "Токен устройства",
"Using a Reverse Proxy?": "Используете обратный прокси?",
"Check how to config it for WebSocket": "Проверьте, как настроить его для WebSocket",
"Steam Game Server": "Steam Game Server",
"Steam Game Server": "Игровой сервер Steam",
"Most likely causes:": "Наиболее вероятные причины:",
"The resource is no longer available.": "Ресурс больше не доступен.",
"There might be a typing error in the address.": "В адресе может быть опечатка.",
@ -536,24 +536,24 @@
"certificationExpiryDescription": "HTTPS Мониторы инициируют уведомление, когда срок действия сертификата TLS истечет:",
"Setup Docker Host": "Настроить Docker Host",
"Connection Type": "Тип соединения",
"Docker Daemon": "Docker Daemon",
"deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?",
"socket": "Socket",
"Docker Daemon": "Демон Docker",
"deleteDockerHostMsg": "Вы уверены, что хотите удалить этот узел docker для всех мониторов?",
"socket": "Сокет",
"tcp": "TCP / HTTP",
"Docker Container": "Docker контейнер",
"Container Name / ID": "Название контейнера / ID",
"Docker Host": "Docker Host",
"Docker Hosts": "Docker Hosts",
"ntfy Topic": "ntfy Topic",
"Docker Host": "Хост Docker",
"Docker Hosts": "Хосты Docker",
"ntfy Topic": "тема ntfy",
"Domain": "Домен",
"Workstation": "Workstation",
"Workstation": "Рабочая станция",
"disableCloudflaredNoAuthMsg": "Вы находитесь в режиме без авторизации, пароль не требуется.",
"trustProxyDescription": "Доверять заголовкам 'X-Forwarded-*'. Если вы хотите получить правильный IP-адрес клиента, а ваш Uptime Kuma находится под Nginx или Apache, вам следует включить этот параметр.",
"wayToGetLineNotifyToken": "Вы можете получить токен доступа в {0}",
"Examples": "Примеры",
"Home Assistant URL": "Home Assistant URL",
"Home Assistant URL": "URL-адрес Home Assistant",
"Long-Lived Access Token": "Токен доступа с длительным сроком службы",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Токен доступа с длительным сроком действия можно создать, нажав на имя вашего профиля (внизу слева) и прокрутив его вниз, затем нажмите Создать токен. ",
"Notification Service": "Служба уведомлений",
"default: notify all devices": "по стандарту: уведомлять все устройства",
"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 в разделе \"Инструменты разработчика > Службы\", выполнив поиск по слову \"уведомление\", чтобы найти название вашего устройства/телефона.",
@ -565,7 +565,7 @@
"Frontend Version": "Версия интерфейса",
"Frontend Version do not match backend version!": "Версия интерфейса не соответствует версии серверной части!",
"Base URL": "Базовый URL",
"goAlertInfo": "GoAlert is a An open source application for on-call scheduling, automated escalations and notifications (like SMS or voice calls). Automatically engage the right person, the right way, and at the right time! {0}",
"goAlertInfo": "GoAlert — это приложение с открытым исходным кодом для составления расписания вызовов, автоматической эскалации и уведомлений (например, SMS или голосовых звонков). Автоматически привлекайте нужного человека, нужным способом и в нужное время! {0}",
"goAlertIntegrationKeyInfo": "Получить общий ключ интеграции API для сервиса в этом формате \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" обычно значение параметра токена скопированного URL.",
"goAlert": "GoAlert",
"backupOutdatedWarning": "Устарело: поскольку добавлено множество функций, а эта функция резервного копирования немного не поддерживается, она не может создать или восстановить полную резервную копию.",
@ -669,17 +669,17 @@
"smseagle": "SMSEagle",
"Google Analytics ID": "ID Google Аналитики",
"wayToGetZohoCliqURL": "Вы можете узнать как создать webhook URL тут {0}.",
"Effective Date Range": "Даты действия",
"Effective Date Range": "Даты действия (Опционально)",
"wayToGetKookGuildID": "Включите \"Режим разработчика\" в настройках Kook, а затем нажмите правой кнопкой по гильдии чтобы скопировать её ID",
"Enable TLS": "Включить TLS",
"Integration Key": "Ключ интеграции",
"Integration URL": "URL интеграции",
"do nothing": "ничего не делать",
"smseagleTo": "Номер(а) телефона",
"smseagleGroup": "Имена групп в телефонной книжке",
"smseagleGroup": "Название(я) групп телефонной книги",
"smseagleContact": "Имена контактов из телефонной книжки",
"smseagleRecipientType": "Тип получателя",
"smseagleRecipient": "Получатель (через запятую, если несколько)",
"smseagleRecipient": "Получатель(я) (через запятую, если необходимо указать несколько)",
"smseagleToken": "Токен доступа API",
"smseagleUrl": "URL вашего SMSEagle устройства",
"smseagleEncoding": "Отправить в юникоде",
@ -687,9 +687,9 @@
"Server Address": "Адрес сервера",
"Learn More": "Узнать больше",
"topicExplanation": "MQTT топик для мониторинга",
"Guild ID": "Guild ID",
"Guild ID": "Идентификатор гильдии",
"Kook": "Kook",
"wayToGetKookBotToken": "Создайте приложение и получите токен вашего бота тут {0}.",
"wayToGetKookBotToken": "Создайте приложение и получите токен бота по адресу {0}",
"Resend Notification if Down X times consecutively": "Повторная отправка уведомления при падении несколько раз",
"telegramProtectContent": "Запретить пересылку/сохранение",
"telegramProtectContentDescription": "Если включено, сообщения бота в Telegram будут запрещены для пересылки и сохранения.",
@ -699,5 +699,64 @@
"Clone Monitor": "Копия",
"Clone": "Копия",
"cloneOf": "Копия {0}",
"notificationRegional": "Региональный"
"notificationRegional": "Региональный",
"Add New Tag": "Добавить тег",
"Body Encoding": "Тип содержимого запроса.(JSON or XML)",
"Strategy": "Стратегия",
"Free Mobile User Identifier": "Бесплатный идентификатор мобильного пользователя",
"Auto resolve or acknowledged": "Автоматическое разрешение или подтверждение",
"auto acknowledged": "автоматическое подтверждение",
"auto resolve": "автоматическое разрешение",
"API Keys": "Ключи API",
"Expiry": "Истекает",
"Expiry date": "Дата окончания действия",
"Don't expire": "Не истекает",
"Continue": "Продолжать",
"Add Another": "Добавьте еще один",
"Key Added": "Ключ добавлен",
"Add API Key": "Добавить ключ API",
"No API Keys": "Нет API ключей",
"apiKey-active": "Активный",
"apiKey-expired": "Истёк",
"apiKey-inactive": "Неактивный",
"Expires": "Истекает",
"disableAPIKeyMsg": "Вы уверены, что хотите отключить этот ключ?",
"Generate": "Сгенерировать",
"pagertreeResolve": "Автоматическое разрешение",
"pagertreeDoNothing": "ничего не делать",
"lunaseaTarget": "Цель",
"lunaseaDeviceID": "Идентификатор устройства",
"lunaseaUserID": "Идентификатор пользователя",
"Lowcost": "Низкая стоимость",
"pagertreeIntegrationUrl": "URL-адрес интеграции",
"pagertreeUrgency": "Срочность",
"pagertreeSilent": "Тихий",
"pagertreeLow": "Низкий",
"pagertreeMedium": "Средний",
"pagertreeHigh": "Высокий",
"pagertreeCritical": "Критический",
"high": "высокий",
"promosmsAllowLongSMS": "Разрешить длинные SMS-сообщения",
"Economy": "Экономия",
"wayToGetPagerDutyKey": "Вы можете получить это, перейдя в службу -> Каталог служб -> (Выберите службу) -> Интеграции -> Добавить интеграцию. Здесь вы можете выполнить поиск по \"Events API V2\". Дополнительная информация {0}",
"apiKeyAddedMsg": "Ваш API ключ был добавлен. Пожалуйста, запишите это, так как оно больше не будет показан.",
"deleteAPIKeyMsg": "Вы уверены, что хотите удалить этот ключ API?",
"wayToGetPagerTreeIntegrationURL": "После создания интеграции Uptime Kuma в PagerTree, скопируйте конечную точку. Смотрите полную информацию {0}",
"telegramMessageThreadIDDescription": "Необязательный уникальный идентификатор для цепочки сообщений (темы) форума; только для форумов-супергрупп",
"grpcMethodDescription": "Название метода - преобразовать в формат cammelCase, такой как sayHello, check и т.д.",
"Proto Service Name": "название службы Proto",
"Proto Method": "Метод Proto",
"Proto Content": "Содержание Proto",
"telegramMessageThreadID": "(Необязательно) ID цепочки сообщений",
"statusPageRefreshIn": "Обновлять каждые: {0}",
"twilioAccountSID": "SID учетной записи",
"twilioAuthToken": "Токен авторизации",
"twilioFromNumber": "С номера",
"twilioToNumber": "На номер",
"sameAsServerTimezone": "Аналогично часовому поясу сервера",
"startDateTime": "Начальная дата и время",
"endDateTime": "Конечная дата и время",
"cronExpression": "Выражение для Cron",
"cronSchedule": "Расписание: ",
"invalidCronExpression": "Неверное выражение Cron: {0}"
}

@ -0,0 +1,195 @@
{
"Settings": "Nastavenia",
"Help": "Nápoveda",
"New Update": "Nová aktualizácia",
"Language": "Jazyk",
"Appearance": "Vzhľad",
"Theme": "Téma",
"General": "Základné",
"Primary Base URL": "Základná URL",
"Version": "Verzia",
"List": "Zoznam",
"Add": "Pridať",
"Add New Monitor": "Pridať nové Sledovanie",
"Quick Stats": "Rýchly prehľad",
"Pending": "Čaká sa",
"statusMaintenance": "Údržba",
"Maintenance": "Údržba",
"General Monitor Type": "Základný typ Sledovania",
"Passive Monitor Type": "Pasívny typ Sledovania",
"Specific Monitor Type": "Špecifický typ Sledovania",
"pauseDashboardHome": "Pauza",
"Pause": "Pauza",
"Status": "Stav",
"Message": "Správa",
"No important events": "Žiadne dôležité udalosti",
"Edit": "Upraviť",
"Delete": "Odstrániť",
"Current": "Aktuálne",
"Cert Exp.": "Platnosť cert.",
"day": "deň | dni",
"hour": "hodina",
"Response": "Odpoveď",
"Ping": "Ping",
"Keyword": "Kľúčové slovo",
"Friendly Name": "Názov",
"Port": "Port",
"Retries": "Opakovania",
"Resend Notification if Down X times consecutively": "Poslať oznámenie znovu, ak je nedostupné X-krát za sebou",
"Advanced": "Pokročilé",
"checkEverySecond": "Skontrolovať každých {0} sekúnd",
"retryCheckEverySecond": "Zopakovať každých {0} sekúnd",
"resendEveryXTimes": "Znovu poslať každých {0} krát",
"resendDisabled": "Opakované odoslanie vypnuté",
"ignoreTLSError": "Ignorovať TLS/SSL chyby pre HTTPS stránky",
"upsideDownModeDescription": "Obrátiť stav. Pokiaľ je služba dostupná, zobrazuje sa ako NEDOSTUPNÁ.",
"Upside Down Mode": "Obrátený režim",
"Max. Redirects": "Max. počet presmerovaní",
"Accepted Status Codes": "Akceptované stavové kódy",
"Push URL": "Push URL",
"Save": "Uložiť",
"Notifications": "Notifikácie",
"Not available, please setup.": "Nedostupné, prosím nastavte.",
"Setup Notification": "Nastavenie notifikácií",
"Dark": "Tmavá",
"Light": "Svetlá",
"Auto": "Automaticky",
"Normal": "Normálna",
"Bottom": "Dole",
"None": "Žiadne",
"Timezone": "Časová zóna",
"languageName": "Slovenčina",
"Dashboard": "Dashboard",
"Check Update On GitHub": "Skontrolovať aktualizáciu na GitHub-e",
"Up": "Dostupné",
"Down": "Nedostupné",
"Unknown": "Neznáme",
"markdownSupported": "Podpora Markdown syntaxu",
"Name": "Názov",
"DateTime": "Dátum a čas",
"Resume": "Pokračovať",
"Uptime": "Doba prevádzky",
"Monitor": "Sledovanie | Sledovania",
"-day": "-dní",
"-hour": "-hodín",
"Monitor Type": "Typ Sledovania",
"URL": "URL",
"Hostname": "Adresa",
"Heartbeat Interval": "Heartbeat Interval",
"Heartbeat Retry Interval": "Interval opakovania pre Heartbeat",
"retriesDescription": "Maximálny počet opakovaní pred tým, ako je služba označená ako nedostupná a je zaslaná notifikácia",
"maxRedirectDescription": "Maximálny počet presmerovaní. Hodnota 0 vypne presmerovania.",
"needPushEvery": "Tuto adresu by ste mali volať každých {0} sekúnd.",
"pushOptionalParams": "Voliteľné parametre: {0}",
"Theme - Heartbeat Bar": "Téma - Heartbeat riadok",
"Game": "Hra",
"Search Engine Visibility": "Viditeľnosť vyhľadávačmi",
"Allow indexing": "Povoliť indexovanie",
"Change Password": "Zmeniť heslo",
"Current Password": "Aktuálne heslo",
"New Password": "Nové heslo",
"Repeat New Password": "Zopakovať nové heslo",
"Update Password": "Aktualizovať heslo",
"Disable Auth": "Vypnúť autentifikáciu",
"Enable Auth": "Zapnúť autentifikáciu",
"Please use this option carefully!": "Túto možnosť používajte opatrne!",
"Logout": "Odhlásiť sa",
"Leave": "Odísť",
"I understand, please disable": "Rozumiem, vypnite to",
"Yes": "Áno",
"No": "Nie",
"Username": "Používateľské meno",
"Password": "Heslo",
"Login": "Prihlásiť sa",
"No Monitors, please": "Žiadne sledovanie, prosím",
"add one": "pridať jeden",
"Notification Type": "Typ notifikácie",
"Email": "E-mail",
"Test": "Test",
"Certificate Info": "Informácie o certifikáte",
"Resolver Server": "DNS server",
"Last Result": "Posledný výsledok",
"Repeat Password": "Zopakovať heslo",
"Import Backup": "Importovať zálohu",
"Export Backup": "Exportovať zálohu",
"Export": "Exportovať",
"Import": "Importovať",
"respTime": "Čas odozvy (ms)",
"notAvailableShort": "Nie je číslo",
"Default enabled": "Predvolene povolené",
"Create": "Vytvoriť",
"Clear Data": "Vyčistiť dáta",
"Events": "Udalosti",
"Heartbeats": "Odpovede",
"Auto Get": "Získať automaticky",
"Schedule maintenance": "Naplánovať údržbu",
"Affected Monitors": "Dotknuté sledovania",
"Pick Affected Monitors...": "Vybrať dotknuté sledovania…",
"Start of maintenance": "Začiatok údržby",
"All Status Pages": "Všetky stavové stránky",
"Select status pages...": "Vybrať stránky stavu…",
"alertNoFile": "Vyberte súbor na import.",
"alertWrongFileType": "Vyberte súbor JSON.",
"Clear all statistics": "Vymazať všetky štatistiky",
"Skip existing": "Preskočiť existujúce",
"Overwrite": "Prepísať",
"Options": "Možnosti",
"Keep both": "Ponechať obe",
"Setup 2FA": "Nastavenie 2FA",
"Disable 2FA": "Zakázať 2FA",
"2FA Settings": "Nastavenia 2FA",
"Two Factor Authentication": "Dvojfaktorová autentifikácia",
"Inactive": "Neaktívne",
"Token": "Token",
"Show URI": "Zobraziť URI",
"Tags": "Značky",
"Add New below or Select...": "Pridať novú nižšie alebo vybrať…",
"Tag with this value already exist.": "Značka s touto hodnotou už existuje.",
"color": "Farba",
"value (optional)": "hodnota (voliteľné)",
"Gray": "Šedá",
"Red": "Červená",
"Orange": "Oranžová",
"Green": "Zelená",
"Indigo": "Indigo",
"Purple": "Fialová",
"Pink": "Ružová",
"Custom": "Vlastná",
"Avg. Ping": "Priemerný ping",
"Avg. Response": "Priemerný čas odpovede",
"Entry Page": "Vstupná stránka",
"No Services": "Žiadne služby",
"All Systems Operational": "Všetky systémy funkčné",
"Partially Degraded Service": "Čiastočne zhoršená služba",
"Degraded Service": "Degradovaná služba",
"Add Group": "Pridať skupinu",
"Add a monitor": "Pridať sledovanie",
"Edit Status Page": "Upraviť stavovú stránku",
"Go to Dashboard": "Prejdite na informačný panel",
"Status Page": "Stavová stránka",
"Status Pages": "Stavové stránky",
"defaultNotificationName": "Moje {notification} upozornenie ({number})",
"here": "tu",
"Required": "Povinné",
"Post URL": "Post URL",
"Content Type": "Druh obsahu",
"webhookJsonDesc": "{0} je vhodný pre všetky moderné servery HTTP, ako napríklad Express.js",
"webhookFormDataDesc": "{multipart} je dobré pre PHP. JSON bude potrebné analyzovať pomocou {decodeFunction}",
"Generate": "Generovať",
"Discourage search engines from indexing site": "Odradiť vyhľadávacie nástroje od indexovania stránky",
"disableauth.message1": "Ste si istý, že chcete <strong>vypnúť autentifikáciu</strong>?",
"disableauth.message2": "Je navrhnutý pre scenáre, <strong>kde máte v úmysle implementovať autentifikáciu treťou stranou</strong> pred Uptime Kuma, ako je Cloudflare Access, Authelia alebo iné autentifikačné mechanizmy.",
"Confirm": "Potvrdiť",
"Remember me": "Zapamätať si ma",
"Resource Record Type": "Typ záznamu",
"Create your admin account": "Vytvorte si účet administrátora",
"Apply on all existing monitors": "Aplikujte na všetky existujúce sledovania",
"Verify Token": "Overiť token",
"Enable 2FA": "Povoliť 2FA",
"Active": "Aktívne",
"Add New Tag": "Pridať novú značku",
"Tag with this name already exist.": "Značka s týmto názvom už existuje.",
"Blue": "Modrá",
"Search...": "Hľadať…",
"statusPageNothing": "Nič tu nie je, pridajte skupinu alebo sledovanie."
}

@ -4,7 +4,7 @@
"retryCheckEverySecond": "ลองใหม่ทุก {0} วินาที",
"retriesDescription": "จำนวนครั้งสูงสุดที่จะลองก่อนบริการถูกระบุว่าไม่สามารถใช้งานได้และส่งการแจ้งเตือน",
"ignoreTLSError": "ไม่สนใจข้อผิดพลาด TLS/SSL สำหรับเว็บไซต์ HTTPS",
"upsideDownModeDescription": "กลับด้านสถานะ เช่น ถ้าบริการสามารถใช้งานได้จะถูกเปลี่ยนเป็นใช้งานไม่ได้",
"upsideDownModeDescription": "สลับสถานะ เช่น ถ้าบริการสามารถใช้งานได้จะถูกเปลี่ยนเป็นใช้งานไม่ได้",
"maxRedirectDescription": "จำนวนครั้งสูงสุดที่จะเปลี่ยนเส้นทาง, ตั้งเป็น 0 เพื่อปิดการเปลี่ยนเส้นทาง",
"acceptedStatusCodesDescription": "เลือกรหัสสถานะที่ถือว่าการตอบกลับสำเร็จ",
"passwordNotMatchMsg": "รหัสผ่านไม่ตรงกัน",
@ -30,7 +30,7 @@
"Dashboard": "แผงควบคุม",
"New Update": "อัพเดทใหม่",
"Language": "ภาษา",
"Appearance": "รูปร่าง",
"Appearance": "หน้าตา",
"Theme": "หน้าตา",
"General": "ทั่วไป",
"Primary Base URL": "URL หลัก",
@ -73,7 +73,7 @@
"Retries": "จำนวนครั้งที่จะลองใหม่",
"Heartbeat Retry Interval": "ระยะห่างระหว่างการทดสอบใหม่หลังจากไม่สำเร็จ",
"Advanced": "ขั้นสูง",
"Upside Down Mode": "โหมดกลับด้าน",
"Upside Down Mode": "โหมดสลับ",
"Max. Redirects": "จำนวนการเปลี่ยนเส้นทางสูงสุด",
"Accepted Status Codes": "รหัสสถานะที่ยอมรับ",
"Push URL": "URL เป้าหมาย",

@ -648,7 +648,7 @@
"dnsCacheDescription": "Bazı IPv6 ortamlarında çalışmıyor olabilir, herhangi bir sorunla karşılaşırsanız devre dışı bırakın.",
"Single Maintenance Window": "Tek Seferlik Bakım",
"Maintenance Time Window of a Day": "Bür Günlük Bakım",
"Effective Date Range": "Bakim Tarih Aralığı",
"Effective Date Range": "Geçerlilik Tarihi Aralığı (Opsiyonel)",
"Schedule Maintenance": "Bakım Planla",
"Date and Time": "Tarih ve Saat",
"DateTime Range": "Tarih ve Saat Aralığı",
@ -734,5 +734,20 @@
"pagertreeDoNothing": "Hiçbir şey yapma",
"wayToGetPagerTreeIntegrationURL": "PagerTree'de Uptime Kuma entegrasyonunu oluşturduktan sonra Endpoint'i kopyalayın. Tüm ayrıntıları görün {0}",
"pagertreeIntegrationUrl": "Entegrasyon URL",
"pagertreeResolve": "Otomatik Çöz"
"pagertreeResolve": "Otomatik Çöz",
"lunaseaTarget": "Hedef",
"Add New Tag": "Yeni Etiket Ekle",
"lunaseaDeviceID": "Cihaz ID",
"lunaseaUserID": "Kullanıcı ID",
"statusPageRefreshIn": "{0} içinde yenilenecek",
"twilioAuthToken": "Kimlik Doğrulama Jetonu",
"twilioFromNumber": "Gönderen Numara",
"twilioToNumber": "Alıcı Numara",
"twilioAccountSID": "Hesap ID",
"sameAsServerTimezone": "Sunucu Saat Dilimi ile aynı",
"startDateTime": "Başlangıç Tarihi/Saati",
"endDateTime": "Bitiş Tarihi/Saati",
"cronExpression": "Cron İfadesi",
"cronSchedule": "Zamanlama: ",
"invalidCronExpression": "Geçersiz Cron İfadesi: {0}"
}

@ -740,5 +740,9 @@
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Довготривалий токен доступу можна створити, натиснувши на ім'я вашого профілю (внизу ліворуч), прокрутивши його донизу і натиснувши кнопку Створити токен. ",
"high": "високий",
"Disable": "Вимкнути",
"Resend Notification if Down X times consecutively": "Повторно надіслати сповіщення, якщо було падіння X разів поспіль"
"Resend Notification if Down X times consecutively": "Повторно надіслати сповіщення, якщо було падіння X разів поспіль",
"lunaseaTarget": "Ціль",
"Add New Tag": "Додати новий тег",
"lunaseaDeviceID": "ID пристрою",
"lunaseaUserID": "ID користувача"
}

@ -270,7 +270,7 @@
"No Monitors": "کوئی مانیٹر نہیں",
"Cancel": "منسوخ کریں",
"Powered by": "کی طرف سے طاقت",
"Custom CSS": "حسب ضرورت سی ایس ایس",
"Custom CSS": "اپنی مرضی کے مطابق سی ایس ایس",
"deleteProxyMsg": "کیا آپ واقعی اس پراکسی کو تمام مانیٹر کے لیے حذف کرنا چاہتے ہیں؟",
"enableProxyDescription": "یہ پراکسی مانیٹر کی درخواستوں پر اس وقت تک اثر نہیں کرے گی جب تک کہ اسے فعال نہ کیا جائے۔ آپ ایکٹیویشن اسٹیٹس کے ذریعے تمام مانیٹرس سے پراکسی کو عارضی طور پر غیر فعال کر سکتے ہیں۔",
"setAsDefaultProxyDescription": "یہ پراکسی نئے مانیٹرز کے لیے بطور ڈیفالٹ فعال ہو جائے گی۔ آپ اب بھی ہر مانیٹر کے لیے الگ الگ پراکسی کو غیر فعال کر سکتے ہیں۔",
@ -409,7 +409,7 @@
"maintenanceStatus-scheduled": "طے شدہ",
"maintenanceStatus-ended": "ختم ہوا",
"recurringInterval": "وقفہ",
"Recurring": "بار بار چلنے والا",
"Recurring": "بار چلنے والا",
"strategyManual": "دستی طور پر فعال/غیر فعال",
"warningTimezone": "یہ سرور کا ٹائم زون استعمال کر رہا ہے",
"weekdayShortMon": "پیر",
@ -427,5 +427,25 @@
"lastDay4": "مہینے کا چوتھا آخری دن",
"pauseMaintenanceMsg": "کیا آپ واقعی روکنا چاہتے ہیں؟",
"No Maintenance": "کوئی دیکھ بھال نہیں",
"weekdayShortTue": "منگل"
"weekdayShortTue": "منگل",
"Add New Tag": "نیا ٹیگ شامل کریں",
"Enable DNS Cache": "ڈی این ایس کیشے کو فعال کریں",
"Effective Date Range": "مؤثر تاریخ کی حد",
"Schedule Maintenance": "شیڈول کی بحالی",
"Date and Time": "تاریخ اور وقت",
"DateTime Range": "تاریخ کے وقت کی حد",
"loadingError": "ڈیٹا حاصل نہیں کیا جا سکتا، براہ کرم بعد میں دوبارہ کوشش کریں۔",
"Enable": "فعال",
"Disable": "غیر فعال کریں",
"dnsCacheDescription": "ہو سکتا ہے یہ کچھ IPv6 ماحول میں کام نہ کر رہا ہو، اگر آپ کو کوئی مسئلہ درپیش ہو تو اسے غیر فعال کر دیں۔",
"Single Maintenance Window": "سنگل مینٹیننس ونڈو",
"Maintenance Time Window of a Day": "ایک دن کی مینٹیننس ٹائم ونڈو",
"plugin": "پلگ ان | پلگ انز",
"install": "انسٹال کریں",
"statusPageRefreshIn": "اس میں ریفریش کریں: {0}",
"maintenanceStatus-unknown": "نامعلوم",
"Display Timezone": "ٹائم زون ڈسپلے کریں",
"Server Timezone": "سرور ٹائم زون",
"statusPageMaintenanceEndDate": "ختم",
"IconUrl": "آئیکن یو آر ایل"
}

@ -740,5 +740,6 @@
"Add New Tag": "添加新标签",
"lunaseaDeviceID": "设备ID",
"lunaseaTarget": "目标",
"lunaseaUserID": "用户ID"
"lunaseaUserID": "用户ID",
"statusPageRefreshIn": "将于 {0} 后刷新"
}

@ -228,7 +228,7 @@
"smtpCC": "CC",
"smtpBCC": "BCC",
"Discord Webhook URL": "Discord Webhook 網址",
"wayToGetDiscordURL": "您可以前往伺服器設定 -> 整合 -> Webhook -> 新 Webhook 以取得",
"wayToGetDiscordURL": "您可以前往 伺服器設定 -> 整合 -> Webhook -> 新 Webhook 以取得",
"Bot Display Name": "機器人顯示名稱",
"Prefix Custom Message": "前綴自訂訊息",
"Webhook URL": "Webhook 網址",
@ -376,10 +376,10 @@
"default": "預設",
"enabled": "啟用",
"setAsDefault": "設為預設",
"deleteProxyMsg": "您確定要為所有監測器刪除此代理伺服器嗎?",
"proxyDescription": "必須將代理伺服器指派給監測器才能運作。",
"enableProxyDescription": "此代理伺服器在啟用前不會在監測器上生效,您可以藉由控制啟用狀態來暫時對所有的監測器停用代理伺服器。",
"setAsDefaultProxyDescription": "預設情況下,新監測器將啟用此代理伺服器。您仍可分別停用各監測器的代理伺服器。",
"deleteProxyMsg": "您確定要為所有監測器刪除此 Proxy 嗎?",
"proxyDescription": "必須將 Proxy 指派給監測器才能運作。",
"enableProxyDescription": "此 Proxy 在啟用前不會在監測器上生效,您可以藉由控制啟用狀態來暫時對所有的監測器停用 Proxy。",
"setAsDefaultProxyDescription": "預設情況下,新監測器將啟用此 Proxy。您仍可分別停用各監測器的 Proxy。",
"Maintenance": "維護",
"statusMaintenance": "維護中",
"Enable DNS Cache": "啟用 DNS 快取",
@ -430,8 +430,8 @@
"Remove Token": "移除 Token",
"Start": "開始",
"User": "使用者",
"trustProxyDescription": "信任 'X-Forwarded-*' 的 Header。如果您想取得正確的 Client IP且您的 Uptime Kuma 架設於 Nginx 或 Apache 之後,您應啟用此選項。",
"Reverse Proxy": "Reverse Proxy",
"trustProxyDescription": "信任 'X-Forwarded-*' 的 Header。如果您想取得正確的 Client IP且您的 Uptime Kuma 架設於 Nginx 或 Apache Proxy 之後,您應啟用此選項。",
"Reverse Proxy": "反向 Proxy",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "若要取得長期有效 Access Token請按您的個人檔案名稱 (左下角),捲動至最下方,然後按建立 Token。 ",
"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 中查看通知服務的列表,在\"開發者工具 > 服務\"下搜尋\"通知\"來找到您的裝置/手機的名稱。",
"loadingError": "未能取得數據,請重新再試。",
@ -483,7 +483,7 @@
"API Key": "API Key",
"Show update if available": "有更新時顯示",
"Also check beta release": "檢查 Beta 版本",
"Using a Reverse Proxy?": "正在使用 Reverse Proxy",
"Using a Reverse Proxy?": "正在使用反向代理 (Reverse Proxy)",
"Check how to config it for WebSocket": "查看如何加入 WebSocket 設定",
"Steam Game Server": "Steam 遊戲 Server",
"Most likely causes:": "最可能原因:",
@ -549,7 +549,7 @@
"confirmUninstallPlugin": "你確定要解除安裝?",
"dataRetentionTimeError": "保留限期必需為 0 或正數",
"infiniteRetention": "設定為 0 以作無限期保留。",
"Effective Date Range": "有效日期範圍",
"Effective Date Range": "有效日期範圍 (可選)",
"Hello @everyone is...": "Hello {'@'}everyone is…",
"Packet Size": "Packet 大小",
"Event type:": "事件類型:",
@ -702,5 +702,22 @@
"Platform": "平台",
"Device Token": "裝置 Token",
"telegramProtectContent": "禁止轉發/儲存",
"telegramProtectContentDescription": "如果選擇,用戶將不能轉發/儲存收到的信息。"
"telegramProtectContentDescription": "如果選擇,用戶將不能轉發/儲存收到的信息。",
"Add New Tag": "加新標籤",
"Economy": "經濟",
"Lowcost": "平價",
"high": "高價",
"statusPageRefreshIn": "將於 {0} 後重新整理",
"SendKey": "SendKey",
"SMSManager API Docs": "SMSManager API 文件 ",
"startDateTime": "開始時間",
"pagertreeLow": "低",
"endDateTime": "結束時間",
"cronExpression": "Cron 表達式",
"cronSchedule": "排程: ",
"invalidCronExpression": "無效 Cron 表達式:{0}",
"sameAsServerTimezone": "使用伺服器時區",
"WeCom Bot Key": "WeCom 機器人 Key",
"pagertreeMedium": "中",
"pagertreeHigh": "高"
}

@ -39,6 +39,9 @@ export default {
}
if (this.path.startsWith("/status-page") || this.path.startsWith("/status")) {
if (this.statusPageTheme === "auto") {
return this.system;
}
return this.statusPageTheme;
} else {
if (this.userTheme === "auto") {

@ -3,7 +3,7 @@
<div>
<h1 class="mb-3">{{ pageName }}</h1>
<form @submit.prevent="submit">
<div class="shadow-box">
<div class="shadow-box shadow-box-with-fixed-bottom-bar">
<div class="row">
<div class="col-xl-10">
<!-- Title -->
@ -85,35 +85,39 @@
<h2 class="mt-5">{{ $t("Date and Time") }}</h2>
<div> {{ $t("warningTimezone") }}: <mark>{{ $root.info.serverTimezone }} ({{ $root.info.serverTimezoneOffset }})</mark></div>
<!-- Strategy -->
<div class="my-3">
<label for="strategy" class="form-label">{{ $t("Strategy") }}</label>
<select id="strategy" v-model="maintenance.strategy" class="form-select">
<option value="manual">{{ $t("strategyManual") }}</option>
<option value="single">{{ $t("Single Maintenance Window") }}</option>
<option value="cron">{{ $t("cronExpression") }}</option>
<option value="recurring-interval">{{ $t("Recurring") }} - {{ $t("recurringInterval") }}</option>
<option value="recurring-weekday">{{ $t("Recurring") }} - {{ $t("dayOfWeek") }}</option>
<option value="recurring-day-of-month">{{ $t("Recurring") }} - {{ $t("dayOfMonth") }}</option>
<option v-if="false" value="recurring-day-of-year">{{ $t("Recurring") }} - Day of Year</option>
</select>
</div>
<!-- Single Maintenance Window -->
<template v-if="maintenance.strategy === 'single'">
<!-- DateTime Range -->
</template>
<template v-if="maintenance.strategy === 'cron'">
<!-- Cron -->
<div class="my-3">
<label class="form-label">{{ $t("DateTime Range") }}</label>
<Datepicker
v-model="maintenance.dateRange"
:dark="$root.isDark"
range
:monthChangeOnScroll="false"
:minDate="minDate"
format="yyyy-MM-dd HH:mm"
modelType="yyyy-MM-dd HH:mm:ss"
/>
<label for="cron" class="form-label">
{{ $t("cronExpression") }}
</label>
<p>{{ $t("cronSchedule") }}{{ cronDescription }}</p>
<input id="cron" v-model="maintenance.cron" type="text" class="form-control" required>
</div>
<div class="my-3">
<!-- Duration -->
<label for="duration" class="form-label">
{{ $t("Duration (Minutes)") }}
</label>
<input id="duration" v-model="maintenance.durationMinutes" type="number" class="form-control" required min="1" step="1">
</div>
</template>
@ -180,7 +184,6 @@
</div>
</template>
<!-- For any recurring types -->
<template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month'">
<!-- Maintenance Time Window of a Day -->
<div class="my-3">
@ -192,33 +195,50 @@
disableTimeRangeValidation range
/>
</div>
</template>
<template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month' || maintenance.strategy === 'cron' || maintenance.strategy === 'single'">
<!-- Timezone -->
<div class="mb-4">
<label for="timezone" class="form-label">
{{ $t("Timezone") }}
</label>
<select id="timezone" v-model="maintenance.timezone" class="form-select">
<option :value="null">{{ $t("sameAsServerTimezone") }}</option>
<option value="UTC">UTC</option>
<option
v-for="(timezone, index) in timezoneList"
:key="index"
:value="timezone.value"
>
{{ timezone.name }}
</option>
</select>
</div>
<!-- Date Range -->
<div class="my-3">
<label class="form-label">{{ $t("Effective Date Range") }}</label>
<Datepicker
v-model="maintenance.dateRange"
:dark="$root.isDark"
range datePicker
:monthChangeOnScroll="false"
:minDate="minDate"
format="yyyy-MM-dd HH:mm:ss"
modelType="yyyy-MM-dd HH:mm:ss"
required
/>
<div class="row">
<div class="col">
<div class="mb-2">{{ $t("startDateTime") }}</div>
<input v-model="maintenance.dateRange[0]" type="datetime-local" class="form-control">
</div>
</template>
<div class="mt-4 mb-1">
<button
id="monitor-submit-btn" class="btn btn-primary" type="submit"
:disabled="processing"
>
{{ $t("Save") }}
</button>
<div class="col">
<div class="mb-2">{{ $t("endDateTime") }}</div>
<input v-model="maintenance.dateRange[1]" type="datetime-local" class="form-control">
</div>
</div>
</div>
</template>
</div>
</div>
<div class="fixed-bottom-bar p-3">
<button id="monitor-submit-btn" class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
</div>
</div>
</form>
</div>
@ -226,11 +246,12 @@
</template>
<script>
import { useToast } from "vue-toastification";
import VueMultiselect from "vue-multiselect";
import dayjs from "dayjs";
import Datepicker from "@vuepic/vue-datepicker";
import { timezoneList } from "../util-frontend";
import cronstrue from "cronstrue/i18n";
const toast = useToast();
@ -242,6 +263,7 @@ export default {
data() {
return {
timezoneList: timezoneList(),
processing: false,
maintenance: {},
affectedMonitors: [],
@ -256,18 +278,6 @@ export default {
langKey: "lastDay1",
value: "lastDay1",
},
{
langKey: "lastDay2",
value: "lastDay2",
},
{
langKey: "lastDay3",
value: "lastDay3",
},
{
langKey: "lastDay4",
value: "lastDay4",
}
],
weekdays: [
{
@ -311,6 +321,34 @@ export default {
computed: {
cronDescription() {
if (! this.maintenance.cron) {
return "";
}
let locale = "";
if (this.$root.language) {
locale = this.$root.language.replace("-", "_");
}
// Special handling
// If locale is also not working in your language, you can map it here
// https://github.com/bradymholt/cRonstrue/tree/master/src/i18n/locales
if (locale === "zh_HK") {
locale = "zh_TW";
}
try {
return cronstrue.toString(this.maintenance.cron, {
locale,
});
} catch (e) {
return this.$t("invalidCronExpression", e.message);
}
},
selectedStatusPagesOptions() {
return Object.values(this.$root.statusPageList).map(statusPage => {
return {
@ -370,6 +408,8 @@ export default {
description: "",
strategy: "single",
active: 1,
cron: "30 3 * * *",
durationMinutes: 60,
intervalDay: 1,
dateRange: [ this.minDate ],
timeRange: [{
@ -381,6 +421,7 @@ export default {
}],
weekdays: [],
daysOfMonth: [],
timezone: null,
};
} else if (this.isEdit) {
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
@ -501,10 +542,6 @@ export default {
</script>
<style lang="scss" scoped>
.shadow-box {
padding: 20px;
}
textarea {
min-height: 150px;
}

@ -3,7 +3,7 @@
<div>
<h1 class="mb-3">{{ pageName }}</h1>
<form @submit.prevent="submit">
<div class="shadow-box">
<div class="shadow-box shadow-box-with-fixed-bottom-bar">
<div class="row">
<div class="col-md-6">
<h2 class="mb-2">{{ $t("General") }}</h2>
@ -98,7 +98,7 @@
<!-- gRPC URL -->
<div v-if="monitor.type === 'grpc-keyword' " class="my-3">
<label for="grpc-url" class="form-label">{{ $t("URL") }}</label>
<input id="grpc-url" v-model="monitor.grpcUrl" type="url" class="form-control" pattern="[^\:]+:[0-9]{5}" required>
<input id="grpc-url" v-model="monitor.grpcUrl" type="text" class="form-control" required>
</div>
<!-- Push URL -->
@ -283,13 +283,13 @@
<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" placeholder="Server=<hostname>,<port>;Database=<your database>;User Id=<your user id>;Password=<your password>;Encrypt=<true/false>;TrustServerCertificate=<Yes/No>;Connection Timeout=<int>">
<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" placeholder="postgres://username:password@host:port/database">
<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" placeholder="mysql://username:password@host:port/database">
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
</template>
</div>
<div class="my-3">
@ -301,7 +301,7 @@
<template v-if="monitor.type === 'redis'">
<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" placeholder="redis://user:password@host:port">
<input id="redisConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
</div>
</template>
@ -311,7 +311,7 @@
<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" placeholder="mongodb://username:password@host:port/database">
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
</template>
</div>
</template>
@ -546,31 +546,50 @@
<option value="ntlm">
NTLM
</option>
<option value="mtls">
mTLS
</option>
</select>
</div>
<template v-if="monitor.authMethod && monitor.authMethod !== null ">
<template v-if="monitor.authMethod === 'mtls' ">
<div class="my-3">
<label for="tls-cert" class="form-label">{{ $t("Cert") }}</label>
<textarea id="tls-cert" v-model="monitor.tlsCert" class="form-control" :placeholder="$t('Cert body')" required></textarea>
</div>
<div class="my-3">
<label for="tls-key" class="form-label">{{ $t("Key") }}</label>
<textarea id="tls-key" v-model="monitor.tlsKey" class="form-control" :placeholder="$t('Key body')" required></textarea>
</div>
<div class="my-3">
<label for="tls-ca" class="form-label">{{ $t("CA") }}</label>
<textarea id="tls-ca" v-model="monitor.tlsCa" class="form-control" :placeholder="$t('Server CA')"></textarea>
</div>
</template>
<template v-else>
<div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Username") }}</label>
<label for="basicauth-user" class="form-label">{{ $t("Username") }}</label>
<input id="basicauth-user" v-model="monitor.basic_auth_user" type="text" class="form-control" :placeholder="$t('Username')">
</div>
<div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Password") }}</label>
<label for="basicauth-pass" class="form-label">{{ $t("Password") }}</label>
<input id="basicauth-pass" v-model="monitor.basic_auth_pass" type="password" autocomplete="new-password" class="form-control" :placeholder="$t('Password')">
</div>
<template v-if="monitor.authMethod === 'ntlm' ">
<div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Domain") }}</label>
<input id="basicauth-domain" v-model="monitor.authDomain" type="text" class="form-control" :placeholder="$t('Domain')">
<label for="ntlm-domain" class="form-label">{{ $t("Domain") }}</label>
<input id="ntlm-domain" v-model="monitor.authDomain" type="text" class="form-control" :placeholder="$t('Domain')">
</div>
<div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Workstation") }}</label>
<input id="basicauth-workstation" v-model="monitor.authWorkstation" type="text" class="form-control" :placeholder="$t('Workstation')">
<label for="ntlm-workstation" class="form-label">{{ $t("Workstation") }}</label>
<input id="ntlm-workstation" v-model="monitor.authWorkstation" type="text" class="form-control" :placeholder="$t('Workstation')">
</div>
</template>
</template>
</template>
</template>
<!-- gRPC Options -->
<template v-if="monitor.type === 'grpc-keyword' ">
@ -673,6 +692,13 @@ export default {
ipOrHostnameRegexPattern: hostNameRegexPattern(),
mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true),
gameList: null,
connectionStringTemplates: {
"sqlserver": "Server=<hostname>,<port>;Database=<your database>;User Id=<your user id>;Password=<your password>;Encrypt=<true/false>;TrustServerCertificate=<Yes/No>;Connection Timeout=<int>",
"postgres": "postgres://username:password@host:port/database",
"mysql": "mysql://username:password@host:port/database",
"redis": "redis://user:password@host:port",
"mongodb": "mongodb://username:password@host:port/database",
}
};
},
@ -834,6 +860,24 @@ message HealthCheckResponse {
}
});
}
// Set default database connection string if empty or it is a template from another database monitor type
for (let monitorType in this.connectionStringTemplates) {
if (this.monitor.type === monitorType) {
let isTemplate = false;
for (let key in this.connectionStringTemplates) {
if (this.monitor.databaseConnectionString === this.connectionStringTemplates[key]) {
isTemplate = true;
break;
}
}
if (!this.monitor.databaseConnectionString || isTemplate) {
this.monitor.databaseConnectionString = this.connectionStringTemplates[monitorType];
}
break;
}
}
},
currentGameObject(newGameObject, previousGameObject) {
@ -841,8 +885,7 @@ message HealthCheckResponse {
this.monitor.port = newGameObject.options.port;
}
this.monitor.game = newGameObject.keys[0];
}
},
},
mounted() {
this.init();
@ -888,7 +931,7 @@ message HealthCheckResponse {
interval: 60,
retryInterval: this.interval,
resendInterval: 0,
maxretries: 0,
maxretries: 1,
notificationIDList: {},
ignoreTls: false,
upsideDown: false,
@ -925,6 +968,14 @@ message HealthCheckResponse {
} else if (this.isEdit || this.isClone) {
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
if (res.ok) {
if (this.isClone) {
// Reset push token for cloned monitors
if (res.monitor.type === "push") {
res.monitor.pushToken = undefined;
}
}
this.monitor = res.monitor;
if (this.isClone) {
@ -1075,31 +1126,7 @@ message HealthCheckResponse {
<style lang="scss" scoped>
@import "../assets/vars.scss";
$padding: 20px;
.shadow-box {
padding-top: $padding;
padding-bottom: 0;
padding-right: $padding;
padding-left: $padding;
}
textarea {
min-height: 200px;
}
.fixed-bottom-bar {
position: sticky;
bottom: 0;
margin-left: -$padding;
margin-right: -$padding;
z-index: 100;
background-color: rgba(white, 0.2);
backdrop-filter: blur(2px);
border-radius: 0 0 10px 10px;
.dark & {
background-color: rgba($dark-header-bg, 0.9);
}
}
</style>

@ -34,9 +34,13 @@
</div>
</div>
<div class="my-3 form-check form-switch">
<input id="switch-theme" v-model="config.theme" class="form-check-input" type="checkbox" true-value="dark" false-value="light">
<label class="form-check-label" for="switch-theme">{{ $t("Switch to Dark Theme") }}</label>
<div class="my-3">
<label for="switch-theme" class="form-label">{{ $t("Theme") }}</label>
<select id="switch-theme" v-model="config.theme" class="form-select">
<option value="auto">{{ $t("Auto") }}</option>
<option value="light">{{ $t("Light") }}</option>
<option value="dark">{{ $t("Dark") }}</option>
</select>
</div>
<div class="my-3 form-check form-switch">
@ -276,9 +280,24 @@
<div class="mt-3">
<div v-if="allMonitorList.length > 0 && loadedData">
<label>{{ $t("Add a monitor") }}:</label>
<select v-model="selectedMonitor" class="form-control">
<option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option>
</select>
<VueMultiselect
v-model="selectedMonitor"
:options="allMonitorList"
:multiple="false"
:searchable="true"
:placeholder="$t('Add a monitor')"
label="name"
trackBy="name"
class="mt-3"
>
<template #option="{ option }">
<div
class="d-inline-flex"
>
<span>{{ option.name }} <Tag v-for="tag in option.tags" :key="tag" :item="tag" :size="'sm'" /></span>
</div>
</template>
</VueMultiselect>
</div>
<div v-else class="text-center">
{{ $t("No monitors available.") }} <router-link to="/add">{{ $t("Add one") }}</router-link>
@ -306,6 +325,11 @@
<p v-if="config.showPoweredBy">
{{ $t("Powered by") }} <a target="_blank" rel="noopener noreferrer" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
</p>
<div class="refresh-info mb-2">
<div>{{ $t("Last Updated") }}: <date-time :value="lastUpdateTime" /></div>
<div>{{ $tc("statusPageRefreshIn", [ updateCountdownText]) }}</div>
</div>
</footer>
</div>
@ -322,6 +346,7 @@
<script>
import axios from "axios";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import Favico from "favico.js";
// import highlighting library (you can use any library you want just return html string)
import { highlight, languages } from "prismjs/components/prism-core";
@ -337,10 +362,14 @@ import DOMPurify from "dompurify";
import Confirm from "../components/Confirm.vue";
import PublicGroupList from "../components/PublicGroupList.vue";
import MaintenanceTime from "../components/MaintenanceTime.vue";
import DateTime from "../components/Datetime.vue";
import { getResBaseURL } from "../util-frontend";
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE } from "../util.ts";
import Tag from "../components/Tag.vue";
import VueMultiselect from "vue-multiselect";
const toast = useToast();
dayjs.extend(duration);
const leavePageMsg = "Do you really want to leave? you have unsaved changes!";
@ -359,6 +388,9 @@ export default {
Confirm,
PrismEditor,
MaintenanceTime,
DateTime,
Tag,
VueMultiselect
},
// Leave Page for vue route change
@ -400,6 +432,10 @@ export default {
baseURL: "",
clickedEditButton: false,
maintenanceList: [],
autoRefreshInterval: 5,
lastUpdateTime: dayjs(),
updateCountdown: null,
updateCountdownText: null,
};
},
computed: {
@ -637,11 +673,13 @@ export default {
console.log(error);
});
// 5mins a loop
// Configure auto-refresh loop
this.updateHeartbeatList();
feedInterval = setInterval(() => {
this.updateHeartbeatList();
}, (300 + 10) * 1000);
}, (this.autoRefreshInterval * 60 + 10) * 1000);
this.updateUpdateTimer();
// Go to edit page if ?edit present
// null means ?edit present, but no value
@ -700,10 +738,29 @@ export default {
favicon.badge(downMonitors);
this.loadedData = true;
this.lastUpdateTime = dayjs();
this.updateUpdateTimer();
});
}
},
/**
* Setup timer to display countdown to refresh
* @returns {void}
*/
updateUpdateTimer() {
clearInterval(this.updateCountdown);
this.updateCountdown = setInterval(() => {
const countdown = dayjs.duration(this.lastUpdateTime.add(this.autoRefreshInterval, "minutes").add(10, "seconds").diff(dayjs()));
if (countdown.as("seconds") < 0) {
clearInterval(this.updateCountdown);
} else {
this.updateCountdownText = countdown.format("mm:ss");
}
}, 1000);
},
/** Enable editing mode */
edit() {
if (this.hasToken) {
@ -889,7 +946,11 @@ export default {
* @returns {string} Sanitized HTML
*/
maintenanceHTML(description) {
if (description) {
return DOMPurify.sanitize(marked(description));
} else {
return "";
}
},
}
@ -1118,4 +1179,8 @@ footer {
}
}
.refresh-info {
opacity: 0.7;
}
</style>

@ -90,7 +90,7 @@ export function hostNameRegexPattern(mqtt = false) {
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
const ipRegexPattern = `((^${mqtt ? mqttSchemeRegexPattern : ""}((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?$))`;
// Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
const hostNameRegexPattern = `^${mqtt ? mqttSchemeRegexPattern : ""}([a-zA-Z0-9])?(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])$`;
const hostNameRegexPattern = `^${mqtt ? mqttSchemeRegexPattern : ""}([a-zA-Z0-9])?(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])(\\.)?$`;
return `${ipRegexPattern}|${hostNameRegexPattern}`;
}

Loading…
Cancel
Save