Merge branch 'master' into ntfy-bearer-authorization

This commit is contained in:
Josua Frank 2023-04-03 08:33:05 +02:00 committed by GitHub
commit 6b078b83bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1180 additions and 678 deletions

View file

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

23
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.21.1", "version": "1.21.2-beta.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.21.1", "version": "1.21.2-beta.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "~1.7.3", "@grpc/grpc-js": "~1.7.3",
@ -26,6 +26,7 @@
"command-exists": "~1.2.9", "command-exists": "~1.2.9",
"compare-versions": "~3.6.0", "compare-versions": "~3.6.0",
"compression": "~1.7.4", "compression": "~1.7.4",
"croner": "^6.0.3",
"dayjs": "~1.11.5", "dayjs": "~1.11.5",
"dotenv": "~16.0.3", "dotenv": "~16.0.3",
"express": "~4.17.3", "express": "~4.17.3",
@ -87,6 +88,7 @@
"chartjs-adapter-dayjs": "~1.0.0", "chartjs-adapter-dayjs": "~1.0.0",
"concurrently": "^7.1.0", "concurrently": "^7.1.0",
"core-js": "~3.26.1", "core-js": "~3.26.1",
"cronstrue": "~2.24.0",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
"cypress": "^10.1.0", "cypress": "^10.1.0",
"delay": "^5.0.0", "delay": "^5.0.0",
@ -7243,6 +7245,23 @@
"yup": "0.32.9" "yup": "0.32.9"
} }
}, },
"node_modules/croner": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/croner/-/croner-6.0.3.tgz",
"integrity": "sha512-Go+s9AaI+MeZUDJ6Kp7OYXCbM3svJ0qZ3IpkGoPetZLnP5wpX8MBTEiJOTYDFokP0Ph85GFZEUTBL9fo1e4DtQ==",
"engines": {
"node": ">=6.0"
}
},
"node_modules/cronstrue": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.24.0.tgz",
"integrity": "sha512-A1of24mAGz+OWrdGsxT9BOnDqn2ba182hie8Jx0UcEC2t+ZKtfAJxaFntKUgR7sIisU297fgHBSlNhMIfvAkSA==",
"dev": true,
"bin": {
"cronstrue": "bin/cli.js"
}
},
"node_modules/cross-env": { "node_modules/cross-env": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",

View file

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.21.1", "version": "1.21.2-beta.0",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -85,6 +85,7 @@
"command-exists": "~1.2.9", "command-exists": "~1.2.9",
"compare-versions": "~3.6.0", "compare-versions": "~3.6.0",
"compression": "~1.7.4", "compression": "~1.7.4",
"croner": "^6.0.3",
"dayjs": "~1.11.5", "dayjs": "~1.11.5",
"dotenv": "~16.0.3", "dotenv": "~16.0.3",
"express": "~4.17.3", "express": "~4.17.3",
@ -146,6 +147,7 @@
"chartjs-adapter-dayjs": "~1.0.0", "chartjs-adapter-dayjs": "~1.0.0",
"concurrently": "^7.1.0", "concurrently": "^7.1.0",
"core-js": "~3.26.1", "core-js": "~3.26.1",
"cronstrue": "~2.24.0",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
"cypress": "^10.1.0", "cypress": "^10.1.0",
"delay": "^5.0.0", "delay": "^5.0.0",

View file

@ -30,11 +30,6 @@ class Database {
*/ */
static patched = false; static patched = false;
/**
* For Backup only
*/
static backupPath = null;
/** /**
* Add patch filename in key * Add patch filename in key
* Values: * Values:
@ -74,6 +69,7 @@ class Database {
"patch-add-description-monitor.sql": true, "patch-add-description-monitor.sql": true,
"patch-api-key-table.sql": true, "patch-api-key-table.sql": true,
"patch-monitor-tls.sql": true, "patch-monitor-tls.sql": true,
"patch-maintenance-cron.sql": true,
}; };
/** /**
@ -198,15 +194,7 @@ class Database {
} else { } else {
log.info("db", "Database patch is needed"); log.info("db", "Database patch is needed");
try { // Try catch anything here
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 { try {
for (let i = version + 1; i <= this.latestVersion; i++) { for (let i = version + 1; i <= this.latestVersion; i++) {
const sqlFile = `./db/patch${i}.sql`; const sqlFile = `./db/patch${i}.sql`;
@ -222,7 +210,6 @@ class Database {
log.error("db", "Start Uptime-Kuma failed due to issue patching the 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"); 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); process.exit(1);
} }
} }
@ -264,8 +251,6 @@ class Database {
log.error("db", "Start Uptime-Kuma failed due to issue patching the 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"); 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); process.exit(1);
} }
@ -367,8 +352,6 @@ class Database {
} }
} }
this.backup(dayjs().format("YYYYMMDDHHmmss"));
log.info("db", sqlFilename + " is patching"); log.info("db", sqlFilename + " is patching");
this.patched = true; this.patched = true;
await this.importSQLFile("./db/" + sqlFilename); await this.importSQLFile("./db/" + sqlFilename);
@ -450,100 +433,6 @@ class Database {
process.removeListener("unhandledRejection", listener); 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 */ /** Get the size of the database */
static getSize() { static getSize() {
log.debug("db", "Database.getSize()"); log.debug("db", "Database.getSize()");

View file

@ -1,8 +1,10 @@
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util"); const { parseTimeObject, parseTimeFromTimeObject, log } = require("../../src/util");
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const Cron = require("croner");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const apicache = require("../modules/apicache");
class Maintenance extends BeanModel { class Maintenance extends BeanModel {
@ -15,16 +17,16 @@ class Maintenance extends BeanModel {
let dateRange = []; let dateRange = [];
if (this.start_date) { if (this.start_date) {
dateRange.push(utcToLocal(this.start_date)); dateRange.push(this.start_date);
if (this.end_date) { if (this.end_date) {
dateRange.push(utcToLocal(this.end_date)); dateRange.push(this.end_date);
} }
} }
let timeRange = []; let timeRange = [];
let startTime = timeObjectToLocal(parseTimeObject(this.start_time)); let startTime = parseTimeObject(this.start_time);
timeRange.push(startTime); timeRange.push(startTime);
let endTime = timeObjectToLocal(parseTimeObject(this.end_time)); let endTime = parseTimeObject(this.end_time);
timeRange.push(endTime); timeRange.push(endTime);
let obj = { let obj = {
@ -39,12 +41,43 @@ class Maintenance extends BeanModel {
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [], weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [], daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
timeslotList: [], 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(); 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();
for (let timeslot of timeslotList) { if (runningTimeslot) {
obj.timeslotList.push(await timeslot.toPublicJSON()); obj.timeslotList.push(runningTimeslot);
}
let nextRunDate = this.beanMeta.job.nextRun();
if (nextRunDate) {
let startDateDayjs = dayjs(nextRunDate);
let startDate = startDateDayjs.toISOString();
let endDate = startDateDayjs.add(this.duration, "second").toISOString();
obj.timeslotList.push({
startDate,
endDate,
});
}
}
} }
if (!Array.isArray(obj.weekdays)) { if (!Array.isArray(obj.weekdays)) {
@ -55,54 +88,9 @@ class Maintenance extends BeanModel {
obj.daysOfMonth = []; obj.daysOfMonth = [];
} }
// 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();
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());
obj.status = "under-maintenance";
break;
}
}
if (!obj.status) {
obj.status = "scheduled";
}
} else if (obj.timeslotList.length === 0) {
obj.status = "ended";
} else {
obj.status = "unknown";
}
return obj; return obj;
} }
/**
* 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 an object that ready to parse to JSON * Return an object that ready to parse to JSON
* @param {string} timezone If not specified, the timeRange will be in UTC * @param {string} timezone If not specified, the timeRange will be in UTC
@ -126,7 +114,7 @@ class Maintenance extends BeanModel {
/** /**
* Get a list of days in month that maintenance is active for * 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() { getDayOfMonthList() {
return JSON.parse(this.days_of_month).sort(function (a, b) { return JSON.parse(this.days_of_month).sort(function (a, b) {
@ -135,26 +123,10 @@ class Maintenance extends BeanModel {
} }
/** /**
* Get the start date and time for maintenance * Get the duration of maintenance in seconds
* @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
* @returns {number} Duration of maintenance * @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"); 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 // Add 24hours if it is across day
if (duration < 0) { if (duration < 0) {
@ -169,71 +141,266 @@ class Maintenance extends BeanModel {
* @param {Object} obj Data to fill bean with * @param {Object} obj Data to fill bean with
* @returns {Bean} Filled bean * @returns {Bean} Filled bean
*/ */
static jsonToBean(bean, obj) { static async jsonToBean(bean, obj) {
if (obj.id) { if (obj.id) {
bean.id = 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.title = obj.title;
bean.description = obj.description; bean.description = obj.description;
bean.strategy = obj.strategy; bean.strategy = obj.strategy;
bean.interval_day = obj.intervalDay; bean.interval_day = obj.intervalDay;
bean.timezone = obj.timezone;
bean.active = obj.active; bean.active = obj.active;
if (obj.dateRange[0]) { if (obj.dateRange[0]) {
bean.start_date = localToUTC(obj.dateRange[0]); bean.start_date = obj.dateRange[0];
if (obj.dateRange[1]) { if (obj.dateRange[1]) {
bean.end_date = localToUTC(obj.dateRange[1]); bean.end_date = obj.dateRange[1];
} }
} }
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.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]); bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
bean.weekdays = JSON.stringify(obj.weekdays); bean.weekdays = JSON.stringify(obj.weekdays);
bean.days_of_month = JSON.stringify(obj.daysOfMonth); bean.days_of_month = JSON.stringify(obj.daysOfMonth);
await bean.generateCron();
this.validateCron(bean.cron);
}
return bean; return bean;
} }
/** /**
* SQL conditions for active maintenance * Throw error if cron is invalid
* @returns {string} * @param cron
* @returns {Promise<void>}
*/ */
static getActiveMaintenanceSQLCondition() { static async validateCron(cron) {
return ` let job = new Cron(cron, () => {});
( job.stop();
(maintenance_timeslot.start_date <= DATETIME('now')
AND maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1)
)
`;
} }
/** /**
* SQL conditions for active and future maintenance * Run the cron
* @returns {string}
*/ */
static getActiveAndFutureMaintenanceSQLCondition() { async run(throwError = false) {
return ` if (this.beanMeta.job) {
( log.debug("maintenance", "Maintenance is already running, stop it first. id: " + this.id);
((maintenance_timeslot.end_date >= DATETIME('now') this.stop();
AND maintenance.active = 1) }
OR
(maintenance.strategy = 'manual' AND active = 1)) 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();
}
} }
} }

View file

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

View file

@ -16,7 +16,6 @@ const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server"); const { UptimeKumaServer } = require("../uptime-kuma-server");
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent"); const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
const { DockerHost } = require("../docker"); const { DockerHost } = require("../docker");
const Maintenance = require("./maintenance");
const { UptimeCacheList } = require("../uptime-cache-list"); const { UptimeCacheList } = require("../uptime-cache-list");
const Gamedig = require("gamedig"); const Gamedig = require("gamedig");
@ -1303,18 +1302,19 @@ class Monitor extends BeanModel {
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
*/ */
static async isUnderMaintenance(monitorID) { static async isUnderMaintenance(monitorID) {
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition(); const maintenanceIDList = await R.getCol(`
const maintenance = await R.getRow(` SELECT maintenance_id FROM monitor_maintenance
SELECT COUNT(*) AS count WHERE monitor_id = ?
FROM monitor_maintenance mm `, [ monitorID ]);
JOIN maintenance
ON mm.maintenance_id = maintenance.id for (const maintenanceID of maintenanceIDList) {
AND mm.monitor_id = ? const maintenance = await UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
LEFT JOIN maintenance_timeslot if (maintenance && await maintenance.isUnderMaintenance()) {
ON maintenance_timeslot.maintenance_id = maintenance.id return true;
WHERE ${activeCondition} }
LIMIT 1`, [ monitorID ]); }
return maintenance.count !== 0;
return false;
} }
/** Make sure monitor interval is between bounds */ /** Make sure monitor interval is between bounds */

View file

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

View file

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

View file

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

View file

@ -47,8 +47,6 @@ class UptimeKumaServer {
*/ */
indexHTML = ""; indexHTML = "";
generateMaintenanceTimeslotsInterval = undefined;
/** /**
* Plugins Manager * Plugins Manager
* @type {PluginsManager} * @type {PluginsManager}
@ -112,8 +110,7 @@ class UptimeKumaServer {
log.debug("DEBUG", "Timezone: " + process.env.TZ); log.debug("DEBUG", "Timezone: " + process.env.TZ);
log.debug("DEBUG", "Current Time: " + dayjs.tz().format()); log.debug("DEBUG", "Current Time: " + dayjs.tz().format());
await this.generateMaintenanceTimeslots(); await this.loadMaintenanceList();
this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
} }
/** /**
@ -175,16 +172,33 @@ class UptimeKumaServer {
*/ */
async getMaintenanceJSONList(userID) { async getMaintenanceJSONList(userID) {
let result = {}; 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) { 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;
} }
/** /**
@ -240,7 +254,7 @@ class UptimeKumaServer {
* Attempt to get the current server timezone * Attempt to get the current server timezone
* If this fails, fall back to environment variables and then make a * If this fails, fall back to environment variables and then make a
* guess. * guess.
* @returns {string} * @returns {Promise<string>}
*/ */
async getTimezone() { async getTimezone() {
let timezone = await Settings.get("serverTimezone"); let timezone = await Settings.get("serverTimezone");
@ -271,28 +285,9 @@ class UptimeKumaServer {
dayjs.tz.setDefault(timezone); 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 */ /** Stop the server */
async stop() { async stop() {
clearTimeout(this.generateMaintenanceTimeslotsInterval);
} }
loadPlugins() { loadPlugins() {
@ -341,5 +336,4 @@ module.exports = {
}; };
// Must be at the end // Must be at the end
const MaintenanceTimeslot = require("./model/maintenance_timeslot");
const { MonitorType } = require("./monitor-types/monitor-type"); const { MonitorType } = require("./monitor-types/monitor-type");

View file

@ -556,6 +556,31 @@ h5.settings-subheading::after {
border-bottom: 1px solid $dark-border-color; 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 // Localization
@import "localization.scss"; @import "localization.scss";

View file

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

View file

@ -102,7 +102,7 @@
"deleteNotificationMsg": "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?", "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.", "resolverserverDescription": "Cloudflare ist als der Standardserver festgelegt. Dieser kann jederzeit geändert werden.",
"Resolver Server": "Auflösungsserver", "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", "Last Result": "Letztes Ergebnis",
"pauseMonitorMsg": "Bist du sicher, dass du den Monitor pausieren möchtest?", "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?", "clearEventsMsg": "Bist du sicher, dass du alle Ereignisse für diesen Monitor löschen möchtest?",
@ -206,7 +206,7 @@
"mattermost": "Mattermost", "mattermost": "Mattermost",
"Primary Base URL": "Primär URL", "Primary Base URL": "Primär URL",
"Push URL": "Push 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}", "pushOptionalParams": "Optionale Parameter: {0}",
"defaultNotificationName": "Mein {notification} Alarm ({number})", "defaultNotificationName": "Mein {notification} Alarm ({number})",
"here": "hier", "here": "hier",
@ -276,10 +276,10 @@
"appriseInstalled": "Apprise ist installiert.", "appriseInstalled": "Apprise ist installiert.",
"appriseNotInstalled": "Apprise ist nicht installiert. {0}", "appriseNotInstalled": "Apprise ist nicht installiert. {0}",
"Access Token": "Access Token", "Access Token": "Access Token",
"Channel access token": "Channel access token", "Channel access token": "Channel Access Token",
"Line Developers Console": "Line Developers Console", "Line Developers Console": "Line Developers Console",
"lineDevConsoleTo": "Line Developers Console - {0}", "lineDevConsoleTo": "Line Developers Console - {0}",
"Basic Settings": "Basic Settings", "Basic Settings": "Grundeinstellungen",
"User ID": "User ID", "User ID": "User ID",
"Messaging API": "Messaging API", "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.", "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", "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.", "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}", "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", "Body": "Body",
"Headers": "Headers", "Headers": "Headers",
"PushUrl": "Push URL", "PushUrl": "Push URL",
@ -348,7 +348,7 @@
"Services": "Dienste", "Services": "Dienste",
"Discard": "Verwerfen", "Discard": "Verwerfen",
"Cancel": "Abbrechen", "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.", "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", "serwersms": "SerwerSMS.pl",
"serwersmsAPIUser": "API Benutzername (inkl. webapi_ prefix)", "serwersmsAPIUser": "API Benutzername (inkl. webapi_ prefix)",
@ -533,7 +533,7 @@
"Also check beta release": "Auch nach beta Versionen schauen", "Also check beta release": "Auch nach beta Versionen schauen",
"Using a Reverse Proxy?": "Wird ein Reverse Proxy genutzt?", "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", "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:", "Most likely causes:": "Wahrscheinliche Ursachen:",
"The resource is no longer available.": "Die Quelle ist nicht mehr verfügbar.", "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.", "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", "atLeastOneMonitor": "Wähle mindestens einen Monitor",
"deleteMaintenanceMsg": "Möchtest du diese Wartung löschen?", "deleteMaintenanceMsg": "Möchtest du diese Wartung löschen?",
"Base URL": "Basis URL", "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.", "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", "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.", "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", "squadcast": "Squadcast",
"SendKey": "SendKey", "SendKey": "SendKey",
"SMSManager API Docs": "SMSManager API Dokumente ", "SMSManager API Docs": "SMSManager API Dokumente ",
"Gateway Type": "Gateway Type", "Gateway Type": "Gateway Typ",
"SMSManager": "SMSManager", "SMSManager": "SMSManager",
"You can divide numbers with": "Du kannst Zahlen teilen mit", "You can divide numbers with": "Du kannst Zahlen teilen mit",
"or": "oder", "or": "oder",
"recurringInterval": "Intervall", "recurringInterval": "Intervall",
"Recurring": "Wiederkehrend", "Recurring": "Wiederkehrend",
"strategyManual": "Active/Inactive Manually", "strategyManual": "Aktiv/Inaktiv Manuell",
"warningTimezone": "Es wird die Zeitzone des Servers genutzt", "warningTimezone": "Es wird die Zeitzone des Servers genutzt",
"weekdayShortMon": "Mo", "weekdayShortMon": "Mo",
"weekdayShortTue": "Di", "weekdayShortTue": "Di",
@ -640,10 +640,10 @@
"installing": "Installiere", "installing": "Installiere",
"uninstall": "Deinstallieren", "uninstall": "Deinstallieren",
"uninstalling": "Deinstalliere", "uninstalling": "Deinstalliere",
"confirmUninstallPlugin": "Möchten Sie dieses Plugin wirklich deinstallieren?", "confirmUninstallPlugin": "Möchtest du dieses Plugin wirklich deinstallieren?",
"notificationRegional": "Regional", "notificationRegional": "Regional",
"Single Maintenance Window": "Einmaliges Wartungsfenster", "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", "Maintenance Time Window of a Day": "Wartungszeitfenster eines Tages",
"Effective Date Range": "Gültigkeitsbereich", "Effective Date Range": "Gültigkeitsbereich",
"Schedule Maintenance": "Wartung planen", "Schedule Maintenance": "Wartung planen",
@ -674,15 +674,15 @@
"Don't expire": "Nicht ablaufen", "Don't expire": "Nicht ablaufen",
"Add Another": "Hinzufügen", "Add Another": "Hinzufügen",
"Key Added": "Schlüssel hinzugefügt", "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", "Add API Key": "API Schlüssel hinzufügen",
"No API Keys": "Kein API Schlüssel", "No API Keys": "Kein API Schlüssel",
"apiKey-active": "Aktiv", "apiKey-active": "Aktiv",
"apiKey-expired": "Abgelaufen", "apiKey-expired": "Abgelaufen",
"apiKey-inactive": "Inaktiv", "apiKey-inactive": "Inaktiv",
"Expires": "Läuft ab", "Expires": "Läuft ab",
"disableAPIKeyMsg": "Sind Sie sicher, dass Sie diesen API Schlüssel deaktivieren möchten?", "disableAPIKeyMsg": "Bist du sicher, dass du diesen API Schlüssel deaktivieren willst?",
"deleteAPIKeyMsg": "Sind Sie sicher, dass Sie diesen API Schlüssel löschen möchten?", "deleteAPIKeyMsg": "Bist du sicher, dass du diesen API Schlüssel löschen willst?",
"Generate": "Generieren", "Generate": "Generieren",
"infiniteRetention": "Für unendliche Speicherung auf 0 setzen.", "infiniteRetention": "Für unendliche Speicherung auf 0 setzen.",
"dataRetentionTimeError": "Aufbewahrungsfrist muss grösser oder gleich 0 sein", "dataRetentionTimeError": "Aufbewahrungsfrist muss grösser oder gleich 0 sein",
@ -691,8 +691,8 @@
"cloneOf": "Klon von {0}", "cloneOf": "Klon von {0}",
"wayToGetZohoCliqURL": "Wie eine Webhook URL erstellt werden kann, erfährst du {0}.", "wayToGetZohoCliqURL": "Wie eine Webhook URL erstellt werden kann, erfährst du {0}.",
"enableGRPCTls": "Senden von gRPC Anforderungen mit TLS Verbindung zulassen", "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.)", "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", "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", "Guild ID": "Gilde ID",
"Lowcost": "Kostengünstig", "Lowcost": "Kostengünstig",
"high": "hoch", "high": "hoch",
@ -712,7 +712,7 @@
"pagertreeCritical": "Kritisch", "pagertreeCritical": "Kritisch",
"pagertreeResolve": "Automatisch auflösen", "pagertreeResolve": "Automatisch auflösen",
"pagertreeDoNothing": "Nichts tun", "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", "Server Address": "Serveradresse",
"Learn More": "Erfahre mehr", "Learn More": "Erfahre mehr",
"Edit Tag": "Tag editieren", "Edit Tag": "Tag editieren",
@ -724,8 +724,8 @@
"smseagleEncoding": "Als Unicode senden", "smseagleEncoding": "Als Unicode senden",
"smseaglePriority": "Nachrichtenpriorität (0-9, Standard = 0)", "smseaglePriority": "Nachrichtenpriorität (0-9, Standard = 0)",
"smseagleContact": "Telefonbuch Kontaktname(n)", "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.", "confirmDeleteTagMsg": "Möchtest du dieses Tag wirklich löschen? 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}", "wayToGetKookBotToken": "Erstelle eine Anwendung und erhalte den Bot-Token unter {0}",
"Strategy": "Strategie", "Strategy": "Strategie",
"Free Mobile User Identifier": "Kostenlose mobile Benutzerkennung", "Free Mobile User Identifier": "Kostenlose mobile Benutzerkennung",
"smseagleGroup": "Telefonbuch Gruppenname(n)", "smseagleGroup": "Telefonbuch Gruppenname(n)",
@ -737,5 +737,10 @@
"lunaseaDeviceID": "Geräte-ID", "lunaseaDeviceID": "Geräte-ID",
"lunaseaUserID": "Benutzer-ID", "lunaseaUserID": "Benutzer-ID",
"AuthenticationMethod": "Authentifizierungsmethode", "AuthenticationMethod": "Authentifizierungsmethode",
"UsernameAndPassword": "Benutzername und Passwort" "UsernameAndPassword": "Benutzername und Passwort",
"twilioAccountSID": "Account SID",
"twilioFromNumber": "Absender",
"twilioToNumber": "Empfänger",
"twilioAuthToken": "Auth Token",
"statusPageRefreshIn": "Aktualisierung in: {0}"
} }

View file

@ -590,7 +590,7 @@
"atLeastOneMonitor": "Wähle mindestens einen Monitor", "atLeastOneMonitor": "Wähle mindestens einen Monitor",
"deleteMaintenanceMsg": "Möchtest du diese Wartung löschen?", "deleteMaintenanceMsg": "Möchtest du diese Wartung löschen?",
"Base URL": "Basis URL", "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.", "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", "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.", "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,7 +605,7 @@
"or": "oder", "or": "oder",
"recurringInterval": "Intervall", "recurringInterval": "Intervall",
"Recurring": "Wiederkehrend", "Recurring": "Wiederkehrend",
"Single Maintenance Window": "Einzigartiges Wartungsfenster", "Single Maintenance Window": "Einmaliges Wartungsfenster",
"Maintenance Time Window of a Day": "Zeitfenster für die Wartung", "Maintenance Time Window of a Day": "Zeitfenster für die Wartung",
"Effective Date Range": "Bereich der Wirksamkeitsdaten", "Effective Date Range": "Bereich der Wirksamkeitsdaten",
"strategyManual": "Aktiv/Inaktiv Manuell", "strategyManual": "Aktiv/Inaktiv Manuell",
@ -646,19 +646,19 @@
"Disable": "Deaktivieren", "Disable": "Deaktivieren",
"Custom Monitor Type": "Benutzerdefinierter Monitortyp", "Custom Monitor Type": "Benutzerdefinierter Monitortyp",
"webhookAdditionalHeadersDesc": "Legt zusätzliche Header fest, die mit der Webhook gesendet wurden.", "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.", "loadingError": "Die Daten konnten nicht abgerufen werden, bitte später noch einmal versuchen.",
"confirmUninstallPlugin": "Möchten Sie dieses Plugin wirklich deinstallieren?", "confirmUninstallPlugin": "Möchtest du dieses Plugin wirklich deinstallieren?",
"grpcMethodDescription": "Der Name der Methode wird in das \"cammelCase\"-Format konvertiert (z.B. sayHello, check, etc.)", "grpcMethodDescription": "Der Name der Methode wird in das \"cammelCase\"-Format konvertiert (z.B. sayHello, check, etc.)",
"Passive Monitor Type": "Passiver Monitortyp", "Passive Monitor Type": "Passiver Monitortyp",
"Specific Monitor Type": "Spezifischer Monitortyp", "Specific Monitor Type": "Spezifischer Monitortyp",
"webhookAdditionalHeadersTitle": "Zusätzliche Header", "webhookAdditionalHeadersTitle": "Zusätzliche Header",
"Packet Size": "Paketgröße", "Packet Size": "Paketgröße",
"IconUrl": "Symbol-URL", "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", "dataRetentionTimeError": "Aufbewahrungszeit muss 0 oder größer sein",
"infiniteRetention": "Für unendliche Aufbewahrung auf 0 setzen.", "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", "enableGRPCTls": "Senden von gRPC-Anforderungen mit TLS-Verbindung zulassen",
"ZohoCliq": "ZohoCliq", "ZohoCliq": "ZohoCliq",
"Monitor": "Überwachung | Monitore", "Monitor": "Überwachung | Monitore",
@ -668,8 +668,8 @@
"uninstall": "Deinstallieren", "uninstall": "Deinstallieren",
"uninstalling": "Deinstallation", "uninstalling": "Deinstallation",
"markdownSupported": "Markdown-Syntax unterstützt", "markdownSupported": "Markdown-Syntax unterstützt",
"wayToGetKookBotToken": "Erstellen Sie eine Anwendung und erhalten Sie Ihren Bot-Token unter {0}", "wayToGetKookBotToken": "Erstelle eine Anwendung und erhalte den 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", "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", "Guild ID": "Guild-ID",
"Free Mobile User Identifier": "Kostenlose mobile Benutzerkennung", "Free Mobile User Identifier": "Kostenlose mobile Benutzerkennung",
"Free Mobile API Key": "Kostenloser Mobile API-Schlüssel", "Free Mobile API Key": "Kostenloser Mobile API-Schlüssel",
@ -687,7 +687,7 @@
"smseagleGroup": "Telefonbuch Gruppenname(n)", "smseagleGroup": "Telefonbuch Gruppenname(n)",
"smseagleContact": "Telefonbuch Kontaktname(n)", "smseagleContact": "Telefonbuch Kontaktname(n)",
"smseagleRecipientType": "Empfängertyp", "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", "smseagleToken": "API-Zugriffstoken",
"smseagleUrl": "Ihre SMSEagle-Geräte-URL", "smseagleUrl": "Ihre SMSEagle-Geräte-URL",
"Kook": "Kook", "Kook": "Kook",
@ -730,7 +730,7 @@
"telegramProtectContentDescription": "Die Bot-Nachrichten in Telegram sind gegen Weiterleitung und Speichern geschützt.", "telegramProtectContentDescription": "Die Bot-Nachrichten in Telegram sind gegen Weiterleitung und Speichern geschützt.",
"notificationRegional": "Regional", "notificationRegional": "Regional",
"Key Added": "Schlüssel hinzugefügt", "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", "telegramMessageThreadID": "(Optional) Nachrichten Thread ID",
"telegramMessageThreadIDDescription": "Optionale eindeutige Kennung für den Ziel-Thread (Thema) des Forums; nur für Forum-Supergroups", "telegramMessageThreadIDDescription": "Optionale eindeutige Kennung für den Ziel-Thread (Thema) des Forums; nur für Forum-Supergroups",
"telegramSendSilently": "Stumm Senden", "telegramSendSilently": "Stumm Senden",
@ -740,5 +740,10 @@
"lunaseaTarget": "Ziel", "lunaseaTarget": "Ziel",
"lunaseaUserID": "Benutzer-ID", "lunaseaUserID": "Benutzer-ID",
"AuthenticationMethod": "Authentifizierungsmethode", "AuthenticationMethod": "Authentifizierungsmethode",
"UsernameAndPassword": "Benutzername und Passwort" "UsernameAndPassword": "Benutzername und Passwort",
"twilioAccountSID": "Account SID",
"twilioFromNumber": "Absender",
"twilioToNumber": "Empfänger",
"twilioAuthToken": "Auth Token",
"statusPageRefreshIn": "Aktualisierung in: {0}"
} }

View file

@ -394,6 +394,12 @@
"backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.", "backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.",
"Optional": "Optional", "Optional": "Optional",
"or": "or", "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", "recurringInterval": "Interval",
"Recurring": "Recurring", "Recurring": "Recurring",
"strategyManual": "Active/Inactive Manually", "strategyManual": "Active/Inactive Manually",
@ -429,7 +435,7 @@
"dnsCacheDescription": "It may be not working in some IPv6 environments, disable it if you encounter any issues.", "dnsCacheDescription": "It may be not working in some IPv6 environments, disable it if you encounter any issues.",
"Single Maintenance Window": "Single Maintenance Window", "Single Maintenance Window": "Single Maintenance Window",
"Maintenance Time Window of a Day": "Maintenance Time Window of a Day", "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", "Schedule Maintenance": "Schedule Maintenance",
"Date and Time": "Date and Time", "Date and Time": "Date and Time",
"DateTime Range": "DateTime Range", "DateTime Range": "DateTime Range",

View file

@ -1,34 +1,34 @@
{ {
"languageName": "Farsi", "languageName": "فارسی",
"checkEverySecond": "بررسی هر {0} ثانیه.", "checkEverySecond": "بررسی هر {0} ثانیه",
"retryCheckEverySecond": "تکرار مجدد هر {0} ثانیه.", "retryCheckEverySecond": "تکرار مجدد هر {0} ثانیه",
"retriesDescription": "حداکثر تعداد تکرار پیش از علامت گذاری وب‌سایت بعنوان خارج از دسترس و ارسال اطلاع‌رسانی.", "retriesDescription": "حداکثر تعداد تکرار پیش از علامت گذاری وب‌سایت بعنوان خارج از دسترس و ارسال اطلاع‌رسانی",
"ignoreTLSError": "بی‌خیال ارور TLS/SSL برای سایت‌های HTTPS", "ignoreTLSError": "بی‌خیال ارور TLS/SSL برای سایت‌های HTTPS",
"upsideDownModeDescription": "نتیجه وضعیت را برعکس کن، مثلا اگر سرویس در دسترس بود فرض کن که سرویس پایین است!", "upsideDownModeDescription": "نتیجه وضعیت را برعکس کن، مثلا اگر سرویس در دسترس بود فرض کن که سرویس پایین است.",
"maxRedirectDescription": "حداکثر تعداد ریدایرکتی که سرویس پشتیبانی کند. برای اینکه ری‌دایرکت‌ها پشتیبانی نشوند، عدد 0 را وارد کنید.", "maxRedirectDescription": "حداکثر تعداد ریدایرکتی که سرویس پشتیبانی کند. برای اینکه ری‌دایرکت‌ها پشتیبانی نشوند، عدد 0 را وارد کنید.",
"acceptedStatusCodesDescription": "لطفا HTTP Status Code هایی که میخواهید به عنوان پاسخ موفقیت آمیز در نظر گرفته شود را انتخاب کنید.", "acceptedStatusCodesDescription": "لطفا HTTP Status Code هایی که میخواهید به عنوان پاسخ موفقیت آمیز در نظر گرفته شود را انتخاب کنید.",
"passwordNotMatchMsg": "تکرار رمز عبور مطابقت ندارد!", "passwordNotMatchMsg": "تکرار رمز عبور مطابقت ندارد.",
"notificationDescription": "برای اینکه سرویس اطلاع‌رسانی کار کند، آنرا به یکی از مانیتور‌ها متصل کنید.", "notificationDescription": "برای اینکه سرویس اطلاع‌رسانی کار کند، آنرا به یکی از مانیتور‌ها متصل کنید.",
"keywordDescription": "در نتیجه درخواست (اهمیتی ندارد پاسخ JSON است یا HTML) بدنبال این کلمه بگرد (حساس به کوچک/بزرگ بودن حروف).", "keywordDescription": "در نتیجه درخواست (اهمیتی ندارد پاسخ JSON است یا HTML) بدنبال این کلمه بگرد (حساس به کوچک/بزرگ بودن حروف).",
"pauseDashboardHome": "متوقف شده", "pauseDashboardHome": "متوقف شده",
"deleteMonitorMsg": "آیا از حذف این مانیتور مطمئن هستید؟", "deleteMonitorMsg": "آیا از حذف این مانیتور مطمئن هستید؟",
"deleteNotificationMsg": "آیا مطمئن هستید که میخواهید این سرویس اطلاع‌رسانی را برای تمامی مانیتورها حذف کنید؟", "deleteNotificationMsg": "آیا مطمئن هستید که میخواهید این سرویس اطلاع‌رسانی را برای تمامی مانیتورها حذف کنید؟",
"resolverserverDescription": "سرویس CloudFlare به عنوان سرور پیش‌فرض استفاده می‌شود، شما میتوانید آنرا به هر سرور دیگری بعدا تغییر دهید.", "resolverserverDescription": "سرویس CloudFlare به عنوان سرور پیش‌فرض استفاده می‌شود، شما میتوانید آنرا به هر سرور دیگری بعدا تغییر دهید.",
"rrtypeDescription": "لطفا نوع Resource Record را انتخاب کنید.", "rrtypeDescription": "لطفا نوع Resource Record را انتخاب کنید",
"pauseMonitorMsg": "آیا مطمئن هستید که میخواهید این مانیتور را متوقف کنید ؟", "pauseMonitorMsg": "آیا مطمئن هستید که میخواهید این مانیتور را متوقف کنید ؟",
"enableDefaultNotificationDescription": "برای هر مانیتور جدید، این سرویس اطلاع‌رسانی به صورت پیش‌فرض فعال خواهد شد. البته که شما میتوانید به صورت دستی آنرا برای هر مانیتور به صورت جداگانه غیر فعال کنید.", "enableDefaultNotificationDescription": "برای هر مانیتور جدید، این سرویس اطلاع‌رسانی به صورت پیش‌فرض فعال خواهد شد. البته که شما میتوانید به صورت دستی آنرا برای هر مانیتور به صورت جداگانه غیر فعال کنید.",
"clearEventsMsg": "آیا از اینکه تمامی تاریخچه رویداد‌های این مانیتور حذف شود مطمئن هستید؟", "clearEventsMsg": "آیا از اینکه تمامی تاریخچه رویداد‌های این مانیتور حذف شود مطمئن هستید؟",
"clearHeartbeatsMsg": "آیا از اینکه تاریخچه تمامی Heartbeat های این مانیتور حذف شود مطمئن هستید؟ ", "clearHeartbeatsMsg": "آیا از اینکه تاریخچه تمامی ضربان قلب های این مانیتور حذف شود مطمئن هستید؟",
"confirmClearStatisticsMsg": "آیا از حذف تمامی آمار و ارقام مطمئن هستید؟", "confirmClearStatisticsMsg": "آیا از حذف تمامی آمار و ارقام مطمئن هستید؟",
"importHandleDescription": "اگر که میخواهید بیخیال مانیتورها و یا سرویس‌های اطلاع‌رسانی که با نام مشابه از قبل موجود هستند شوید، گزینه 'بی‌خیال موارد ..' را انتخاب کنید. توجه کنید که گزینه 'بازنویسی' تمامی موارد موجود با نام مشابه را از بین خواهد برد.", "importHandleDescription": "اگر که میخواهید بیخیال مانیتورها و یا سرویس‌های اطلاع‌رسانی که با نام مشابه از قبل موجود هستند شوید، گزینه 'بی‌خیال موارد ..' را انتخاب کنید. توجه کنید که گزینه 'بازنویسی' تمامی موارد موجود با نام مشابه را از بین خواهد برد.",
"confirmImportMsg": "آیا از بازگردانی بک آپ مطمئن هستید؟ لطفا از اینکه نوع بازگردانی درستی را انتخاب کرده‌اید اطمینان حاصل کنید!", "confirmImportMsg": "آیا از بازگردانی بک آپ مطمئن هستید؟ لطفا از اینکه نوع بازگردانی درستی را انتخاب کرده‌اید اطمینان حاصل کنید.",
"twoFAVerifyLabel": "لطفا جهت اطمینان از عملکرد احراز هویت دو مرحله‌ای توکن خود را وارد کنید!", "twoFAVerifyLabel": "لطفا جهت اطمینان از عملکرد احراز هویت دو مرحله‌ای توکن خود را وارد کنید:",
"tokenValidSettingsMsg": "توکن شما معتبر است، هم اکنون میتوانید احراز هویت دو مرحله‌ای را فعال کنید!", "tokenValidSettingsMsg": "توکن شما معتبر است، هم اکنون میتوانید احراز هویت دو مرحله‌ای را فعال کنید.",
"confirmEnableTwoFAMsg": "آیا از فعال سازی احراز هویت دو مرحله‌ای مطمئن هستید؟", "confirmEnableTwoFAMsg": "آیا از فعال سازی احراز هویت دو مرحله‌ای مطمئن هستید؟",
"confirmDisableTwoFAMsg": "آیا از غیرفعال سازی احراز هویت دومرحله‌ای مطمئن هستید؟", "confirmDisableTwoFAMsg": "آیا از غیرفعال سازی احراز هویت دومرحله‌ای مطمئن هستید؟",
"Settings": "تنظیمات", "Settings": "تنظیمات",
"Dashboard": "پیشخوان", "Dashboard": "پیشخوان",
"New Update": "بروزرسانی جدید!", "New Update": "بروزرسانی جدید",
"Language": "زبان", "Language": "زبان",
"Appearance": "ظاهر", "Appearance": "ظاهر",
"Theme": "پوسته", "Theme": "پوسته",
@ -48,13 +48,13 @@
"Status": "وضعیت", "Status": "وضعیت",
"DateTime": "تاریخ و زمان", "DateTime": "تاریخ و زمان",
"Message": "پیام", "Message": "پیام",
"No important events": "رخداد جدیدی موجود نیست.", "No important events": "رخداد جدیدی موجود نیست",
"Resume": "ادامه", "Resume": "ادامه",
"Edit": "ویرایش", "Edit": "ویرایش",
"Delete": "حذف", "Delete": "حذف",
"Current": "فعلی", "Current": "فعلی",
"Uptime": "آپتایم", "Uptime": "آپتایم",
"Cert Exp.": "تاریخ انقضای SSL", "Cert Exp.": "تاریخ انقضای SSL.",
"day": "روز", "day": "روز",
"-day": "-روز", "-day": "-روز",
"hour": "ساعت", "hour": "ساعت",
@ -76,7 +76,7 @@
"Accepted Status Codes": "وضعیت‌های (Status Code) های قابل قبول", "Accepted Status Codes": "وضعیت‌های (Status Code) های قابل قبول",
"Save": "ذخیره", "Save": "ذخیره",
"Notifications": "اطلاع‌رسانی‌ها", "Notifications": "اطلاع‌رسانی‌ها",
"Not available, please setup.": "هیچ موردی موجود نیست، اولین مورد را راه اندازی کنید!", "Not available, please setup.": "هیچ موردی موجود نیست، اولین مورد را راه اندازی کنید.",
"Setup Notification": "راه اندازی اطلاع‌رسانی‌", "Setup Notification": "راه اندازی اطلاع‌رسانی‌",
"Light": "روشن", "Light": "روشن",
"Dark": "تاریک", "Dark": "تاریک",
@ -87,8 +87,8 @@
"None": "هیچ کدام", "None": "هیچ کدام",
"Timezone": "موقعیت زمانی", "Timezone": "موقعیت زمانی",
"Search Engine Visibility": "قابلیت دسترسی برای موتورهای جستجو", "Search Engine Visibility": "قابلیت دسترسی برای موتورهای جستجو",
"Allow indexing": "اجازه ایندکس شدن را بده.", "Allow indexing": "اجازه ایندکس شدن در موتور های جستجو را بده",
"Discourage search engines from indexing site": "به موتورهای جستجو اجازه ایندکس کردن این سامانه را نده.", "Discourage search engines from indexing site": "به موتورهای جستجو اجازه ایندکس کردن این سامانه را نده",
"Change Password": "تغییر رمزعبور", "Change Password": "تغییر رمزعبور",
"Current Password": "رمزعبور فعلی", "Current Password": "رمزعبور فعلی",
"New Password": "رمزعبور جدید", "New Password": "رمزعبور جدید",
@ -98,10 +98,10 @@
"Enable Auth": "فعال سازی تایید هویت", "Enable Auth": "فعال سازی تایید هویت",
"disableauth.message1": "آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?", "disableauth.message1": "آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?",
"disableauth.message2": "این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کرده‌اند</strong>، مانند Cloudflare Access.", "disableauth.message2": "این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کرده‌اند</strong>، مانند Cloudflare Access.",
"Please use this option carefully!": "لطفا از این امکان با دقت استفاده کنید.", "Please use this option carefully!": "لطفا از این امکان با دقت استفاده کنید!",
"Logout": "خروج", "Logout": "خروج",
"Leave": "منصرف شدم", "Leave": "منصرف شدم",
"I understand, please disable": "متوجه هستم، لطفا غیرفعال کنید!", "I understand, please disable": "متوجه هستم، غیرفعال کن",
"Confirm": "تایید", "Confirm": "تایید",
"Yes": "بلی", "Yes": "بلی",
"No": "خیر", "No": "خیر",
@ -126,12 +126,12 @@
"Import": "ورود اطلاعات", "Import": "ورود اطلاعات",
"respTime": "زمان پاسخگویی (میلی‌ثانیه)", "respTime": "زمان پاسخگویی (میلی‌ثانیه)",
"notAvailableShort": "ناموجود", "notAvailableShort": "ناموجود",
"Default enabled": "به صورت پیش‌فرض فعال باشد.", "Default enabled": "به صورت پیش‌فرض فعال باشد",
"Apply on all existing monitors": "بر روی تمامی مانیتور‌های فعلی اعمال شود.", "Apply on all existing monitors": "بر روی تمامی مانیتور‌های فعلی اعمال شود",
"Create": "ایجاد", "Create": "ایجاد",
"Clear Data": "پاکسازی داده‌ها", "Clear Data": "پاکسازی داده‌ها",
"Events": "رخداد‌ها", "Events": "رخداد‌ها",
"Heartbeats": "Heartbeats", "Heartbeats": "ضربان قلب",
"Auto Get": "Auto Get", "Auto Get": "Auto Get",
"backupDescription": "شما میتوانید تمامی مانیتورها و تنظیمات اطلاع‌رسانی‌ها را در قالب یه فایل JSON دریافت کنید.", "backupDescription": "شما میتوانید تمامی مانیتورها و تنظیمات اطلاع‌رسانی‌ها را در قالب یه فایل JSON دریافت کنید.",
"backupDescription2": "البته تاریخچه رخدادها دراین فایل قرار نخواهند داشت.", "backupDescription2": "البته تاریخچه رخدادها دراین فایل قرار نخواهند داشت.",
@ -155,7 +155,7 @@
"Show URI": "نمایش آدرس (URI)", "Show URI": "نمایش آدرس (URI)",
"Tags": "برچسب‌ها", "Tags": "برچسب‌ها",
"Add New below or Select...": "یک مورد جدید اضافه کنید و یا از لیست انتخاب کنید…", "Add New below or Select...": "یک مورد جدید اضافه کنید و یا از لیست انتخاب کنید…",
"Tag with this name already exist.": "یک برچسب با این «نام» از قبل وجود دارد", "Tag with this name already exist.": "یک برچسب با این «نام» از قبل وجود دارد.",
"Tag with this value already exist.": "یک برچسب با این «مقدار» از قبل وجود دارد.", "Tag with this value already exist.": "یک برچسب با این «مقدار» از قبل وجود دارد.",
"color": "رنگ", "color": "رنگ",
"value (optional)": "مقدار (اختیاری)", "value (optional)": "مقدار (اختیاری)",
@ -167,13 +167,13 @@
"Indigo": "نیلی", "Indigo": "نیلی",
"Purple": "بنفش", "Purple": "بنفش",
"Pink": "صورتی", "Pink": "صورتی",
"Search...": "جستجو...", "Search...": "جستجو",
"Avg. Ping": "متوسط پینگ", "Avg. Ping": "متوسط پینگ",
"Avg. Response": "متوسط زمان پاسخ", "Avg. Response": "متوسط زمان پاسخ",
"Entry Page": "صفحه ورودی", "Entry Page": "صفحه ورودی",
"statusPageNothing": "چیزی اینجا نیست، لطفا یک گروه و یا یک مانیتور اضافه کنید!", "statusPageNothing": "چیزی اینجا نیست، لطفا یک گروه و یا یک مانیتور اضافه کنید.",
"No Services": "هیچ سرویسی موجود نیست", "No Services": "هیچ سرویسی موجود نیست",
"All Systems Operational": "تمامی سیستم‌ها عملیاتی هستند!", "All Systems Operational": "تمامی سیستم‌ها عملیاتی هستند",
"Partially Degraded Service": "افت نسبی کیفیت سرویس", "Partially Degraded Service": "افت نسبی کیفیت سرویس",
"Degraded Service": "افت کامل کیفیت سرویس", "Degraded Service": "افت کامل کیفیت سرویس",
"Add Group": "اضافه کردن گروه", "Add Group": "اضافه کردن گروه",
@ -187,7 +187,7 @@
"One record": "یک مورد", "One record": "یک مورد",
"Info": "اطلاعات", "Info": "اطلاعات",
"Powered by": "نیرو گرفته از", "Powered by": "نیرو گرفته از",
"apprise": "Apprise (Support 50+ Notification services)", "apprise": "Apprise (پشتیبانی از 50+ خدمات اعلان)",
"Monitor": "مانیتور | مانتیور ها", "Monitor": "مانیتور | مانتیور ها",
"Help": "کمک", "Help": "کمک",
"Game": "بازی", "Game": "بازی",
@ -197,5 +197,520 @@
"statusMaintenance": "در دست تعمیر", "statusMaintenance": "در دست تعمیر",
"Maintenance": "در حال تعمیر", "Maintenance": "در حال تعمیر",
"General Monitor Type": "حالت مانیتور عمومی", "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": "ایموجی آیکون"
} }

View file

@ -739,5 +739,9 @@
"lunaseaUserID": "Identifiant de l'utilisateur", "lunaseaUserID": "Identifiant de l'utilisateur",
"Add New Tag": "Ajouter une étiquette", "Add New Tag": "Ajouter une étiquette",
"lunaseaTarget": "Cible", "lunaseaTarget": "Cible",
"statusPageRefreshIn": "Actualisation dans: {0}" "statusPageRefreshIn": "Actualisation dans: {0}",
"twilioFromNumber": "Du Nombre",
"twilioToNumber": "Au Nombre",
"twilioAccountSID": "ID du compte",
"twilioAuthToken": "Jeton d'authentification"
} }

View file

@ -727,5 +727,21 @@
"Key Added": "Klucz dodany", "Key Added": "Klucz dodany",
"pagertreeDoNothing": "Nie rób nic", "pagertreeDoNothing": "Nie rób nic",
"wayToGetPagerTreeIntegrationURL": "Po utworzeniu integracji Uptime Kuma w PagerTree, należy skopiować Endpoint. Zobacz pełne szczegóły {0}", "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"
} }

View file

@ -739,5 +739,9 @@
"Add New Tag": "Yeni Etiket Ekle", "Add New Tag": "Yeni Etiket Ekle",
"lunaseaDeviceID": "Cihaz ID", "lunaseaDeviceID": "Cihaz ID",
"lunaseaUserID": "Kullanıcı ID", "lunaseaUserID": "Kullanıcı ID",
"statusPageRefreshIn": "{0} içinde yenilenecek" "statusPageRefreshIn": "{0} içinde yenilenecek",
"twilioAuthToken": "Kimlik Doğrulama Jetonu",
"twilioFromNumber": "Gönderen Numara",
"twilioToNumber": "Alıcı Numara",
"twilioAccountSID": "Hesap ID"
} }

View file

@ -270,7 +270,7 @@
"No Monitors": "کوئی مانیٹر نہیں", "No Monitors": "کوئی مانیٹر نہیں",
"Cancel": "منسوخ کریں", "Cancel": "منسوخ کریں",
"Powered by": "کی طرف سے طاقت", "Powered by": "کی طرف سے طاقت",
"Custom CSS": "حسب ضرورت سی ایس ایس", "Custom CSS": "اپنی مرضی کے مطابق سی ایس ایس",
"deleteProxyMsg": "کیا آپ واقعی اس پراکسی کو تمام مانیٹر کے لیے حذف کرنا چاہتے ہیں؟", "deleteProxyMsg": "کیا آپ واقعی اس پراکسی کو تمام مانیٹر کے لیے حذف کرنا چاہتے ہیں؟",
"enableProxyDescription": "یہ پراکسی مانیٹر کی درخواستوں پر اس وقت تک اثر نہیں کرے گی جب تک کہ اسے فعال نہ کیا جائے۔ آپ ایکٹیویشن اسٹیٹس کے ذریعے تمام مانیٹرس سے پراکسی کو عارضی طور پر غیر فعال کر سکتے ہیں۔", "enableProxyDescription": "یہ پراکسی مانیٹر کی درخواستوں پر اس وقت تک اثر نہیں کرے گی جب تک کہ اسے فعال نہ کیا جائے۔ آپ ایکٹیویشن اسٹیٹس کے ذریعے تمام مانیٹرس سے پراکسی کو عارضی طور پر غیر فعال کر سکتے ہیں۔",
"setAsDefaultProxyDescription": "یہ پراکسی نئے مانیٹرز کے لیے بطور ڈیفالٹ فعال ہو جائے گی۔ آپ اب بھی ہر مانیٹر کے لیے الگ الگ پراکسی کو غیر فعال کر سکتے ہیں۔", "setAsDefaultProxyDescription": "یہ پراکسی نئے مانیٹرز کے لیے بطور ڈیفالٹ فعال ہو جائے گی۔ آپ اب بھی ہر مانیٹر کے لیے الگ الگ پراکسی کو غیر فعال کر سکتے ہیں۔",
@ -409,7 +409,7 @@
"maintenanceStatus-scheduled": "طے شدہ", "maintenanceStatus-scheduled": "طے شدہ",
"maintenanceStatus-ended": "ختم ہوا", "maintenanceStatus-ended": "ختم ہوا",
"recurringInterval": "وقفہ", "recurringInterval": "وقفہ",
"Recurring": "بار بار چلنے والا", "Recurring": "بار چلنے والا",
"strategyManual": "دستی طور پر فعال/غیر فعال", "strategyManual": "دستی طور پر فعال/غیر فعال",
"warningTimezone": "یہ سرور کا ٹائم زون استعمال کر رہا ہے", "warningTimezone": "یہ سرور کا ٹائم زون استعمال کر رہا ہے",
"weekdayShortMon": "پیر", "weekdayShortMon": "پیر",
@ -427,5 +427,25 @@
"lastDay4": "مہینے کا چوتھا آخری دن", "lastDay4": "مہینے کا چوتھا آخری دن",
"pauseMaintenanceMsg": "کیا آپ واقعی روکنا چاہتے ہیں؟", "pauseMaintenanceMsg": "کیا آپ واقعی روکنا چاہتے ہیں؟",
"No Maintenance": "کوئی دیکھ بھال نہیں", "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": "آئیکن یو آر ایل"
} }

View file

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

View file

@ -3,7 +3,7 @@
<div> <div>
<h1 class="mb-3">{{ pageName }}</h1> <h1 class="mb-3">{{ pageName }}</h1>
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<div class="shadow-box"> <div class="shadow-box shadow-box-with-fixed-bottom-bar">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h2 class="mb-2">{{ $t("General") }}</h2> <h2 class="mb-2">{{ $t("General") }}</h2>
@ -907,7 +907,7 @@ message HealthCheckResponse {
interval: 60, interval: 60,
retryInterval: this.interval, retryInterval: this.interval,
resendInterval: 0, resendInterval: 0,
maxretries: 0, maxretries: 1,
notificationIDList: {}, notificationIDList: {},
ignoreTls: false, ignoreTls: false,
upsideDown: false, upsideDown: false,
@ -1102,31 +1102,7 @@ message HealthCheckResponse {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../assets/vars.scss"; @import "../assets/vars.scss";
$padding: 20px;
.shadow-box {
padding-top: $padding;
padding-bottom: 0;
padding-right: $padding;
padding-left: $padding;
}
textarea { textarea {
min-height: 200px; 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> </style>

View file

@ -923,7 +923,11 @@ export default {
* @returns {string} Sanitized HTML * @returns {string} Sanitized HTML
*/ */
maintenanceHTML(description) { maintenanceHTML(description) {
if (description) {
return DOMPurify.sanitize(marked(description)); return DOMPurify.sanitize(marked(description));
} else {
return "";
}
}, },
} }